taro-bluetooth-print 2.2.0 → 2.3.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 (49) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +128 -22
  3. package/dist/index.cjs.js +1 -1
  4. package/dist/index.es.js +1 -6995
  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 +6 -34
  8. package/dist/types/adapters/BaiduAdapter.d.ts +6 -34
  9. package/dist/types/adapters/BaseAdapter.d.ts +112 -1
  10. package/dist/types/adapters/ByteDanceAdapter.d.ts +6 -34
  11. package/dist/types/adapters/TaroAdapter.d.ts +6 -34
  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 +0 -1
  15. package/dist/types/drivers/EscPos.d.ts +0 -1
  16. package/dist/types/drivers/TsplDriver.d.ts +251 -0
  17. package/dist/types/encoding/gbk-data.d.ts +12 -0
  18. package/dist/types/encoding/gbk-table.d.ts +5 -1
  19. package/dist/types/index.d.ts +5 -0
  20. package/dist/types/plugins/PluginManager.d.ts +87 -0
  21. package/dist/types/plugins/builtin/LoggingPlugin.d.ts +14 -0
  22. package/dist/types/plugins/builtin/RetryPlugin.d.ts +18 -0
  23. package/dist/types/plugins/index.d.ts +7 -0
  24. package/dist/types/plugins/types.d.ts +97 -0
  25. package/dist/types/services/CommandBuilder.d.ts +6 -1
  26. package/dist/types/services/ConnectionManager.d.ts +0 -1
  27. package/dist/types/services/PrintJobManager.d.ts +6 -2
  28. package/dist/types/services/interfaces/index.d.ts +0 -1
  29. package/dist/types/template/TemplateEngine.d.ts +0 -1
  30. package/package.json +16 -18
  31. package/src/adapters/AlipayAdapter.ts +8 -314
  32. package/src/adapters/BaiduAdapter.ts +8 -312
  33. package/src/adapters/BaseAdapter.ts +366 -0
  34. package/src/adapters/ByteDanceAdapter.ts +8 -316
  35. package/src/adapters/TaroAdapter.ts +8 -367
  36. package/src/core/EventEmitter.ts +9 -6
  37. package/src/drivers/TsplDriver.ts +417 -0
  38. package/src/encoding/gbk-data.ts +1911 -0
  39. package/src/encoding/gbk-table.ts +22 -498
  40. package/src/index.ts +14 -0
  41. package/src/plugins/PluginManager.ts +193 -0
  42. package/src/plugins/builtin/LoggingPlugin.ts +97 -0
  43. package/src/plugins/builtin/RetryPlugin.ts +109 -0
  44. package/src/plugins/index.ts +10 -0
  45. package/src/plugins/types.ts +119 -0
  46. package/src/preview/PreviewRenderer.ts +7 -1
  47. package/src/queue/PrintQueue.ts +10 -6
  48. package/src/services/CommandBuilder.ts +30 -0
  49. package/src/services/PrintJobManager.ts +51 -35
