taro-bluetooth-print 2.8.4 → 2.9.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.
- package/CHANGELOG.md +33 -0
- package/README.md +53 -4
- package/dist/index.cjs.js +1 -1
- package/dist/index.es.js +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/types/core/BluetoothPrinter.d.ts +21 -4
- package/dist/types/core/di/Container.d.ts +84 -0
- package/dist/types/core/di/Tokens.d.ts +29 -0
- package/dist/types/core/di/index.d.ts +3 -0
- package/dist/types/core/event/EventBus.d.ts +66 -0
- package/dist/types/core/event/index.d.ts +2 -0
- package/dist/types/core/index.d.ts +5 -4
- package/dist/types/core/plugin/PluginManager.d.ts +64 -0
- package/dist/types/core/plugin/index.d.ts +2 -0
- package/dist/types/device/MultiPrinterManager.d.ts +2 -0
- package/dist/types/errors/CommandBuildError.d.ts +40 -0
- package/dist/types/errors/ConnectionError.d.ts +45 -0
- package/dist/types/errors/PrintJobError.d.ts +42 -0
- package/dist/types/errors/index.d.ts +9 -0
- package/dist/types/factory/PrinterFactory.d.ts +108 -0
- package/dist/types/factory/di-factory.d.ts +52 -0
- package/dist/types/factory/index.d.ts +6 -0
- package/dist/types/index.d.ts +9 -1
- package/dist/types/providers/ServiceProvider.d.ts +56 -0
- package/dist/types/providers/index.d.ts +2 -0
- package/dist/types/services/interfaces/ICommandBuilder.d.ts +123 -0
- package/dist/types/services/interfaces/IConnectionManager.d.ts +49 -0
- package/dist/types/services/interfaces/IPrintJobManager.d.ts +67 -0
- package/dist/types/services/interfaces/index.d.ts +6 -233
- package/dist/types/template/TemplateEngine.d.ts +24 -68
- package/dist/types/template/engines/TemplateRenderer.d.ts +71 -0
- package/dist/types/template/parsers/TemplateParser.d.ts +23 -0
- package/dist/types/utils/index.d.ts +8 -0
- package/dist/types/utils/logger.d.ts +4 -3
- package/dist/types/utils/outputLimiter.d.ts +87 -0
- package/dist/types/utils/validation.d.ts +11 -309
- package/dist/types/utils/validators/array.d.ts +19 -0
- package/dist/types/utils/validators/buffer.d.ts +18 -0
- package/dist/types/utils/validators/chain.d.ts +31 -0
- package/dist/types/utils/validators/common.d.ts +22 -0
- package/dist/types/utils/validators/number.d.ts +20 -0
- package/dist/types/utils/validators/object.d.ts +24 -0
- package/dist/types/utils/validators/printer.d.ts +40 -0
- package/dist/types/utils/validators/types.d.ts +125 -0
- package/dist/types/utils/validators/uuid.d.ts +23 -0
- package/package.json +1 -1
- package/src/core/BluetoothPrinter.ts +48 -50
- package/src/core/di/Container.ts +330 -0
- package/src/core/di/Tokens.ts +45 -0
- package/src/core/di/index.ts +3 -0
- package/src/core/event/EventBus.ts +251 -0
- package/src/core/event/index.ts +2 -0
- package/src/core/index.ts +10 -4
- package/src/core/plugin/PluginManager.ts +161 -0
- package/src/core/plugin/index.ts +2 -0
- package/src/device/MultiPrinterManager.ts +15 -6
- package/src/errors/CommandBuildError.ts +72 -0
- package/src/errors/ConnectionError.ts +78 -0
- package/src/errors/PrintJobError.ts +75 -0
- package/src/errors/index.ts +10 -0
- package/src/factory/PrinterFactory.ts +139 -0
- package/src/factory/di-factory.ts +61 -0
- package/src/factory/index.ts +12 -0
- package/src/index.ts +61 -1
- package/src/providers/ServiceProvider.ts +213 -0
- package/src/providers/index.ts +2 -0
- package/src/services/interfaces/ICommandBuilder.ts +145 -0
- package/src/services/interfaces/IConnectionManager.ts +58 -0
- package/src/services/interfaces/IPrintJobManager.ts +83 -0
- package/src/services/interfaces/index.ts +5 -265
- package/src/template/TemplateEngine.ts +27 -792
- package/src/template/engines/TemplateRenderer.ts +762 -0
- package/src/template/parsers/TemplateParser.ts +94 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/logger.ts +17 -4
- package/src/utils/outputLimiter.ts +227 -0
- package/src/utils/validation.ts +21 -1138
- package/src/utils/validators/array.ts +95 -0
- package/src/utils/validators/buffer.ts +81 -0
- package/src/utils/validators/chain.ts +181 -0
- package/src/utils/validators/common.ts +216 -0
- package/src/utils/validators/number.ts +101 -0
- package/src/utils/validators/object.ts +63 -0
- package/src/utils/validators/printer.ts +294 -0
- package/src/utils/validators/types.ts +105 -0
- package/src/utils/validators/uuid.ts +49 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 服务标识符(Tokens)
|
|
3
|
+
* 用于依赖注入的常量定义
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// 适配器相关
|
|
7
|
+
export const ADAPTER_TOKEN = Symbol.for('Adapter');
|
|
8
|
+
export const ADAPTER_FACTORY_TOKEN = Symbol.for('AdapterFactory');
|
|
9
|
+
|
|
10
|
+
// 驱动相关
|
|
11
|
+
export const DRIVER_TOKEN = Symbol.for('Driver');
|
|
12
|
+
export const DRIVER_FACTORY_TOKEN = Symbol.for('DriverFactory');
|
|
13
|
+
|
|
14
|
+
// 服务相关
|
|
15
|
+
export const DEVICE_MANAGER_TOKEN = Symbol.for('DeviceManager');
|
|
16
|
+
export const CONNECTION_MANAGER_TOKEN = Symbol.for('ConnectionManager');
|
|
17
|
+
export const PRINT_JOB_MANAGER_TOKEN = Symbol.for('PrintJobManager');
|
|
18
|
+
export const PRINT_QUEUE_TOKEN = Symbol.for('PrintQueue');
|
|
19
|
+
export const OFFLINE_CACHE_TOKEN = Symbol.for('OfflineCache');
|
|
20
|
+
export const CONFIG_MANAGER_TOKEN = Symbol.for('ConfigManager');
|
|
21
|
+
|
|
22
|
+
// 新增服务令牌
|
|
23
|
+
export const COMMAND_BUILDER_TOKEN = Symbol.for('CommandBuilder');
|
|
24
|
+
export const PRINTER_STATUS_TOKEN = Symbol.for('PrinterStatus');
|
|
25
|
+
export const PRINT_HISTORY_TOKEN = Symbol.for('PrintHistory');
|
|
26
|
+
export const PRINT_STATISTICS_TOKEN = Symbol.for('PrintStatistics');
|
|
27
|
+
export const CLOUD_PRINT_MANAGER_TOKEN = Symbol.for('CloudPrintManager');
|
|
28
|
+
export const SCHEDULED_RETRY_MANAGER_TOKEN = Symbol.for('ScheduledRetryManager');
|
|
29
|
+
export const BATCH_PRINT_MANAGER_TOKEN = Symbol.for('BatchPrintManager');
|
|
30
|
+
|
|
31
|
+
// 工具服务
|
|
32
|
+
export const LOGGER_TOKEN = Symbol.for('Logger');
|
|
33
|
+
export const ENCODING_SERVICE_TOKEN = Symbol.for('EncodingService');
|
|
34
|
+
export const IMAGE_PROCESSING_TOKEN = Symbol.for('ImageProcessing');
|
|
35
|
+
export const BARCODE_GENERATOR_TOKEN = Symbol.for('BarcodeGenerator');
|
|
36
|
+
export const TEMPLATE_ENGINE_TOKEN = Symbol.for('TemplateEngine');
|
|
37
|
+
|
|
38
|
+
// 事件系统
|
|
39
|
+
export const EVENT_BUS_TOKEN = Symbol.for('EventBus');
|
|
40
|
+
|
|
41
|
+
// 插件系统
|
|
42
|
+
export const PLUGIN_MANAGER_TOKEN = Symbol.for('PluginManager');
|
|
43
|
+
|
|
44
|
+
// 性能监控
|
|
45
|
+
export const PERFORMANCE_MONITOR_TOKEN = Symbol.for('PerformanceMonitor');
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 事件总线 - 提供强大的事件驱动能力
|
|
3
|
+
* 支持事件订阅、发布、一次性监听、异步处理等
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
7
|
+
export type EventHandler<T = any> = (payload: T) => void | Promise<void>;
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9
|
+
export type EventFilter<T = any> = (payload: T) => boolean;
|
|
10
|
+
|
|
11
|
+
export interface EventSubscription {
|
|
12
|
+
unsubscribe(): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface EventOptions {
|
|
16
|
+
/** 优先级,数字越小优先级越高 */
|
|
17
|
+
priority?: number;
|
|
18
|
+
/** 过滤器 */
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
+
filter?: EventFilter<any>;
|
|
21
|
+
/** 是否只监听一次 */
|
|
22
|
+
once?: boolean;
|
|
23
|
+
/** 超时时间(毫秒) */
|
|
24
|
+
timeout?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
28
|
+
interface EventListener<T = any> {
|
|
29
|
+
handler: EventHandler<T>;
|
|
30
|
+
options: Required<Pick<EventOptions, 'priority'>> & Omit<EventOptions, 'priority'>;
|
|
31
|
+
id: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class EventBus {
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
36
|
+
private listeners = new Map<string | symbol, EventListener<any>[]>();
|
|
37
|
+
private listenerIdCounter = 0;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 订阅事件
|
|
41
|
+
*/
|
|
42
|
+
on<T>(
|
|
43
|
+
event: string | symbol,
|
|
44
|
+
handler: EventHandler<T>,
|
|
45
|
+
options: EventOptions = {}
|
|
46
|
+
): EventSubscription {
|
|
47
|
+
const id = this.generateListenerId();
|
|
48
|
+
const listener: EventListener<T> = {
|
|
49
|
+
handler,
|
|
50
|
+
options: {
|
|
51
|
+
priority: options.priority ?? 100,
|
|
52
|
+
filter: options.filter,
|
|
53
|
+
once: options.once ?? false,
|
|
54
|
+
timeout: options.timeout,
|
|
55
|
+
},
|
|
56
|
+
id,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const eventKey = String(event);
|
|
60
|
+
if (!this.listeners.has(eventKey)) {
|
|
61
|
+
this.listeners.set(eventKey, []);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const listeners = this.listeners.get(eventKey)!;
|
|
65
|
+
listeners.push(listener);
|
|
66
|
+
|
|
67
|
+
// 按优先级排序
|
|
68
|
+
listeners.sort((a, b) => a.options.priority - b.options.priority);
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
unsubscribe: () => {
|
|
72
|
+
this.off(event, handler);
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 只监听一次
|
|
79
|
+
*/
|
|
80
|
+
once<T>(
|
|
81
|
+
event: string | symbol,
|
|
82
|
+
handler: EventHandler<T>,
|
|
83
|
+
options: Omit<EventOptions, 'once'> = {}
|
|
84
|
+
): EventSubscription {
|
|
85
|
+
return this.on(event, handler, { ...options, once: true });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 取消订阅
|
|
90
|
+
*/
|
|
91
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
92
|
+
off<T>(event: string | symbol, handler: EventHandler<T>): void {
|
|
93
|
+
const eventKey = String(event);
|
|
94
|
+
const listeners = this.listeners.get(eventKey);
|
|
95
|
+
if (!listeners) return;
|
|
96
|
+
|
|
97
|
+
const index = listeners.findIndex(l => l.handler === handler);
|
|
98
|
+
if (index !== -1) {
|
|
99
|
+
listeners.splice(index, 1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (listeners.length === 0) {
|
|
103
|
+
this.listeners.delete(eventKey);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 发布事件
|
|
109
|
+
*/
|
|
110
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
111
|
+
async emit<T>(event: string | symbol, payload: T): Promise<void> {
|
|
112
|
+
const eventKey = String(event);
|
|
113
|
+
const listeners = this.listeners.get(eventKey);
|
|
114
|
+
if (!listeners || listeners.length === 0) return;
|
|
115
|
+
|
|
116
|
+
const toRemove: number[] = [];
|
|
117
|
+
|
|
118
|
+
for (let i = 0; i < listeners.length; i++) {
|
|
119
|
+
const listener = listeners[i];
|
|
120
|
+
if (!listener) continue;
|
|
121
|
+
|
|
122
|
+
// 应用过滤器
|
|
123
|
+
if (listener.options.filter && !listener.options.filter(payload)) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
if (listener.options.timeout) {
|
|
129
|
+
await this.executeWithTimeout(listener.handler, payload, listener.options.timeout);
|
|
130
|
+
} else {
|
|
131
|
+
await listener.handler(payload);
|
|
132
|
+
}
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error(`Error in event handler for "${eventKey}":`, error);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 标记一次性监听器
|
|
138
|
+
if (listener.options.once) {
|
|
139
|
+
toRemove.push(i);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 移除一次性监听器
|
|
144
|
+
for (let i = toRemove.length - 1; i >= 0; i--) {
|
|
145
|
+
const index = toRemove[i];
|
|
146
|
+
if (index !== undefined) {
|
|
147
|
+
listeners.splice(index, 1);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (listeners.length === 0) {
|
|
152
|
+
this.listeners.delete(eventKey);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 同步发布事件(不等待处理完成)
|
|
158
|
+
*/
|
|
159
|
+
emitSync<T>(event: string | symbol, payload: T): void {
|
|
160
|
+
this.emit(event, payload).catch(error => {
|
|
161
|
+
console.error(`Error in async event handler for "${String(event)}":`, error);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* 等待特定事件
|
|
167
|
+
*/
|
|
168
|
+
waitFor<T>(
|
|
169
|
+
event: string | symbol,
|
|
170
|
+
timeout?: number,
|
|
171
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
172
|
+
_filter?: EventFilter<T>
|
|
173
|
+
): Promise<T> {
|
|
174
|
+
return new Promise((resolve, reject) => {
|
|
175
|
+
const timer = timeout
|
|
176
|
+
? setTimeout(() => {
|
|
177
|
+
subscription.unsubscribe();
|
|
178
|
+
reject(new Error(`Timeout waiting for event "${String(event)}"`));
|
|
179
|
+
}, timeout)
|
|
180
|
+
: null;
|
|
181
|
+
|
|
182
|
+
const subscription = this.once<T>(event, payload => {
|
|
183
|
+
if (timer) clearTimeout(timer);
|
|
184
|
+
resolve(payload);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 检查是否有监听器
|
|
191
|
+
*/
|
|
192
|
+
hasListeners(event: string | symbol): boolean {
|
|
193
|
+
const listeners = this.listeners.get(String(event));
|
|
194
|
+
return !!listeners && listeners.length > 0;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* 获取监听器数量
|
|
199
|
+
*/
|
|
200
|
+
listenerCount(event: string | symbol): number {
|
|
201
|
+
const listeners = this.listeners.get(String(event));
|
|
202
|
+
return listeners?.length ?? 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* 移除所有监听器
|
|
207
|
+
*/
|
|
208
|
+
removeAllListeners(event?: string | symbol): void {
|
|
209
|
+
if (event) {
|
|
210
|
+
this.listeners.delete(String(event));
|
|
211
|
+
} else {
|
|
212
|
+
this.listeners.clear();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* 获取所有事件名称
|
|
218
|
+
*/
|
|
219
|
+
eventNames(): (string | symbol)[] {
|
|
220
|
+
return Array.from(this.listeners.keys());
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private generateListenerId(): string {
|
|
224
|
+
return `listener_${++this.listenerIdCounter}`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private executeWithTimeout<T>(
|
|
228
|
+
handler: EventHandler<T>,
|
|
229
|
+
payload: T,
|
|
230
|
+
timeout: number
|
|
231
|
+
): Promise<void> {
|
|
232
|
+
return new Promise((resolve, reject) => {
|
|
233
|
+
const timer = setTimeout(() => {
|
|
234
|
+
reject(new Error(`Event handler timed out after ${timeout}ms`));
|
|
235
|
+
}, timeout);
|
|
236
|
+
|
|
237
|
+
Promise.resolve(handler(payload))
|
|
238
|
+
.then(() => {
|
|
239
|
+
clearTimeout(timer);
|
|
240
|
+
resolve();
|
|
241
|
+
})
|
|
242
|
+
.catch(error => {
|
|
243
|
+
clearTimeout(timer);
|
|
244
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 全局事件总线实例
|
|
251
|
+
export const globalEventBus = new EventBus();
|
package/src/core/index.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 核心架构模块
|
|
3
|
+
* 提供依赖注入、事件总线、插件管理等核心能力
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
export
|
|
7
|
-
|
|
6
|
+
// 依赖注入
|
|
7
|
+
export * from './di';
|
|
8
|
+
|
|
9
|
+
// 事件总线
|
|
10
|
+
export * from './event';
|
|
11
|
+
|
|
12
|
+
// 插件管理
|
|
13
|
+
export * from './plugin';
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import type { EventBus } from '../event/EventBus';
|
|
2
|
+
import type { Container } from '../di/Container';
|
|
3
|
+
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5
|
+
export interface PluginContext {
|
|
6
|
+
/** 事件总线 */
|
|
7
|
+
eventBus: EventBus;
|
|
8
|
+
/** DI容器 */
|
|
9
|
+
container: Container;
|
|
10
|
+
/** 配置 */
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12
|
+
config: Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface Plugin {
|
|
16
|
+
/** 插件名称 */
|
|
17
|
+
name: string;
|
|
18
|
+
/** 插件版本 */
|
|
19
|
+
version: string;
|
|
20
|
+
/** 插件依赖 */
|
|
21
|
+
dependencies?: string[];
|
|
22
|
+
/** 安装插件 */
|
|
23
|
+
install(context: PluginContext): void | Promise<void>;
|
|
24
|
+
/** 卸载插件 */
|
|
25
|
+
uninstall?(context: PluginContext): void | Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PluginRegistration {
|
|
29
|
+
plugin: Plugin;
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
31
|
+
config: Record<string, unknown>;
|
|
32
|
+
installed: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class PluginManager {
|
|
36
|
+
private plugins = new Map<string, PluginRegistration>();
|
|
37
|
+
private context: PluginContext;
|
|
38
|
+
|
|
39
|
+
constructor(context: PluginContext) {
|
|
40
|
+
this.context = context;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 注册插件
|
|
45
|
+
*/
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
47
|
+
register(plugin: Plugin, config: Record<string, unknown> = {}): void {
|
|
48
|
+
if (this.plugins.has(plugin.name)) {
|
|
49
|
+
throw new Error(`Plugin "${plugin.name}" is already registered`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.plugins.set(plugin.name, {
|
|
53
|
+
plugin,
|
|
54
|
+
config,
|
|
55
|
+
installed: false,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 安装插件
|
|
61
|
+
*/
|
|
62
|
+
async install(pluginName: string): Promise<void> {
|
|
63
|
+
const registration = this.plugins.get(pluginName);
|
|
64
|
+
if (!registration) {
|
|
65
|
+
throw new Error(`Plugin "${pluginName}" not found`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (registration.installed) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 检查依赖
|
|
73
|
+
if (registration.plugin.dependencies) {
|
|
74
|
+
for (const dep of registration.plugin.dependencies) {
|
|
75
|
+
if (!this.plugins.has(dep)) {
|
|
76
|
+
throw new Error(`Plugin "${pluginName}" depends on "${dep}" which is not registered`);
|
|
77
|
+
}
|
|
78
|
+
await this.install(dep);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 安装插件
|
|
83
|
+
const context: PluginContext = {
|
|
84
|
+
...this.context,
|
|
85
|
+
config: { ...this.context.config, ...registration.config },
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
await registration.plugin.install(context);
|
|
89
|
+
registration.installed = true;
|
|
90
|
+
|
|
91
|
+
// 发布插件安装事件
|
|
92
|
+
await this.context.eventBus.emit('plugin:installed', {
|
|
93
|
+
name: pluginName,
|
|
94
|
+
version: registration.plugin.version,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 卸载插件
|
|
100
|
+
*/
|
|
101
|
+
async uninstall(pluginName: string): Promise<void> {
|
|
102
|
+
const registration = this.plugins.get(pluginName);
|
|
103
|
+
if (!registration || !registration.installed) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 检查是否有其他插件依赖此插件
|
|
108
|
+
for (const [name, reg] of this.plugins) {
|
|
109
|
+
if (reg.installed && reg.plugin.dependencies?.includes(pluginName)) {
|
|
110
|
+
throw new Error(`Cannot uninstall "${pluginName}" because "${name}" depends on it`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (registration.plugin.uninstall) {
|
|
115
|
+
await registration.plugin.uninstall(this.context);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
registration.installed = false;
|
|
119
|
+
|
|
120
|
+
await this.context.eventBus.emit('plugin:uninstalled', { name: pluginName });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 批量安装插件
|
|
125
|
+
*/
|
|
126
|
+
async installAll(): Promise<void> {
|
|
127
|
+
for (const [name] of this.plugins) {
|
|
128
|
+
await this.install(name);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 获取插件
|
|
134
|
+
*/
|
|
135
|
+
getPlugin(name: string): Plugin | undefined {
|
|
136
|
+
return this.plugins.get(name)?.plugin;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 检查插件是否已安装
|
|
141
|
+
*/
|
|
142
|
+
isInstalled(name: string): boolean {
|
|
143
|
+
return this.plugins.get(name)?.installed ?? false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 获取所有已安装的插件
|
|
148
|
+
*/
|
|
149
|
+
getInstalledPlugins(): Plugin[] {
|
|
150
|
+
return Array.from(this.plugins.values())
|
|
151
|
+
.filter(r => r.installed)
|
|
152
|
+
.map(r => r.plugin);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 获取所有已注册的插件
|
|
157
|
+
*/
|
|
158
|
+
getRegisteredPlugins(): Plugin[] {
|
|
159
|
+
return Array.from(this.plugins.values()).map(r => r.plugin);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -44,6 +44,8 @@ export interface PrinterConnection {
|
|
|
44
44
|
connectedAt: number;
|
|
45
45
|
/** Last activity timestamp */
|
|
46
46
|
lastActivity?: number;
|
|
47
|
+
/** Error handler reference (for cleanup) */
|
|
48
|
+
errorHandler?: (error: Error) => void;
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
/**
|
|
@@ -200,14 +202,15 @@ export class MultiPrinterManager {
|
|
|
200
202
|
try {
|
|
201
203
|
const printer = new BluetoothPrinter();
|
|
202
204
|
|
|
203
|
-
//
|
|
204
|
-
printer.on('error', error => {
|
|
205
|
-
this.emit('printer-error', { printerId, error });
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
// Connect
|
|
205
|
+
// Connect first so we know it works before storing
|
|
209
206
|
await printer.connect(actualDeviceId);
|
|
210
207
|
|
|
208
|
+
// Set up error handler and store reference for cleanup
|
|
209
|
+
const errorHandler = (error: Error) => {
|
|
210
|
+
this.emit('printer-error', { printerId, error });
|
|
211
|
+
};
|
|
212
|
+
printer.on('error', errorHandler);
|
|
213
|
+
|
|
211
214
|
const connection: PrinterConnection = {
|
|
212
215
|
printerId,
|
|
213
216
|
deviceId: actualDeviceId,
|
|
@@ -215,6 +218,7 @@ export class MultiPrinterManager {
|
|
|
215
218
|
printer,
|
|
216
219
|
connectedAt: Date.now(),
|
|
217
220
|
lastActivity: Date.now(),
|
|
221
|
+
errorHandler,
|
|
218
222
|
};
|
|
219
223
|
|
|
220
224
|
this.printers.set(printerId, connection);
|
|
@@ -242,6 +246,11 @@ export class MultiPrinterManager {
|
|
|
242
246
|
|
|
243
247
|
this.logger.info(`Disconnecting printer "${printerId}"`);
|
|
244
248
|
|
|
249
|
+
// Remove error handler to prevent memory leak
|
|
250
|
+
if (connection.errorHandler) {
|
|
251
|
+
connection.printer.off('error', connection.errorHandler);
|
|
252
|
+
}
|
|
253
|
+
|
|
245
254
|
try {
|
|
246
255
|
await connection.printer.disconnect();
|
|
247
256
|
} catch (error) {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command Build Error
|
|
3
|
+
*
|
|
4
|
+
* Specialized error for command building failures
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { BluetoothPrintError, ErrorCode } from './BluetoothError';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Command building error codes
|
|
11
|
+
*/
|
|
12
|
+
export enum CommandBuildErrorCode {
|
|
13
|
+
/** Invalid configuration */
|
|
14
|
+
INVALID_CONFIG = 'INVALID_CONFIGURATION',
|
|
15
|
+
/** Encoding not supported */
|
|
16
|
+
ENCODING_NOT_SUPPORTED = 'ENCODING_NOT_SUPPORTED',
|
|
17
|
+
/** Invalid image data */
|
|
18
|
+
INVALID_IMAGE = 'INVALID_IMAGE_DATA',
|
|
19
|
+
/** Invalid QR code data */
|
|
20
|
+
INVALID_QR = 'INVALID_QR_DATA',
|
|
21
|
+
/** Driver error */
|
|
22
|
+
DRIVER_ERROR = 'DRIVER_ERROR',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* CommandBuildError - Specialized error for command building failures
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* throw new CommandBuildError(
|
|
31
|
+
* CommandBuildErrorCode.ENCODING_NOT_SUPPORTED,
|
|
32
|
+
* 'Encoding "EUC-JP" is not supported',
|
|
33
|
+
* originalError
|
|
34
|
+
* );
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export class CommandBuildError extends BluetoothPrintError {
|
|
38
|
+
constructor(
|
|
39
|
+
public readonly buildErrorCode: CommandBuildErrorCode,
|
|
40
|
+
message: string,
|
|
41
|
+
originalError?: Error
|
|
42
|
+
) {
|
|
43
|
+
const baseCode = CommandBuildError._toBaseCode(buildErrorCode);
|
|
44
|
+
super(baseCode, message, originalError);
|
|
45
|
+
this.name = 'CommandBuildError';
|
|
46
|
+
|
|
47
|
+
if (Error.captureStackTrace) {
|
|
48
|
+
Error.captureStackTrace(this, CommandBuildError);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Converts CommandBuildErrorCode to base ErrorCode
|
|
54
|
+
*/
|
|
55
|
+
private static _toBaseCode(code: CommandBuildErrorCode): ErrorCode {
|
|
56
|
+
const mapping: Partial<Record<CommandBuildErrorCode, ErrorCode>> = {
|
|
57
|
+
[CommandBuildErrorCode.INVALID_CONFIG]: ErrorCode.INVALID_CONFIGURATION,
|
|
58
|
+
[CommandBuildErrorCode.ENCODING_NOT_SUPPORTED]: ErrorCode.ENCODING_NOT_SUPPORTED,
|
|
59
|
+
[CommandBuildErrorCode.INVALID_IMAGE]: ErrorCode.INVALID_IMAGE_DATA,
|
|
60
|
+
[CommandBuildErrorCode.INVALID_QR]: ErrorCode.INVALID_QR_DATA,
|
|
61
|
+
[CommandBuildErrorCode.DRIVER_ERROR]: ErrorCode.PRINT_JOB_FAILED,
|
|
62
|
+
};
|
|
63
|
+
return mapping[code] ?? ErrorCode.INVALID_CONFIGURATION;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Checks if an error is a command build error
|
|
68
|
+
*/
|
|
69
|
+
static isCommandBuildError(error: unknown): error is CommandBuildError {
|
|
70
|
+
return error instanceof CommandBuildError;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection Error
|
|
3
|
+
*
|
|
4
|
+
* Specialized error for connection-related failures
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { BluetoothPrintError, ErrorCode } from './BluetoothError';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Connection-related error codes
|
|
11
|
+
*/
|
|
12
|
+
export enum ConnectionErrorCode {
|
|
13
|
+
/** Failed to establish connection */
|
|
14
|
+
FAILED = 'CONNECTION_FAILED',
|
|
15
|
+
/** Connection attempt timed out */
|
|
16
|
+
TIMEOUT = 'CONNECTION_TIMEOUT',
|
|
17
|
+
/** Device not found during discovery */
|
|
18
|
+
NOT_FOUND = 'DEVICE_NOT_FOUND',
|
|
19
|
+
/** Device disconnected unexpectedly */
|
|
20
|
+
DISCONNECTED = 'DEVICE_DISCONNECTED',
|
|
21
|
+
/** Bluetooth service not found on device */
|
|
22
|
+
SERVICE_NOT_FOUND = 'SERVICE_NOT_FOUND',
|
|
23
|
+
/** Bluetooth characteristic not found */
|
|
24
|
+
CHARACTERISTIC_NOT_FOUND = 'CHARACTERISTIC_NOT_FOUND',
|
|
25
|
+
/** Service discovery failed */
|
|
26
|
+
DISCOVERY_FAILED = 'SERVICE_DISCOVERY_FAILED',
|
|
27
|
+
/** Platform doesn't support Bluetooth */
|
|
28
|
+
PLATFORM_UNSUPPORTED = 'PLATFORM_NOT_SUPPORTED',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* ConnectionError - Specialized error for connection-related failures
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* throw new ConnectionError(
|
|
37
|
+
* ConnectionErrorCode.TIMEOUT,
|
|
38
|
+
* 'Connection timed out after 30s',
|
|
39
|
+
* originalError
|
|
40
|
+
* );
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export class ConnectionError extends BluetoothPrintError {
|
|
44
|
+
constructor(code: ConnectionErrorCode, message: string, originalError?: Error) {
|
|
45
|
+
// Map our codes to the base ErrorCode enum
|
|
46
|
+
const baseCode = ConnectionError._toBaseCode(code);
|
|
47
|
+
super(baseCode, message, originalError);
|
|
48
|
+
this.name = 'ConnectionError';
|
|
49
|
+
|
|
50
|
+
if (Error.captureStackTrace) {
|
|
51
|
+
Error.captureStackTrace(this, ConnectionError);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Converts ConnectionErrorCode to base ErrorCode
|
|
57
|
+
*/
|
|
58
|
+
private static _toBaseCode(code: ConnectionErrorCode): ErrorCode {
|
|
59
|
+
const mapping: Partial<Record<ConnectionErrorCode, ErrorCode>> = {
|
|
60
|
+
[ConnectionErrorCode.FAILED]: ErrorCode.CONNECTION_FAILED,
|
|
61
|
+
[ConnectionErrorCode.TIMEOUT]: ErrorCode.CONNECTION_TIMEOUT,
|
|
62
|
+
[ConnectionErrorCode.NOT_FOUND]: ErrorCode.DEVICE_NOT_FOUND,
|
|
63
|
+
[ConnectionErrorCode.DISCONNECTED]: ErrorCode.DEVICE_DISCONNECTED,
|
|
64
|
+
[ConnectionErrorCode.SERVICE_NOT_FOUND]: ErrorCode.SERVICE_NOT_FOUND,
|
|
65
|
+
[ConnectionErrorCode.CHARACTERISTIC_NOT_FOUND]: ErrorCode.CHARACTERISTIC_NOT_FOUND,
|
|
66
|
+
[ConnectionErrorCode.DISCOVERY_FAILED]: ErrorCode.SERVICE_DISCOVERY_FAILED,
|
|
67
|
+
[ConnectionErrorCode.PLATFORM_UNSUPPORTED]: ErrorCode.PLATFORM_NOT_SUPPORTED,
|
|
68
|
+
};
|
|
69
|
+
return mapping[code] ?? ErrorCode.CONNECTION_FAILED;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Checks if an error is a connection-related error
|
|
74
|
+
*/
|
|
75
|
+
static isConnectionError(error: unknown): error is ConnectionError {
|
|
76
|
+
return error instanceof ConnectionError;
|
|
77
|
+
}
|
|
78
|
+
}
|