taro-bluetooth-print 2.15.2 → 2.15.4

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 (43) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/README.md +4 -4
  3. package/dist/adapters/index.cjs.js +1 -0
  4. package/dist/adapters/index.es.js +1 -0
  5. package/dist/chunks/AdapterFactory-CkjGKGZN.js +1 -0
  6. package/dist/chunks/AdapterFactory-DA4PnIrY.js +1 -0
  7. package/dist/chunks/EscPosDriver-BEwkA9KT.js +1 -0
  8. package/dist/chunks/EscPosDriver-ju4j3bXJ.js +1 -0
  9. package/dist/chunks/gbk-data-BhfiCGRq.js +1 -0
  10. package/dist/chunks/gbk-data-Cunq2WDW.js +1 -0
  11. package/dist/chunks/index-Cee15KIh.js +1 -0
  12. package/dist/chunks/index-DIquif_6.js +1 -0
  13. package/dist/chunks/shared-C7eA759x.js +1 -0
  14. package/dist/chunks/shared-CvHoYVGG.js +1 -0
  15. package/dist/core/index.cjs.js +1 -0
  16. package/dist/core/index.es.js +1 -0
  17. package/dist/drivers/index.cjs.js +1 -0
  18. package/dist/drivers/index.es.js +1 -0
  19. package/dist/encoding/index.cjs.js +1 -0
  20. package/dist/encoding/index.es.js +1 -0
  21. package/dist/index.cjs.js +1 -1
  22. package/dist/index.es.js +1 -1
  23. package/dist/types/config/PrinterConfigManager.d.ts +58 -7
  24. package/dist/types/core/BluetoothPrinter.d.ts +54 -0
  25. package/dist/types/encoding/GbkTable.d.ts +14 -5
  26. package/dist/types/index.d.ts +3 -3
  27. package/dist/types/plugins/builtin/RetryPlugin.d.ts +19 -1
  28. package/dist/types/plugins/index.d.ts +1 -0
  29. package/dist/types/services/BatchPrintManager.d.ts +48 -2
  30. package/package.json +7 -3
  31. package/src/config/PrinterConfigManager.ts +150 -45
  32. package/src/core/BluetoothPrinter.ts +115 -5
  33. package/src/encoding/GbkTable.ts +18 -7
  34. package/src/index.ts +15 -3
  35. package/src/plugins/builtin/RetryPlugin.ts +46 -12
  36. package/src/plugins/index.ts +1 -0
  37. package/src/services/BatchPrintManager.ts +126 -8
  38. package/dist/hero-illustration.svg +0 -44
  39. package/dist/index.umd.js +0 -1
  40. package/dist/logo.svg +0 -17
  41. package/dist/manifest.webmanifest +0 -17
  42. package/dist/offline.html +0 -146
  43. package/dist/service-worker.js +0 -161
@@ -84,6 +84,40 @@ export interface GlobalConfig {
84
84
  /** Enable logging */
85
85
  enableLogging: boolean;
86
86
  }
87
+ /**
88
+ * Schema version of the export format. Bump when {@link PrinterConfigSnapshot}
89
+ * adds/removes fields so old exports can be migrated.
90
+ */
91
+ export declare const PRINTER_CONFIG_EXPORT_VERSION = 1;
92
+ /**
93
+ * Versioned wrapper around the printer configuration export.
94
+ * Persist this verbatim (JSON.stringify / parse) for cross-device sync.
95
+ */
96
+ export interface PrinterConfigSnapshot {
97
+ /** Schema version (always present). */
98
+ format: typeof PRINTER_CONFIG_EXPORT_VERSION;
99
+ /** Timestamp when the snapshot was exported (epoch ms). */
100
+ exportedAt: number;
101
+ /** Optional human-readable source identifier (e.g. "pos-terminal-01"). */
102
+ source?: string;
103
+ /** Saved printer configurations. */
104
+ printers: SavedPrinter[];
105
+ /** Global configuration overrides. */
106
+ globalConfig: GlobalConfig;
107
+ /** Last-used printer id (may be null if unset). */
108
+ lastUsedPrinterId: string | null;
109
+ }
110
+ /**
111
+ * Result of an import operation — distinguishes "nothing matched" from "rejected".
112
+ */
113
+ export interface PrinterConfigImportResult {
114
+ /** Number of printers successfully loaded into the manager. */
115
+ imported: number;
116
+ /** Number of printer entries in the snapshot that were skipped (validation failure). */
117
+ skipped: number;
118
+ /** Detected snapshot format version. */
119
+ format: number;
120
+ }
87
121
  /**
88
122
  * Configuration storage interface
89
123
  */