@@ -0,0 +1,193 @@
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(`Registering plugin: ${plugin.name}${plugin.version ? ` v${plugin.version}` : ''}`);
33
+
34
+ if (plugin.init) {
35
+ await plugin.init(options);
36
+ }
37
+
38
+ this.plugins.set(plugin.name, plugin);
39
+ this.logger.debug(`Plugin registered: ${plugin.name}`);
40
+ }
41
+
42
+ /**
43
+ * Unregister a plugin
44
+ * @param name - Plugin name to unregister
45
+ */
46
+ async unregister(name: string): Promise<void> {
47
+ const plugin = this.plugins.get(name);
48
+ if (!plugin) {
49
+ this.logger.warn(`Plugin not found: ${name}`);
50
+ return;
51
+ }
52
+
53
+ if (plugin.destroy) {
54
+ await plugin.destroy();
55
+ }
56
+
57
+ this.plugins.delete(name);
58
+ this.logger.info(`Plugin unregistered: ${name}`);
59
+ }
60
+
61
+ /**
62
+ * Get a registered plugin
63
+ * @param name - Plugin name
64
+ * @returns Plugin instance or undefined
65
+ */
66
+ get(name: string): Plugin | undefined {
67
+ return this.plugins.get(name);
68
+ }
69
+
70
+ /**
71
+ * Get all registered plugin names
72
+ * @returns Array of plugin names
73
+ */
74
+ getNames(): string[] {
75
+ return Array.from(this.plugins.keys());
76
+ }
77
+
78
+ /**
79
+ * Check if a plugin is registered
80
+ * @param name - Plugin name
81
+ * @returns True if registered
82
+ */
83
+ has(name: string): boolean {
84
+ return this.plugins.has(name);
85
+ }
86
+
87
+ /**
88
+ * Execute a hook across all plugins
89
+ * @param hookName - Name of the hook to execute
90
+ * @param args - Arguments to pass to the hook
91
+ * @returns Result from hooks (last non-void result)
92
+ */
93
+ async executeHook<K extends keyof PluginHooks>(
94
+ hookName: K,
95
+ ...args: Parameters<NonNullable<PluginHooks[K]>>
96
+ ): Promise<unknown> {
97
+ let result: unknown = undefined;
98
+
99
+ for (const [name, plugin] of this.plugins) {
100
+ const hook = plugin.hooks[hookName];
101
+ if (hook) {
102
+ try {
103
+ // @ts-expect-error - TypeScript can't infer the correct types here
104
+ const hookResult = await hook(...args);
105
+ if (hookResult !== undefined) {
106
+ result = hookResult;
107
+ }
108
+ } catch (error) {
109
+ this.logger.error(`Plugin "${name}" hook "${hookName}" failed:`, error);
110
+ // Continue to next plugin
111
+ }
112
+ }
113
+ }
114
+
115
+ return result;
116
+ }
117
+
118
+ /**
119
+ * Execute beforeConnect hooks
120
+ */
121
+ async beforeConnect(deviceId: string): Promise<string> {
122
+ const result = await this.executeHook('beforeConnect', deviceId);
123
+ return (result as string) || deviceId;
124
+ }
125
+
126
+ /**
127
+ * Execute afterConnect hooks
128
+ */
129
+ async afterConnect(deviceId: string): Promise<void> {
130
+ await this.executeHook('afterConnect', deviceId);
131
+ }
132
+
133
+ /**
134
+ * Execute beforeDisconnect hooks
135
+ */
136
+ async beforeDisconnect(deviceId: string): Promise<void> {
137
+ await this.executeHook('beforeDisconnect', deviceId);
138
+ }
139
+
140
+ /**
141
+ * Execute afterDisconnect hooks
142
+ */
143
+ async afterDisconnect(deviceId: string): Promise<void> {
144
+ await this.executeHook('afterDisconnect', deviceId);
145
+ }
146
+
147
+ /**
148
+ * Execute beforePrint hooks
149
+ */
150
+ async beforePrint(buffer: Uint8Array): Promise<Uint8Array> {
151
+ const result = await this.executeHook('beforePrint', buffer);
152
+ return (result as Uint8Array) || buffer;
153
+ }
154
+
155
+ /**
156
+ * Execute afterPrint hooks
157
+ */
158
+ async afterPrint(bytesSent: number): Promise<void> {
159
+ await this.executeHook('afterPrint', bytesSent);
160
+ }
161
+
162
+ /**
163
+ * Execute onError hooks
164
+ * @returns True if error should be suppressed
165
+ */
166
+ async onError(error: BluetoothPrintError): Promise<boolean> {
167
+ const result = await this.executeHook('onError', error);
168
+ return result === true;
169
+ }
170
+
171
+ /**
172
+ * Execute onStateChange hooks
173
+ */
174
+ async onStateChange(state: PrinterState, previousState: PrinterState): Promise<void> {
175
+ await this.executeHook('onStateChange', state, previousState);
176
+ }
177
+
178
+ /**
179
+ * Execute onProgress hooks
180
+ */
181
+ async onProgress(sent: number, total: number): Promise<void> {
182
+ await this.executeHook('onProgress', sent, total);
183
+ }
184
+
185
+ /**
186
+ * Clear all plugins
187
+ */
188
+ async clear(): Promise<void> {
189
+ for (const name of this.plugins.keys()) {
190
+ await this.unregister(name);
191
+ }
192
+ }
193
+ }
@@ -0,0 +1,97 @@
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(`${formatTime()}Print complete: ${bytesSent} bytes in ${elapsed}ms (${speed} B/s)`);
69
+ },
70
+
71
+ onError: (error) => {
72
+ logger.error(`${formatTime()}Error [${error.code}]: ${error.message}`);
73
+ return false; // Don't suppress the error
74
+ },
75
+
76
+ onStateChange: (state, previousState) => {
77
+ logger.debug(`${formatTime()}State: ${previousState} → ${state}`);
78
+ },
79
+
80
+ onProgress: (sent, total) => {
81
+ if (opts.logProgress) {
82
+ const percent = ((sent / total) * 100).toFixed(1);
83
+ logger.debug(`${formatTime()}Progress: ${sent}/${total} (${percent}%)`);
84
+ }
85
+ },
86
+ },
87
+
88
+ init: () => {
89
+ Logger.setLevel(opts.level);
90
+ logger.info('Logging plugin initialized');
91
+ },
92
+
93
+ destroy: () => {
94
+ logger.info('Logging plugin destroyed');
95
+ },
96
+ };
97
+ };
@@ -0,0 +1,109 @@
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 (
46
+ retryCount < opts.maxRetries &&
47
+ opts.retryableErrors.includes(error.code)
48
+ );
49
+ };
50
+
51
+ const sleep = (ms: number): Promise<void> => {
52
+ return new Promise(resolve => setTimeout(resolve, ms));
53
+ };
54
+
55
+ return {
56
+ name: 'retry',
57
+ version: '1.0.0',
58
+ description: 'Automatic retry with exponential backoff',
59
+
60
+ hooks: {
61
+ beforeConnect: () => {
62
+ // Reset retry state on new connection attempt
63
+ retryCount = 0;
64
+ currentDelay = opts.initialDelay;
65
+ },
66
+
67
+ beforePrint: () => {
68
+ // Reset retry state on new print job
69
+ retryCount = 0;
70
+ currentDelay = opts.initialDelay;
71
+ },
72
+
73
+ onError: async (error: BluetoothPrintError) => {
74
+ if (shouldRetry(error)) {
75
+ retryCount++;
76
+ logger.warn(
77
+ `Retryable error occurred (attempt ${retryCount}/${opts.maxRetries}): ${error.code}`
78
+ );
79
+ logger.info(`Waiting ${currentDelay}ms before retry...`);
80
+
81
+ await sleep(currentDelay);
82
+
83
+ // Exponential backoff
84
+ currentDelay = Math.min(
85
+ currentDelay * opts.backoffMultiplier,
86
+ opts.maxDelay
87
+ );
88
+
89
+ // Note: Returning false means error is not suppressed
90
+ // The actual retry logic would need to be implemented in the printer
91
+ // This plugin mainly provides the delay and logging
92
+ return false;
93
+ }
94
+
95
+ if (retryCount > 0) {
96
+ logger.error(
97
+ `Failed after ${retryCount} retries: ${error.code} - ${error.message}`
98
+ );
99
+ }
100
+
101
+ return false;
102
+ },
103
+ },
104
+
105
+ init: () => {
106
+ logger.info(`Retry plugin initialized (max: ${opts.maxRetries}, delay: ${opts.initialDelay}ms)`);
107
+ },
108
+ };
109
+ };
@@ -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';
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Plugin System Types
3
+ * Defines interfaces for extending BluetoothPrinter functionality
4
+ */
5
+
6
+ import { BluetoothPrintError } from '@/errors/BluetoothError';
7
+ import { PrinterState } from '@/types';
8
+
9
+ /**
10
+ * Plugin lifecycle hooks
11
+ */
12
+ export interface PluginHooks {
13
+ /**
14
+ * Called before connecting to a device
15
+ * @param deviceId - Target device ID
16
+ * @returns Modified device ID or void
17
+ */
18
+ beforeConnect?: (deviceId: string) => string | void | Promise<string | void>;
19
+
20
+ /**
21
+ * Called after successful connection
22
+ * @param deviceId - Connected device ID
23
+ */
24
+ afterConnect?: (deviceId: string) => void | Promise<void>;
25
+
26
+ /**
27
+ * Called before disconnecting
28
+ * @param deviceId - Device ID to disconnect
29
+ */
30
+ beforeDisconnect?: (deviceId: string) => void | Promise<void>;
31
+
32
+ /**
33
+ * Called after disconnection
34
+ * @param deviceId - Disconnected device ID
35
+ */
36
+ afterDisconnect?: (deviceId: string) => void | Promise<void>;
37
+
38
+ /**
39
+ * Called before sending print data
40
+ * @param buffer - Data buffer to send
41
+ * @returns Modified buffer or void
42
+ */
43
+ beforePrint?: (buffer: Uint8Array) => Uint8Array | void | Promise<Uint8Array | void>;
44
+
45
+ /**
46
+ * Called after print job completes
47
+ * @param bytesSent - Total bytes sent
48
+ */
49
+ afterPrint?: (bytesSent: number) => void | Promise<void>;
50
+
51
+ /**
52
+ * Called when an error occurs
53
+ * @param error - The error that occurred
54
+ * @returns Whether to suppress the error (true = suppress)
55
+ */
56
+ onError?: (error: BluetoothPrintError) => boolean | void | Promise<boolean | void>;
57
+
58
+ /**
59
+ * Called when printer state changes
60
+ * @param state - New printer state
61
+ * @param previousState - Previous state
62
+ */
63
+ onStateChange?: (state: PrinterState, previousState: PrinterState) => void | Promise<void>;
64
+
65
+ /**
66
+ * Called during print progress
67
+ * @param sent - Bytes sent
68
+ * @param total - Total bytes
69
+ */
70
+ onProgress?: (sent: number, total: number) => void | Promise<void>;
71
+ }
72
+
73
+ /**
74
+ * Plugin configuration options
75
+ */
76
+ export interface PluginOptions {
77
+ [key: string]: unknown;
78
+ }
79
+
80
+ /**
81
+ * Plugin interface
82
+ */
83
+ export interface Plugin {
84
+ /**
85
+ * Unique plugin name
86
+ */
87
+ name: string;
88
+
89
+ /**
90
+ * Plugin version
91
+ */
92
+ version?: string;
93
+
94
+ /**
95
+ * Plugin description
96
+ */
97
+ description?: string;
98
+
99
+ /**
100
+ * Plugin hooks
101
+ */
102
+ hooks: PluginHooks;
103
+
104
+ /**
105
+ * Plugin initialization
106
+ * @param options - Plugin options
107
+ */
108
+ init?: (options?: PluginOptions) => void | Promise<void>;
109
+
110
+ /**
111
+ * Plugin cleanup
112
+ */
113
+ destroy?: () => void | Promise<void>;
114
+ }
115
+
116
+ /**
117
+ * Plugin factory function type
118
+ */
119
+ export type PluginFactory = (options?: PluginOptions) => Plugin;
@@ -236,7 +236,13 @@ export class PreviewRenderer implements IPreviewRenderer {
236
236
  case 0x61: // ESC a - Alignment
237
237
  flushText();
238
238
  if (i + 2 < commands.length) {
239
- state.alignment = commands[i + 2] as Alignment;
239
+ const alignValue = commands[i + 2];
240
+ state.alignment =
241
+ alignValue === Alignment.LEFT ||
242
+ alignValue === Alignment.CENTER ||
243
+ alignValue === Alignment.RIGHT
244
+ ? alignValue
245
+ : Alignment.LEFT;
240
246
  }
241
247
  i += 3;
242
248
  break;
@@ -385,12 +385,16 @@ export class PrintQueue implements IPrintQueue {
385
385
  if (!job || job.status !== PrintJobStatus.PENDING) continue;
386
386
 
387
387
  this.activeJobs++;
388
- void this.executeJob(job).finally(() => {
389
- this.activeJobs--;
390
- if (!this.isPaused) {
391
- this.processQueue();
392
- }
393
- });
388
+ this.executeJob(job)
389
+ .catch(error => {
390
+ this.logger.error(`Unhandled error in job execution for ${job.id}:`, error);
391
+ })
392
+ .finally(() => {
393
+ this.activeJobs--;
394
+ if (!this.isPaused) {
395
+ this.processQueue();
396
+ }
397
+ });
394
398
  }
395
399
 
396
400
  this.isProcessing = false;