@@ -179,17 +213,34 @@ export declare class PrinterConfigManager {
179
213
  */
180
214
  loadPrinterConfig(printerId: string): PrintConfig;
181
215
  /**
182
- * Export all configuration as JSON
216
+ * Export all configuration as a versioned JSON snapshot.
217
+ *
218
+ * The returned string can be persisted to disk, sent over the network, or
219
+ * shared across devices. Always re-import via {@link import} — never JSON.parse
220
+ * it yourself, as the snapshot wraps a versioned {@link PrinterConfigSnapshot}.
221
+ *
222
+ * @param source - Optional human-readable identifier (e.g. "pos-terminal-01")
223
+ * recorded in the snapshot for diagnostics.
224
+ * @returns Pretty-printed JSON string
183
225
  */
184
- export(): string;
226
+ export(source?: string): string;
185
227
  /**
186
- * Import configuration from JSON
228
+ * Import configuration from a versioned JSON snapshot produced by {@link export}.
229
+ *
230
+ * Behavior:
231
+ * - Throws `BluetoothPrintError(INVALID_CONFIGURATION)` on parse / schema failure.
232
+ * - When `merge === false` (default), existing printers are cleared first.
233
+ * - Printer entries that fail per-entry validation are skipped (counted in `skipped`).
234
+ * - Unknown / higher `format` versions throw rather than silently partial-load.
187
235
  *
188
- * @param json - JSON string to import
189
- * @param merge - If true, merge with existing config; if false, replace
190
- * @returns Number of printers imported
236
+ * @param json - JSON string previously produced by `export()`.
237
+ * @param merge - When true, merge into existing config; when false (default), replace.
238
+ * @returns Import summary (imported count + skipped count + detected version).
239
+ * @throws BluetoothPrintError on malformed JSON, missing fields, or unsupported version.
191
240
  */
192
- import(json: string, merge?: boolean): number;
241
+ import(json: string, merge?: boolean): PrinterConfigImportResult;
242
+ /** Lightweight per-entry validator for an imported printer. */
243
+ private isValidSavedPrinter;
193
244
  /**
194
245
  * Clear all configuration
195
246
  */
@@ -4,6 +4,17 @@ import { BluetoothPrintError } from '../errors/BaseError';
4
4
  import { IConnectionManager, IPrintJobManager, ICommandBuilder } from '../services/interfaces';
5
5
  import { TextAlign } from '../formatter';
6
6
  import { BarcodeFormat } from '../barcode';
7
+ /** Payload for `job-completed` / `job-failed` events. */
8
+ export interface JobResult {
9
+ /** Source of the completed job: `print` (CommandBuilder pipeline) or `writeRaw` (raw bytes). */
10
+ source: 'print' | 'writeRaw';
11
+ /** Total bytes transmitted to the printer. */
12
+ bytes: number;
13
+ /** Job end timestamp (epoch ms). */
14
+ completedAt: number;
15
+ /** Wall-clock duration of the job in ms (0 for synchronous completion). */
16
+ durationMs: number;
17
+ }
7
18
  /** Printer event map. */
8
19
  export interface PrinterEvents {
9
20
  'state-change': PrinterState;
@@ -15,6 +26,12 @@ export interface PrinterEvents {
15
26
  connected: string;
16
27
  disconnected: string;
17
28
  'print-complete': void;
29
+ /** Emitted when a print/writeRaw job successfully completes all bytes. */
30
+ 'job-completed': JobResult;
31
+ /** Emitted when a print/writeRaw job fails (immediately before re-throwing). */
32
+ 'job-failed': JobResult & {
33
+ error: BluetoothPrintError;
34
+ };
18
35
  }
19
36
  /** Bluetooth Thermal Printer Controller. */
20
37
  export declare class BluetoothPrinter extends EventEmitter<PrinterEvents> {
@@ -41,6 +58,12 @@ export declare class BluetoothPrinter extends EventEmitter<PrinterEvents> {
41
58
  * Resolve the first constructor argument to an IConnectionManager. An adapter
42
59
  * (anything implementing `connect`/`disconnect`/`write`) gets auto-wrapped for
43
60
  * backward compatibility; missing/undefined falls back to a default manager.
61
+ *
62
+ * Disambiguation: `IConnectionManager` exposes `getState()` (manager-only
63
+ * method on the interface). Adapters only have `connect`/`disconnect`/`write`.
64
+ * Using `getState` as the discriminator correctly identifies a pre-built
65
+ * manager vs. a raw adapter — using `connect` (which both have) silently
66
+ * re-wrapped managers and broke isConnected() (v2.15.3 fix).
44
67
  */
45
68
  private resolveConnectionManager;
46
69
  /**
@@ -76,6 +99,37 @@ export declare class BluetoothPrinter extends EventEmitter<PrinterEvents> {
76
99
  showText?: boolean;
77
100
  }): this;
78
101
  setOptions(options: IAdapterOptions): this;
102
+ /**
103
+ * Write a pre-built byte buffer directly to the connected printer,
104
+ * bypassing the CommandBuilder entirely. Use this when a driver (such as
105
+ * TsplDriver, ZplDriver, StarPrinter, CPCL or any custom protocol) emits
106
+ * its own byte stream and you want the same connection / chunking /
107
+ * progress / pause pipeline as the standard print() path.
108
+ *
109
+ * Unlike print() — which calls commandBuilder.getBuffer() then clears it —
110
+ * writeRaw() does NOT touch the command queue. It is safe to call between
111
+ * text() / qr() / cut() calls; those calls continue to accumulate in the
112
+ * command buffer for the next print() invocation.
113
+ *
114
+ * @param buffer Bytes to send (e.g. `tsplDriver.getBuffer()`)
115
+ * @param options Optional adapter overrides (chunkSize, delay, retries).
116
+ * Forwarded to PrintJobManager.setOptions() before start.
117
+ * @throws BluetoothPrintError(CONNECTION_FAILED) if not connected
118
+ * @throws BluetoothPrintError(PRINT_JOB_FAILED) on adapter write failure
119
+ *
120
+ * @example
121
+ * ```ts
122
+ * // TSPL label printing — full end-to-end flow now works.
123
+ * const tspl = new TsplDriver()
124
+ * .size(60, 40)
125
+ * .gap(3)
126
+ * .clear()
127
+ * .text('Hello', { x: 20, y: 20, font: 3 })
128
+ * .print(1, 1);
129
+ * await printer.writeRaw(tspl.getBuffer());
130
+ * ```
131
+ */
132
+ writeRaw(buffer: Uint8Array, options?: IAdapterOptions): Promise<void>;
79
133
  /**
80
134
  * Pauses the current print job.
81
135
  */
@@ -1,10 +1,19 @@
1
1
  /**
2
2
  * GBK Encoding Table - 懒加载版本
3
3
  *
4
- * 优化策略:
5
- * 1. 默认使用精简版编码表 (gbk-lite.ts,约 3500 常用字)
6
- * 2. 遇到非常用字时动态加载完整编码表
7
- * 3. 二分查找代替 Map,大幅减少内存占用
4
+ * 优化策略(v2.15.4 重新设计):
5
+ * 1. 默认使用精简版编码表 (GbkLite.ts,约 106 个最常用汉字)
6
+ * 2. 命中精简表的字符直接返回(毫秒级)
7
+ * 3. 未命中时查完整表 (GbkData.ts)
8
+ * 4. GbkData 通过 vite manualChunks 拆成独立 chunk `chunks/gbk-data-*.js`
9
+ * —— 在 dist 输出层面做到了代码分割,但运行时仍然同步可用
10
+ *
11
+ * 注意:dynamic import() 方案曾在 v2.15.4-dev 中尝试,但因为
12
+ * EncodingService.encode() / TemplateRenderer 都是同步 API,
13
+ * 切换到 dynamic 后会导致中文编码首次失败('生产日期'/'谢谢惠顾'
14
+ * 这类字符不在 lite 表中)。v2.15.4-final 选择保留 static import
15
+ * 来换取 100% backward compat;异步 pre-warm 留给 v2.15.5+ 重构
16
+ * sync → async encoding API 时再启用。
8
17
  *
9
18
  * GBK: 23940 个字符映射
10
19
  * Big5: 13911 个字符映射
@@ -14,7 +23,7 @@ export declare const gbkToUnicode: Map<number, number>;
14
23
  export declare const unicodeToBig5: Map<number, number>;
15
24
  export declare const big5ToUnicode: Map<number, number>;
16
25
  /**
17
- * Get GBK bytes for a Unicode character
26
+ * Get GBK bytes for a Unicode character.
18
27
  * 先查精简表,查不到再懒加载完整表
19
28
  */
20
29
  export declare function getGbkBytes(unicode: number): [number, number] | null;
@@ -5,7 +5,7 @@
5
5
  * @packageDocumentation
6
6
  */
7
7
  export { BluetoothPrinter } from './core/BluetoothPrinter';
8
- export type { PrinterEvents } from './core/BluetoothPrinter';
8
+ export type { PrinterEvents, JobResult } from './core/BluetoothPrinter';
9
9
  export { EventEmitter } from './core/EventEmitter';
10
10
  export * from './drivers';
11
11
  export { TaroAdapter } from './adapters/TaroAdapter';
@@ -49,8 +49,8 @@ export { CommandBuildError, CommandBuildErrorCode } from './errors/CommandBuildE
49
49
  export { createBluetoothPrinter, createWebBluetoothPrinter, PrinterFactory, type PrinterFactoryOptions, } from './factory';
50
50
  export { DEFAULT_CONFIG, mergeConfig } from './config/PrinterConfig';
51
51
  export type { PrinterConfig, AdapterConfig, DriverConfig, LoggingConfig, } from './config/PrinterConfig';
52
- export { PrinterConfigManager, printerConfigManager } from './config/PrinterConfigManager';
53
- export type { SavedPrinter, GlobalConfig, IConfigStorage } from './config/PrinterConfigManager';
52
+ export { PrinterConfigManager, printerConfigManager, PRINTER_CONFIG_EXPORT_VERSION, LocalStorage, } from './config/PrinterConfigManager';
53
+ export type { SavedPrinter, GlobalConfig, IConfigStorage, PrintConfig, PrinterConfigSnapshot, PrinterConfigImportResult, } from './config/PrinterConfigManager';
54
54
  export { createLoggingPlugin, createRetryPlugin } from './plugins';
55
55
  export type { Plugin, PluginHooks, PluginOptions, PluginFactory } from './plugins/PluginTypes';
56
56
  export * from './types';
@@ -1,5 +1,16 @@
1
1
  import { PluginFactory, PluginOptions } from '../PluginTypes';
2
- import { ErrorCode } from '../../errors/BaseError';
2
+ import { BluetoothPrintError, ErrorCode } from '../../errors/BaseError';
3
+ /** Payload passed to the onRetry callback for each retry attempt. */
4
+ export interface RetryAttempt {
5
+ /** 1-indexed attempt number (1 = first retry after the initial failure). */
6
+ attempt: number;
7
+ /** Maximum retries allowed. */
8
+ maxRetries: number;
9
+ /** Delay in ms before this retry will be scheduled to run. */
10
+ delayMs: number;
11
+ /** The error that triggered this retry. */
12
+ error: BluetoothPrintError;
13
+ }
3
14
  export interface RetryPluginOptions extends PluginOptions {
4
15
  /** Maximum retry attempts (default: 3) */
5
16
  maxRetries?: number;
@@ -11,6 +22,13 @@ export interface RetryPluginOptions extends PluginOptions {
11
22
  backoffMultiplier?: number;
12
23
  /** Error codes that should trigger retry */
13
24
  retryableErrors?: ErrorCode[];
25
+ /**
26
+ * Optional callback invoked BEFORE sleeping for each retry attempt.
27
+ * Receives the 1-indexed attempt number, the planned delay, and the
28
+ * triggering error. Use this for surfacing retry state to UIs (toast
29
+ * "Reconnecting (2/3)…") or for telemetry.
30
+ */
31
+ onRetry?: (info: RetryAttempt) => void;
14
32
  }
15
33
  /**
16
34
  * Creates a retry plugin instance
@@ -5,3 +5,4 @@ export { PluginManager } from './PluginManager';
5
5
  export type { Plugin, PluginHooks, PluginOptions, PluginFactory } from './PluginTypes';
6
6
  export { createLoggingPlugin } from './builtin/LoggingPlugin';
7
7
  export { createRetryPlugin } from './builtin/RetryPlugin';
8
+ export type { RetryPluginOptions, RetryAttempt } from './builtin/RetryPlugin';
@@ -1,3 +1,4 @@
1
+ import { BluetoothPrintError } from '../errors/BaseError';
1
2
  import { EventEmitter } from '../core/EventEmitter';
2
3
  /**
3
4
  * Batch job entry
@@ -76,6 +77,23 @@ export interface BatchEvents {
76
77
  toCount: number;
77
78
  savedBytes: number;
78
79
  };
80
+ /** Per-job progress while the batch processor is running. */
81
+ 'batch-progress': {
82
+ /** Number of bytes the processor has accepted so far (caller-reported). */
83
+ sent: number;
84
+ /** Total bytes the processor was asked to handle for this batch. */
85
+ total: number;
86
+ /** Job IDs in the current batch. */
87
+ jobIds: string[];
88
+ };
89
+ /** Emitted when the batch processor throws — the entire batch is marked failed. */
90
+ 'batch-failed': {
91
+ jobIds: string[];
92
+ bytes: number;
93
+ error: BluetoothPrintError;
94
+ };
95
+ /** Emitted when a previously-failed job is re-queued via retryJob(). */
96
+ 'job-retried': BatchJob;
79
97
  }
80
98
  /**
81
99
  * Batch Print Manager
@@ -102,6 +120,8 @@ export declare class BatchPrintManager extends EventEmitter<BatchEvents> {
102
120
  private flushFired;
103
121
  private pendingBytes;
104
122
  private stats;
123
+ /** Jobs from the most recent failed batch. Cleared on next successful processBatch(). */
124
+ private failedJobs;
105
125
  /**
106
126
  * Last job timestamp for interval timeout tracking
107
127
  */
@@ -168,10 +188,36 @@ export declare class BatchPrintManager extends EventEmitter<BatchEvents> {
168
188
  /**
169
189
  * Process the current batch
170
190
  *
171
- * @param processor - Function to send batch data to printer
191
+ * @param processor - Function to send batch data to printer. May return a
192
+ * promise that resolves with the number of bytes sent, or
193
+ * void if the byte count is unknown.
172
194
  * @returns Number of jobs processed
173
195
  */
174
- processBatch(processor: (data: Uint8Array) => Promise<void>): Promise<number>;
196
+ processBatch(processor: (data: Uint8Array) => Promise<void | number>): Promise<number>;
197
+ /**
198
+ * Re-queue a previously-failed job.
199
+ *
200
+ * Searches `failedJobs` for the given id; if found, re-inserts the job at the
201
+ * front of the queue with bumped priority (current max + 1) so it is processed
202
+ * before other pending jobs. Returns false if no failed job matches.
203
+ *
204
+ * Does NOT auto-trigger processBatch — caller decides when to retry.
205
+ *
206
+ * @param id - Job ID returned from a prior addJob() call.
207
+ * @returns true if re-queued, false if no matching failed job.
208
+ */
209
+ retryJob(id: string): boolean;
210
+ /**
211
+ * Re-queue ALL currently-failed jobs in one call. Returns the count re-queued.
212
+ */
213
+ retryAllFailedJobs(): number;
214
+ /**
215
+ * Snapshot of the most recent failed batch (empty if last batch succeeded
216
+ * or no batch has run yet).
217
+ */
218
+ getFailedJobs(): BatchJob[];
219
+ /** Discard the failed-jobs buffer without re-queueing. */
220
+ clearFailedJobs(): void;
175
221
  /**
176
222
  * Prepare batch from pending jobs
177
223
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taro-bluetooth-print",
3
- "version": "2.15.2",
3
+ "version": "2.15.4",
4
4
  "description": "Taro 蓝牙打印库 v2.6 - 轻量级、高性能、跨平台支持微信、支付宝、百度、字节跳动小程序及H5 Web Bluetooth",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs.js",
@@ -10,8 +10,7 @@
10
10
  ".": {
11
11
  "types": "./dist/types/index.d.ts",
12
12
  "import": "./dist/index.es.js",
13
- "require": "./dist/index.cjs.js",
14
- "default": "./dist/index.umd.js"
13
+ "require": "./dist/index.cjs.js"
15
14
  },
16
15
  "./core": {
17
16
  "types": "./dist/types/core/index.d.ts",
@@ -25,6 +24,10 @@
25
24
  "types": "./dist/types/adapters/index.d.ts",
26
25
  "import": "./dist/adapters/index.es.js"
27
26
  },
27
+ "./encoding": {
28
+ "types": "./dist/types/encoding/index.d.ts",
29
+ "import": "./dist/encoding/index.es.js"
30
+ },
28
31
  "./package.json": "./package.json"
29
32
  },
30
33
  "files": [
@@ -36,6 +39,7 @@
36
39
  "scripts": {
37
40
  "clean": "rimraf dist docs/.vitepress/dist",
38
41
  "build": "vite build",
42
+ "build:umd": "vite build --config vite.umd.config.ts",
39
43
  "dev": "vite build --watch",
40
44
  "docs:dev": "vitepress dev docs",
41
45
  "docs:build": "vitepress build docs",
@@ -91,6 +91,43 @@ export interface GlobalConfig {
91
91
  enableLogging: boolean;
92
92
  }
93
93
 
94
+ /**
95
+ * Schema version of the export format. Bump when {@link PrinterConfigSnapshot}
96
+ * adds/removes fields so old exports can be migrated.
97
+ */
98
+ export const PRINTER_CONFIG_EXPORT_VERSION = 1;
99
+
100
+ /**
101
+ * Versioned wrapper around the printer configuration export.
102
+ * Persist this verbatim (JSON.stringify / parse) for cross-device sync.
103
+ */
104
+ export interface PrinterConfigSnapshot {
105
+ /** Schema version (always present). */
106
+ format: typeof PRINTER_CONFIG_EXPORT_VERSION;
107
+ /** Timestamp when the snapshot was exported (epoch ms). */
108
+ exportedAt: number;
109
+ /** Optional human-readable source identifier (e.g. "pos-terminal-01"). */
110
+ source?: string;
111
+ /** Saved printer configurations. */
112
+ printers: SavedPrinter[];
113
+ /** Global configuration overrides. */
114
+ globalConfig: GlobalConfig;
115
+ /** Last-used printer id (may be null if unset). */
116
+ lastUsedPrinterId: string | null;
117
+ }
118
+
119
+ /**
120
+ * Result of an import operation — distinguishes "nothing matched" from "rejected".
121
+ */
122
+ export interface PrinterConfigImportResult {
123
+ /** Number of printers successfully loaded into the manager. */
124
+ imported: number;
125
+ /** Number of printer entries in the snapshot that were skipped (validation failure). */
126
+ skipped: number;
127
+ /** Detected snapshot format version. */
128
+ format: number;
129
+ }
130
+
94
131
  /**
95
132
  * Configuration storage interface
96
133
  */
@@ -424,67 +461,135 @@ export class PrinterConfigManager {
424
461
  }
425
462
 
426
463
  /**
427
- * Export all configuration as JSON
464
+ * Export all configuration as a versioned JSON snapshot.
465
+ *
466
+ * The returned string can be persisted to disk, sent over the network, or
467
+ * shared across devices. Always re-import via {@link import} — never JSON.parse
468
+ * it yourself, as the snapshot wraps a versioned {@link PrinterConfigSnapshot}.
469
+ *
470
+ * @param source - Optional human-readable identifier (e.g. "pos-terminal-01")
471
+ * recorded in the snapshot for diagnostics.
472
+ * @returns Pretty-printed JSON string
428
473
  */
429
- export(): string {
430
- return JSON.stringify(
431
- {
432
- printers: Array.from(this.printers.values()),
433
- globalConfig: this.globalConfig,
434
- lastUsedPrinterId: this.lastUsedPrinterId,
435
- exportedAt: Date.now(),
436
- },
437
- null,
438
- 2
439
- );
474
+ export(source?: string): string {
475
+ const snapshot: PrinterConfigSnapshot = {
476
+ format: PRINTER_CONFIG_EXPORT_VERSION,
477
+ exportedAt: Date.now(),
478
+ source,
479
+ printers: Array.from(this.printers.values()),
480
+ globalConfig: this.globalConfig,
481
+ lastUsedPrinterId: this.lastUsedPrinterId,
482
+ };
483
+ return JSON.stringify(snapshot, null, 2);
440
484
  }
441
485
 
442
486
  /**
443
- * Import configuration from JSON
487
+ * Import configuration from a versioned JSON snapshot produced by {@link export}.
444
488
  *
445
- * @param json - JSON string to import
446
- * @param merge - If true, merge with existing config; if false, replace
447
- * @returns Number of printers imported
489
+ * Behavior:
490
+ * - Throws `BluetoothPrintError(INVALID_CONFIGURATION)` on parse / schema failure.
491
+ * - When `merge === false` (default), existing printers are cleared first.
492
+ * - Printer entries that fail per-entry validation are skipped (counted in `skipped`).
493
+ * - Unknown / higher `format` versions throw rather than silently partial-load.
494
+ *
495
+ * @param json - JSON string previously produced by `export()`.
496
+ * @param merge - When true, merge into existing config; when false (default), replace.
497
+ * @returns Import summary (imported count + skipped count + detected version).
498
+ * @throws BluetoothPrintError on malformed JSON, missing fields, or unsupported version.
448
499
  */
449
- import(json: string, merge = false): number {
500
+ import(json: string, merge = false): PrinterConfigImportResult {
501
+ if (typeof json !== 'string' || json.length === 0) {
502
+ throw new BluetoothPrintError(
503
+ ErrorCode.INVALID_CONFIGURATION,
504
+ 'import() requires a non-empty JSON string'
505
+ );
506
+ }
507
+
508
+ let parsed: unknown;
450
509
  try {
451
- const data = JSON.parse(json) as {
452
- printers?: SavedPrinter[];
453
- globalConfig?: GlobalConfig;
454
- lastUsedPrinterId?: string;
455
- };
510
+ parsed = JSON.parse(json);
511
+ } catch (err) {
512
+ throw new BluetoothPrintError(
513
+ ErrorCode.INVALID_CONFIGURATION,
514
+ `Failed to parse JSON: ${(err as Error).message}`
515
+ );
516
+ }
517
+
518
+ if (!parsed || typeof parsed !== 'object') {
519
+ throw new BluetoothPrintError(
520
+ ErrorCode.INVALID_CONFIGURATION,
521
+ 'Snapshot root must be a JSON object'
522
+ );
523
+ }
456
524
 
457
- let imported = 0;
525
+ const data = parsed as Partial<PrinterConfigSnapshot>;
458
526
 
459
- if (!merge) {
460
- this.printers.clear();
461
- }
527
+ if (typeof data.format !== 'number') {
528
+ throw new BluetoothPrintError(
529
+ ErrorCode.INVALID_CONFIGURATION,
530
+ 'Snapshot is missing required field: format'
531
+ );
532
+ }
533
+ if (data.format > PRINTER_CONFIG_EXPORT_VERSION) {
534
+ throw new BluetoothPrintError(
535
+ ErrorCode.INVALID_CONFIGURATION,
536
+ `Snapshot format ${data.format} is newer than supported version ${PRINTER_CONFIG_EXPORT_VERSION}. Please upgrade the library.`
537
+ );
538
+ }
539
+
540
+ if (!merge) {
541
+ this.printers.clear();
542
+ }
462
543
 
463
- if (data.printers && Array.isArray(data.printers)) {
464
- for (const printer of data.printers) {
465
- if (printer.id) {
466
- this.printers.set(printer.id, printer);
467
- imported++;
468
- }
544
+ let imported = 0;
545
+ let skipped = 0;
546
+ if (Array.isArray(data.printers)) {
547
+ for (const candidate of data.printers) {
548
+ if (this.isValidSavedPrinter(candidate)) {
549
+ this.printers.set(candidate.id, candidate);
550
+ imported++;
551
+ } else {
552
+ skipped++;
553
+ this.logger.warn(
554
+ `Skipped invalid printer entry during import: ${JSON.stringify(candidate).slice(0, 80)}`
555
+ );
469
556
  }
470
557
  }
558
+ }
471
559
 
472
- if (data.globalConfig) {
473
- this.globalConfig = { ...DEFAULT_GLOBAL_CONFIG, ...data.globalConfig };
474
- }
560
+ if (data.globalConfig && typeof data.globalConfig === 'object') {
561
+ this.globalConfig = { ...DEFAULT_GLOBAL_CONFIG, ...data.globalConfig };
562
+ }
475
563
 
476
- if (data.lastUsedPrinterId && this.printers.has(data.lastUsedPrinterId)) {
477
- this.lastUsedPrinterId = data.lastUsedPrinterId;
478
- }
564
+ if (
565
+ typeof data.lastUsedPrinterId === 'string' &&
566
+ data.lastUsedPrinterId.length > 0 &&
567
+ this.printers.has(data.lastUsedPrinterId)
568
+ ) {
569
+ this.lastUsedPrinterId = data.lastUsedPrinterId;
570
+ }
479
571
 
480
- this.save();
481
- this.logger.info(`Imported ${imported} printers`);
572
+ this.save();
573
+ this.logger.info(
574
+ `Imported ${imported} printer(s) (skipped ${skipped}) from format v${data.format}`
575
+ );
482
576
 
483
- return imported;
484
- } catch (error) {
485
- this.logger.error('Failed to import configuration:', error);
486
- return 0;
487
- }
577
+ return { imported, skipped, format: data.format };
578
+ }
579
+
580
+ /** Lightweight per-entry validator for an imported printer. */
581
+ private isValidSavedPrinter(p: unknown): p is SavedPrinter {
582
+ if (!p || typeof p !== 'object') return false;
583
+ const r = p as Record<string, unknown>;
584
+ return (
585
+ typeof r.id === 'string' &&
586
+ r.id.length > 0 &&
587
+ typeof r.deviceId === 'string' &&
588
+ r.deviceId.length > 0 &&
589
+ typeof r.name === 'string' &&
590
+ typeof r.createdAt === 'number' &&
591
+ typeof r.updatedAt === 'number'
592
+ );
488
593
  }
489
594
 
490
595
  /**