@gohcltech/edge-print-client 2.0.54-develop → 2.0.61-develop

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/README.md CHANGED
@@ -36,11 +36,12 @@ const printers = await ep.printers()
36
36
  console.log(printers.map(p => p.name))
37
37
 
38
38
  // 3. Print a PDF (base64-encoded)
39
- const jobId = await ep.print(
39
+ const { jobId, warnings } = await ep.print(
40
40
  { printer: 'Office Laser', copies: 1 },
41
41
  [{ type: 'pixel', format: 'pdf', flavor: 'base64', data: pdfBase64 }],
42
42
  )
43
43
  console.log('Queued as', jobId)
44
+ for (const w of warnings) console.warn(w.message)
44
45
 
45
46
  // 4. Disconnect when done
46
47
  ep.disconnect()
@@ -152,24 +153,53 @@ const name = await ep.defaultPrinter() // "Office Laser"
152
153
 
153
154
  ### `print(config, data)`
154
155
 
155
- Submits a print job. Returns the job ID assigned by the agent.
156
+ Submits a print job. Resolves to a `PrintResult`.
156
157
 
157
158
  ```ts
158
- const jobId = await ep.print(config, data)
159
+ const { jobId, warnings } = await ep.print(config, data)
159
160
  ```
160
161
 
162
+ | Field | Type | Description |
163
+ |---|---|---|
164
+ | `jobId` | `string` | The agent's id for the job, matching its entry in the job history |
165
+ | `warnings` | `JobWarning[]` | Caveats the agent recorded about a job it printed anyway. Empty in the common case |
166
+
167
+ Each `JobWarning` is `{ code, message }`. Today the only code is
168
+ `capability_unverified` — the agent could not determine a capability, so it
169
+ skipped the check and passed the request through. Treat the set as open-ended:
170
+ log or surface `message` rather than branching on every code.
171
+
172
+ > **Breaking change.** `print()` used to resolve to the job id string. It now
173
+ > resolves to an object. Destructure it (`const { jobId } = await ep.print(…)`),
174
+ > or the value you pass on will be `[object Object]`. TypeScript callers get a
175
+ > compile error; JavaScript callers do not.
176
+
161
177
  Rejects if the job is refused, and also if it fails on the way to the spooler —
162
178
  where it previously resolved with a job id regardless. Always handle the
163
- rejection:
179
+ rejection, and branch on `err.code` rather than the message text:
164
180
 
165
181
  ```ts
182
+ import { EdgePrintError } from '@gohcltech/edge-print-client'
183
+
166
184
  try {
167
- const jobId = await ep.print(config, data)
185
+ const { jobId } = await ep.print(config, data)
168
186
  } catch (err) {
169
- // printer not found, decode failure, spooler error
187
+ if (err instanceof EdgePrintError && err.code === 'unsupported_capability') {
188
+ // err.field names the setting the printer cannot do — 'colorType', 'duplex'
189
+ }
170
190
  }
171
191
  ```
172
192
 
193
+ A job rejected by validation still gets a record in the agent's History, so an
194
+ `EdgePrintError` from `print()` usually carries a `jobId` you can quote to
195
+ support. A request refused before a job existed — one the agent could not parse
196
+ — has no `jobId`.
197
+
198
+ A rejection never carries warnings, even where the agent had already recorded
199
+ some before the rule that failed. They are dropped deliberately: a note saying
200
+ "we could not verify duplex" alongside a rejection for an unrelated reason is
201
+ noise, not help.
202
+
173
203
  Two limits worth knowing before you build on this:
174
204
 
175
205
  - **Resolved means spooled, not printed.** The agent resolves once the OS print
@@ -309,6 +339,96 @@ attempt, and they all fire on the next close.
309
339
 
310
340
  ---
311
341
 
342
+ ## Errors
343
+
344
+ Every rejection from this library is an `EdgePrintError` — including the ones
345
+ raised locally, before a request ever reaches the agent. It extends `Error`, so
346
+ `err.message` and `String(err)` read exactly as they did before codes existed.
347
+
348
+ ```ts
349
+ import ep, { EdgePrintError } from '@gohcltech/edge-print-client'
350
+
351
+ try {
352
+ await ep.print(config, data)
353
+ } catch (err) {
354
+ if (!(err instanceof EdgePrintError)) throw err
355
+
356
+ if (err.retryable) {
357
+ // transport trouble — the same request may well work on a second attempt
358
+ } else if (err.field) {
359
+ console.error(`${err.field}: ${err.message}`)
360
+ } else {
361
+ console.error(err.message)
362
+ }
363
+ }
364
+ ```
365
+
366
+ | Property | Type | Description |
367
+ |---|---|---|
368
+ | `code` | `EdgePrintErrorCode` | The failure, from a closed set. `'unknown'` when the agent sent no code, or one this client predates |
369
+ | `message` | `string` | Human-readable. Do not match on it — that is what `code` is for |
370
+ | `field` | `string?` | The offending setting, when one setting is to blame |
371
+ | `supported` | `string[]?` | What the printer accepts, for the codes that can say |
372
+ | `requestId` | `string?` | The request this answers. Absent for failures raised before one existed |
373
+ | `jobId` | `string?` | Present when the agent created a job and then failed it |
374
+ | `retryable` | `boolean` | True only for transport failures — see below |
375
+
376
+ ### Codes
377
+
378
+ Sent by the agent:
379
+
380
+ | Code | Raised when |
381
+ |---|---|
382
+ | `not_authenticated` | A request was sent before `connect()` finished authenticating |
383
+ | `invalid_token` | The token is unknown, or still awaiting approval in the agent UI |
384
+ | `invalid_request` | The agent could not parse the request |
385
+ | `unsupported_capability` | A capability the printer explicitly reports it lacks |
386
+ | `invalid_geometry` | A scale or margin outside what the protocol accepts |
387
+ | `unsupported_for_path` | A setting that cannot be honoured on the requested path |
388
+ | `printer_not_found` | `defaultPrinter()` found no default configured on the machine |
389
+ | `print_failed` | The backend failed after the job was accepted and recorded |
390
+ | `internal_error` | The agent itself failed — not the caller's doing |
391
+
392
+ Raised locally by this library, never sent by the agent:
393
+
394
+ | Code | Raised when |
395
+ |---|---|
396
+ | `not_connected` | A call was made while disconnected |
397
+ | `unreachable` | The WebSocket could not be opened — agent not running, or wrong host/port |
398
+ | `protocol_mismatch` | The agent and this library speak different wire protocol versions |
399
+ | `connection_closed` | The connection dropped with a request in flight |
400
+ | `timeout` | The agent accepted the request and did not answer within 30 seconds |
401
+
402
+ `invalid_request` is raised by **both** sides: by the agent for a request it
403
+ cannot parse, and here for a `config` or `data` value that cannot be serialised
404
+ to JSON at all — a circular reference, say. Neither is retryable.
405
+
406
+ The four codes in the table above are never accepted *from* the agent. A frame
407
+ claiming `code: "timeout"` is read as `unknown`, so a failure the agent
408
+ considers final cannot arrive labelled retryable.
409
+
410
+ Three more are **reserved**: `unsupported_value`, `content_overflow` and
411
+ `encrypted_pdf` are part of the closed set and typed here, so code written
412
+ today handles them, but no agent sends them yet. `supported` is populated only
413
+ by `unsupported_value`, so it is always `undefined` against an agent of this
414
+ generation.
415
+
416
+ And `unknown`, the fallback for a code this client does not recognise — which
417
+ is what lets a client built today keep working against a future agent. Treat an
418
+ unfamiliar code as a plain failure rather than a bug.
419
+
420
+ ### `retryable`
421
+
422
+ `err.retryable` is true for `unreachable`, `connection_closed`, and `timeout`,
423
+ and false for everything else. A validation rejection is deterministic: retrying
424
+ it sends the same bad request again.
425
+
426
+ Note that `retryable` says the *request* may succeed — not that nothing
427
+ happened. See the `timeout` entry under Troubleshooting before you retry a
428
+ print.
429
+
430
+ ---
431
+
312
432
  ## Using a custom instance
313
433
 
314
434
  The default export `ep` is a module-level singleton. If you need to connect to multiple agents simultaneously, create separate instances:
@@ -374,16 +494,18 @@ To confirm the certificate is trusted, open `https://127.0.0.1:8181` in the
374
494
  browser. If the page loads (even with an empty body) the certificate is fine.
375
495
  If you see a security warning, the certificate is not yet trusted.
376
496
 
377
- ### `connect()` throws "Cannot reach Edge Printing agent"
497
+ ### `connect()` rejects with `code: 'unreachable'`
378
498
 
379
499
  - Confirm the Edge Printing agent is running (check the system tray / menu bar).
380
500
  - Confirm `host` and `port` match the agent's configured values (default `127.0.0.1:8181`).
381
501
 
382
- ### `connect()` throws after authentication
502
+ ### `connect()` rejects with `code: 'invalid_token'`
383
503
 
384
- The token was rejected. Generate a new one from the Edge Printing settings window.
504
+ The token was rejected, or it is new and still awaiting approval in the agent's
505
+ Clients tab. Approve it there, or generate a fresh one from the Edge Printing
506
+ settings window.
385
507
 
386
- ### `connect()` throws "Protocol version mismatch"
508
+ ### `connect()` rejects with `code: 'protocol_mismatch'`
387
509
 
388
510
  The agent and this library speak different wire protocol versions. The message
389
511
  names both and says which side to update:
@@ -399,10 +521,15 @@ A mismatch is deterministic, so `connect()`'s retries cannot clear it — it sti
399
521
  exhausts them (about 3 seconds by default) before throwing. Pass `retries: 0`
400
522
  when probing for a usable agent. One side has to be upgraded.
401
523
 
402
- ### Requests time out after 30 seconds
524
+ ### A request rejects with `code: 'timeout'`
525
+
526
+ The agent accepted the connection and did not answer within 30 seconds. Restart
527
+ the agent. If the problem persists, file an issue with the agent log attached.
403
528
 
404
- The agent accepted the connection but stopped responding. Restart the agent. If
405
- the problem persists, file an issue with the agent log attached.
529
+ For a `print()`, a timeout does **not** prove nothing printed a large PDF can
530
+ take longer than 30 seconds to spool, and the rejection arrives while the agent
531
+ is still working. Never resubmit a print automatically; surface it and let a
532
+ person decide.
406
533
 
407
534
  ---
408
535
 
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * await ep.connect({ token: 'your-api-token' })
12
12
  * const printers = await ep.printers()
13
- * const jobId = await ep.print(
13
+ * const { jobId, warnings } = await ep.print(
14
14
  * { printer: 'Office Laser' },
15
15
  * [{ type: 'pixel', format: 'pdf', flavor: 'base64', data: pdfBase64 }],
16
16
  * )
@@ -228,6 +228,65 @@ export interface ConnectOptions {
228
228
  */
229
229
  retryDelay?: number;
230
230
  }
231
+ declare const ERROR_CODES: readonly ["not_authenticated", "invalid_token", "protocol_mismatch", "invalid_request", "unsupported_value", "unsupported_capability", "invalid_geometry", "content_overflow", "unsupported_for_path", "encrypted_pdf", "printer_not_found", "print_failed", "internal_error", "not_connected", "unreachable", "connection_closed", "timeout", "unknown"];
232
+ export type EdgePrintErrorCode = typeof ERROR_CODES[number];
233
+ /** A warning the agent recorded about a job it nonetheless printed. */
234
+ export interface JobWarning {
235
+ /** e.g. `"capability_unverified"`. Open-ended: an agent may add codes. */
236
+ code: string;
237
+ message: string;
238
+ }
239
+ /** What {@link EdgePrintClient.print} resolves to. */
240
+ export interface PrintResult {
241
+ /** The agent's id for the job, matching its entry in the job history. */
242
+ jobId: string;
243
+ /** Empty when the agent had nothing to report, which is the common case. */
244
+ warnings: JobWarning[];
245
+ }
246
+ /**
247
+ * A failure from the Edge Printing agent or from this client.
248
+ *
249
+ * Extends `Error`, so `e.message` and `String(e)` keep working for code
250
+ * written before codes existed.
251
+ *
252
+ * **Do not set `name` on this class.** A class name does not set `.name` — it
253
+ * is inherited as `"Error"` — so `String(e)` stays `"Error: …"`. Setting it
254
+ * would change that string for every existing consumer, which is exactly what
255
+ * extending `Error` is here to avoid.
256
+ */
257
+ export declare class EdgePrintError extends Error {
258
+ /** `'unknown'` when the agent sent no code, or one this client predates. */
259
+ readonly code: EdgePrintErrorCode;
260
+ /** The offending setting, when one setting is to blame. */
261
+ readonly field?: string;
262
+ /** What the printer accepts, for the codes that can say. */
263
+ readonly supported?: string[];
264
+ /** The request this answers. Absent for failures raised before one existed. */
265
+ readonly requestId?: string;
266
+ /** Present when the agent created a job and then failed it. */
267
+ readonly jobId?: string;
268
+ constructor(code: EdgePrintErrorCode, message: string, detail?: {
269
+ field?: string;
270
+ supported?: string[];
271
+ requestId?: string;
272
+ jobId?: string;
273
+ });
274
+ /**
275
+ * Builds an error from an agent failure frame.
276
+ *
277
+ * Everything is read defensively: an older agent sends no `code` at all, and
278
+ * a newer one may send a code this client has never heard of. Both become
279
+ * `'unknown'` rather than reaching a caller as an invalid value.
280
+ */
281
+ static fromWire(msg: Record<string, unknown>): EdgePrintError;
282
+ /**
283
+ * Whether the same request might succeed on a retry.
284
+ *
285
+ * True only for transport failures. A validation rejection is deterministic:
286
+ * retrying it sends the same bad request again.
287
+ */
288
+ get retryable(): boolean;
289
+ }
231
290
  /**
232
291
  * WebSocket client for the Edge Printing agent.
233
292
  *
@@ -284,7 +343,7 @@ export declare class EdgePrintClient {
284
343
  * connect-on-demand helper with {@link EdgePrintClient.isConnected} rather
285
344
  * than reconnecting unconditionally.
286
345
  *
287
- * @throws {Error} If the agent is unreachable or the token is rejected after
346
+ * @throws {EdgePrintError} If the agent is unreachable or the token is rejected after
288
347
  * all retries are exhausted.
289
348
  *
290
349
  * @example
@@ -302,7 +361,7 @@ export declare class EdgePrintClient {
302
361
  /**
303
362
  * Return all printers available on the agent machine.
304
363
  *
305
- * @throws {Error} If not connected.
364
+ * @throws {EdgePrintError} If not connected.
306
365
  *
307
366
  * @example
308
367
  * ```ts
@@ -317,7 +376,7 @@ export declare class EdgePrintClient {
317
376
  * Cheaper than calling {@link printers} when you only need the default name
318
377
  * and no other printer metadata.
319
378
  *
320
- * @throws {Error} If not connected.
379
+ * @throws {EdgePrintError} If not connected.
321
380
  */
322
381
  defaultPrinter(): Promise<string>;
323
382
  /**
@@ -325,10 +384,13 @@ export declare class EdgePrintClient {
325
384
  *
326
385
  * @param config - Printer selection and job settings.
327
386
  * @param data - One or more content items to print (pages, labels, …).
328
- * @returns The job ID assigned by the agent.
387
+ * @returns A {@link PrintResult} the agent's `jobId`, and any `warnings`
388
+ * it recorded about a job it printed anyway. `warnings` is empty in the
389
+ * common case.
329
390
  *
330
- * @throws {Error} If not connected, if the agent rejects the job, or if the
331
- * job fails on the way to the spooler.
391
+ * @throws {EdgePrintError} If not connected, if the agent rejects the job, or
392
+ * if the job fails on the way to the spooler. Branch on `err.code`, never
393
+ * on the message text.
332
394
  *
333
395
  * A resolved promise means the job was handed to the OS print spooler — not
334
396
  * that paper came out. A printer that is offline, jammed or out of paper
@@ -338,24 +400,31 @@ export declare class EdgePrintClient {
338
400
  * the client's 30 s timeout rejects while the agent may still be spooling it.
339
401
  * Do not resubmit a print automatically on rejection.
340
402
  *
341
- * When the agent had already created a job before it failed, the thrown error
342
- * carries a `jobId` property matching the entry in the agent's job history —
343
- * useful when surfacing a failure someone has to chase:
403
+ * A job rejected by validation is still recorded, so the thrown error
404
+ * usually carries a `jobId` matching the entry in the agent's job history —
405
+ * useful when surfacing a failure someone has to chase. A request refused
406
+ * before a job existed at all, such as one the agent could not parse, has no
407
+ * `jobId` to carry.
408
+ *
409
+ * A rejection never carries warnings, even where the agent had accumulated
410
+ * some before the rule that failed: they are dropped deliberately rather
411
+ * than reported alongside an unrelated failure.
344
412
  *
345
413
  * ```ts
346
414
  * try {
347
415
  * await ep.print(config, data)
348
416
  * } catch (err) {
349
- * const jobId = (err as Error & { jobId?: string }).jobId
417
+ * if (err instanceof EdgePrintError) console.error(err.code, err.jobId)
350
418
  * }
351
419
  * ```
352
420
  *
353
421
  * @example Print a PDF
354
422
  * ```ts
355
- * const jobId = await ep.print(
423
+ * const { jobId, warnings } = await ep.print(
356
424
  * { printer: 'Office Laser', copies: 2, duplex: 'long-edge' },
357
425
  * [{ type: 'pixel', format: 'pdf', flavor: 'base64', data: pdfBase64 }],
358
426
  * )
427
+ * for (const w of warnings) console.warn(w.message)
359
428
  * ```
360
429
  *
361
430
  * @example Print a ZPL label
@@ -366,7 +435,7 @@ export declare class EdgePrintClient {
366
435
  * )
367
436
  * ```
368
437
  */
369
- print(config: PrintConfig, data: PrintData[]): Promise<string>;
438
+ print(config: PrintConfig, data: PrintData[]): Promise<PrintResult>;
370
439
  /**
371
440
  * Close the WebSocket connection and reset client state.
372
441
  *
@@ -10,12 +10,108 @@
10
10
  *
11
11
  * await ep.connect({ token: 'your-api-token' })
12
12
  * const printers = await ep.printers()
13
- * const jobId = await ep.print(
13
+ * const { jobId, warnings } = await ep.print(
14
14
  * { printer: 'Office Laser' },
15
15
  * [{ type: 'pixel', format: 'pdf', flavor: 'base64', data: pdfBase64 }],
16
16
  * )
17
17
  * ```
18
18
  */
19
+ /**
20
+ * Every failure code this client understands.
21
+ *
22
+ * Declared as a runtime array first, with the union derived from it — a
23
+ * hand-written union would be erased at compile time, leaving nothing to
24
+ * validate a wire value against, and the two would drift the first time the
25
+ * agent gained a code.
26
+ *
27
+ * The first group is the agent's closed set. The second is raised locally and
28
+ * never sent by the agent. `unknown` is the fallback for a code this client
29
+ * does not recognise, which is what lets a new client work against an old agent.
30
+ */
31
+ const AGENT_CODES = [
32
+ 'not_authenticated', 'invalid_token', 'protocol_mismatch', 'invalid_request',
33
+ 'unsupported_value', 'unsupported_capability', 'invalid_geometry',
34
+ 'content_overflow', 'unsupported_for_path', 'encrypted_pdf',
35
+ 'printer_not_found', 'print_failed', 'internal_error',
36
+ ];
37
+ /**
38
+ * Raised here, never accepted off the wire.
39
+ *
40
+ * Kept separate so `fromWire` can reject them. An agent — or anything between
41
+ * the client and it — sending `code: "timeout"` would otherwise be taken at
42
+ * face value, and `retryable` would report `true` for a failure the agent
43
+ * considers final. `invalid_request` is deliberately *not* in this list: both
44
+ * sides can raise it, and a request this client refuses to serialise is the
45
+ * same kind of failure as one the agent refuses to parse.
46
+ */
47
+ const CLIENT_ONLY_CODES = ['not_connected', 'unreachable', 'connection_closed', 'timeout'];
48
+ const ERROR_CODES = [...AGENT_CODES, ...CLIENT_ONLY_CODES, 'unknown'];
49
+ /** Codes where the same request may succeed if simply tried again. */
50
+ const RETRYABLE = ['unreachable', 'connection_closed', 'timeout'];
51
+ /**
52
+ * A failure from the Edge Printing agent or from this client.
53
+ *
54
+ * Extends `Error`, so `e.message` and `String(e)` keep working for code
55
+ * written before codes existed.
56
+ *
57
+ * **Do not set `name` on this class.** A class name does not set `.name` — it
58
+ * is inherited as `"Error"` — so `String(e)` stays `"Error: …"`. Setting it
59
+ * would change that string for every existing consumer, which is exactly what
60
+ * extending `Error` is here to avoid.
61
+ */
62
+ export class EdgePrintError extends Error {
63
+ constructor(code, message, detail = {}) {
64
+ super(message);
65
+ this.code = code;
66
+ this.field = detail.field;
67
+ this.supported = detail.supported;
68
+ this.requestId = detail.requestId;
69
+ this.jobId = detail.jobId;
70
+ }
71
+ /**
72
+ * Builds an error from an agent failure frame.
73
+ *
74
+ * Everything is read defensively: an older agent sends no `code` at all, and
75
+ * a newer one may send a code this client has never heard of. Both become
76
+ * `'unknown'` rather than reaching a caller as an invalid value.
77
+ */
78
+ static fromWire(msg) {
79
+ const raw = msg['code'];
80
+ // Checked against the agent's set, not the union: see CLIENT_ONLY_CODES.
81
+ const code = AGENT_CODES.includes(raw)
82
+ ? raw
83
+ : 'unknown';
84
+ const str = (v) => (typeof v === 'string' && v.length > 0 ? v : undefined);
85
+ const supported = Array.isArray(msg['supported'])
86
+ ? msg['supported'].filter((v) => typeof v === 'string')
87
+ : undefined;
88
+ return new EdgePrintError(code, str(msg['message']) ?? 'Unknown error', {
89
+ field: str(msg['field']),
90
+ supported,
91
+ requestId: str(msg['id']),
92
+ jobId: str(msg['jobId']),
93
+ });
94
+ }
95
+ /**
96
+ * Whether the same request might succeed on a retry.
97
+ *
98
+ * True only for transport failures. A validation rejection is deterministic:
99
+ * retrying it sends the same bad request again.
100
+ */
101
+ get retryable() {
102
+ return RETRYABLE.includes(this.code);
103
+ }
104
+ }
105
+ /**
106
+ * The message an `Error` carries, or the value itself stringified.
107
+ *
108
+ * `String(err)` on an `Error` yields `"TypeError: foo"`, not `"foo"`. Using it
109
+ * to build a replacement error would silently prefix every such message with a
110
+ * constructor name.
111
+ */
112
+ function messageOf(err) {
113
+ return err instanceof Error ? err.message : String(err);
114
+ }
19
115
  /**
20
116
  * The wire protocol this client speaks.
21
117
  *
@@ -121,7 +217,7 @@ export class EdgePrintClient {
121
217
  * connect-on-demand helper with {@link EdgePrintClient.isConnected} rather
122
218
  * than reconnecting unconditionally.
123
219
  *
124
- * @throws {Error} If the agent is unreachable or the token is rejected after
220
+ * @throws {EdgePrintError} If the agent is unreachable or the token is rejected after
125
221
  * all retries are exhausted.
126
222
  *
127
223
  * @example
@@ -148,7 +244,7 @@ export class EdgePrintClient {
148
244
  // discarding whatever connection was established while it slept, and
149
245
  // reconnecting after an explicit disconnect.
150
246
  if (this.connectGeneration !== generation) {
151
- throw new Error('Connection superseded');
247
+ throw new EdgePrintError('connection_closed', 'Connection superseded');
152
248
  }
153
249
  try {
154
250
  await this.openSocket(`wss://${host}:${port}`);
@@ -169,7 +265,7 @@ export class EdgePrintClient {
169
265
  ? ack['protocolVersion']
170
266
  : LEGACY_PROTOCOL_VERSION;
171
267
  if (agentProtocol !== PROTOCOL_VERSION) {
172
- throw new Error(`Protocol version mismatch: this client speaks protocol version ` +
268
+ throw new EdgePrintError('protocol_mismatch', `Protocol version mismatch: this client speaks protocol version ` +
173
269
  `${PROTOCOL_VERSION}, but the agent speaks ${agentProtocol}. ` +
174
270
  (agentProtocol < PROTOCOL_VERSION
175
271
  ? 'Update the Edge Printing agent.'
@@ -182,7 +278,7 @@ export class EdgePrintClient {
182
278
  // same check the loop makes before each attempt and in the catch, which
183
279
  // between them cover every path except this one.
184
280
  if (this.connectGeneration !== generation) {
185
- throw new Error('Connection superseded');
281
+ throw new EdgePrintError('connection_closed', 'Connection superseded');
186
282
  }
187
283
  // Assigned together so the getters and `isConnected()` agree: either
188
284
  // this attempt owns the connection and publishes all of it, or none.
@@ -199,7 +295,7 @@ export class EdgePrintClient {
199
295
  // is not this attempt's to tear down.
200
296
  if (this.connectGeneration !== generation)
201
297
  throw err;
202
- this.discardSocket(new Error('Connection closed'));
298
+ this.discardSocket(new EdgePrintError('connection_closed', 'Connection closed'));
203
299
  // Every failure retries, including a refused token. The agent answers a
204
300
  // token still awaiting approval with the same "Invalid token" it gives
205
301
  // a bad one, so the client cannot tell them apart — and retrying while
@@ -216,7 +312,7 @@ export class EdgePrintClient {
216
312
  /**
217
313
  * Return all printers available on the agent machine.
218
314
  *
219
- * @throws {Error} If not connected.
315
+ * @throws {EdgePrintError} If not connected.
220
316
  *
221
317
  * @example
222
318
  * ```ts
@@ -234,7 +330,7 @@ export class EdgePrintClient {
234
330
  * Cheaper than calling {@link printers} when you only need the default name
235
331
  * and no other printer metadata.
236
332
  *
237
- * @throws {Error} If not connected.
333
+ * @throws {EdgePrintError} If not connected.
238
334
  */
239
335
  async defaultPrinter() {
240
336
  const resp = await this.request('get_default_printer', {});
@@ -245,10 +341,13 @@ export class EdgePrintClient {
245
341
  *
246
342
  * @param config - Printer selection and job settings.
247
343
  * @param data - One or more content items to print (pages, labels, …).
248
- * @returns The job ID assigned by the agent.
344
+ * @returns A {@link PrintResult} the agent's `jobId`, and any `warnings`
345
+ * it recorded about a job it printed anyway. `warnings` is empty in the
346
+ * common case.
249
347
  *
250
- * @throws {Error} If not connected, if the agent rejects the job, or if the
251
- * job fails on the way to the spooler.
348
+ * @throws {EdgePrintError} If not connected, if the agent rejects the job, or
349
+ * if the job fails on the way to the spooler. Branch on `err.code`, never
350
+ * on the message text.
252
351
  *
253
352
  * A resolved promise means the job was handed to the OS print spooler — not
254
353
  * that paper came out. A printer that is offline, jammed or out of paper
@@ -258,24 +357,31 @@ export class EdgePrintClient {
258
357
  * the client's 30 s timeout rejects while the agent may still be spooling it.
259
358
  * Do not resubmit a print automatically on rejection.
260
359
  *
261
- * When the agent had already created a job before it failed, the thrown error
262
- * carries a `jobId` property matching the entry in the agent's job history —
263
- * useful when surfacing a failure someone has to chase:
360
+ * A job rejected by validation is still recorded, so the thrown error
361
+ * usually carries a `jobId` matching the entry in the agent's job history —
362
+ * useful when surfacing a failure someone has to chase. A request refused
363
+ * before a job existed at all, such as one the agent could not parse, has no
364
+ * `jobId` to carry.
365
+ *
366
+ * A rejection never carries warnings, even where the agent had accumulated
367
+ * some before the rule that failed: they are dropped deliberately rather
368
+ * than reported alongside an unrelated failure.
264
369
  *
265
370
  * ```ts
266
371
  * try {
267
372
  * await ep.print(config, data)
268
373
  * } catch (err) {
269
- * const jobId = (err as Error & { jobId?: string }).jobId
374
+ * if (err instanceof EdgePrintError) console.error(err.code, err.jobId)
270
375
  * }
271
376
  * ```
272
377
  *
273
378
  * @example Print a PDF
274
379
  * ```ts
275
- * const jobId = await ep.print(
380
+ * const { jobId, warnings } = await ep.print(
276
381
  * { printer: 'Office Laser', copies: 2, duplex: 'long-edge' },
277
382
  * [{ type: 'pixel', format: 'pdf', flavor: 'base64', data: pdfBase64 }],
278
383
  * )
384
+ * for (const w of warnings) console.warn(w.message)
279
385
  * ```
280
386
  *
281
387
  * @example Print a ZPL label
@@ -288,7 +394,9 @@ export class EdgePrintClient {
288
394
  */
289
395
  async print(config, data) {
290
396
  const resp = await this.request('print', { config, data });
291
- return resp.jobId;
397
+ // Defaulted here rather than left optional: a caller destructuring
398
+ // `warnings` should get an array to iterate, not `undefined` to guard.
399
+ return { jobId: resp.jobId, warnings: resp.warnings ?? [] };
292
400
  }
293
401
  /**
294
402
  * Close the WebSocket connection and reset client state.
@@ -303,7 +411,7 @@ export class EdgePrintClient {
303
411
  // Invalidates any connect still running, so a retry cannot wake up after
304
412
  // this and quietly reconnect.
305
413
  this.connectGeneration++;
306
- this.discardSocket(new Error('Disconnected'));
414
+ this.discardSocket(new EdgePrintError('connection_closed', 'Disconnected'));
307
415
  // Detaching the handlers means the socket's own close never arrives, so
308
416
  // without this the loss would go unannounced.
309
417
  this.markClosed();
@@ -430,7 +538,7 @@ export class EdgePrintClient {
430
538
  return new Promise((resolve, reject) => {
431
539
  // Never hold two sockets. This covers a retry, a reconnect on a live
432
540
  // client, and a second connect racing the first.
433
- this.discardSocket(new Error('Connection superseded'));
541
+ this.discardSocket(new EdgePrintError('connection_closed', 'Connection superseded'));
434
542
  const ws = new WebSocket(url);
435
543
  this.ws = ws;
436
544
  this.pendingOpen = reject;
@@ -438,7 +546,7 @@ export class EdgePrintClient {
438
546
  ws.onopen = () => { settled(); resolve(); };
439
547
  ws.onerror = () => {
440
548
  settled();
441
- reject(new Error(`Cannot reach Edge Printing agent at ${url}`));
549
+ reject(new EdgePrintError('unreachable', `Cannot reach Edge Printing agent at ${url}`));
442
550
  };
443
551
  ws.onmessage = (ev) => this.handleMessage(String(ev.data));
444
552
  ws.onclose = () => {
@@ -448,10 +556,10 @@ export class EdgePrintClient {
448
556
  // cover the open.
449
557
  const openWaiting = this.pendingOpen;
450
558
  settled();
451
- openWaiting?.(new Error(`Cannot reach Edge Printing agent at ${url}`));
559
+ openWaiting?.(new EdgePrintError('unreachable', `Cannot reach Edge Printing agent at ${url}`));
452
560
  this.ws = null;
453
561
  this.clearSessionState();
454
- this.rejectPending(new Error('Connection closed'));
562
+ this.rejectPending(new EdgePrintError('connection_closed', 'Connection closed'));
455
563
  this.markClosed();
456
564
  };
457
565
  });
@@ -476,17 +584,11 @@ export class EdgePrintClient {
476
584
  // than silently resolving.
477
585
  const type = String(msg['type'] ?? '');
478
586
  if (type === 'error' || type.endsWith('_error')) {
479
- const failure = new Error(msg['message'] ?? 'Unknown error');
480
587
  // `print_error` carries the id of the job the agent created and then
481
588
  // failed, which is the handle a caller needs to find it in the agent's
482
- // job history. Attached rather than given an exported error type: the
483
- // typed client error that formalises this arrives later, and a second
484
- // error shape now would only have to be reconciled with it.
485
- const jobId = msg['jobId'];
486
- if (typeof jobId === 'string' && jobId.length > 0) {
487
- Object.assign(failure, { jobId });
488
- }
489
- reject(failure);
589
+ // job history. `fromWire` reads that alongside the code, field and
590
+ // supported values, so the whole failure arrives as one typed object.
591
+ reject(EdgePrintError.fromWire(msg));
490
592
  }
491
593
  else {
492
594
  resolve(msg);
@@ -495,13 +597,13 @@ export class EdgePrintClient {
495
597
  request(type, payload) {
496
598
  return new Promise((resolve, reject) => {
497
599
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
498
- reject(new Error('Not connected to Edge Printing agent'));
600
+ reject(new EdgePrintError('not_connected', 'Not connected to Edge Printing agent'));
499
601
  return;
500
602
  }
501
603
  const id = crypto.randomUUID();
502
604
  const timeout = setTimeout(() => {
503
605
  if (this.pending.delete(id)) {
504
- reject(new Error(`Request "${type}" timed out`));
606
+ reject(new EdgePrintError('timeout', `Request "${type}" timed out`, { requestId: id }));
505
607
  }
506
608
  }, 30000);
507
609
  // Registered once, already wrapped. The previous version inserted the
@@ -515,13 +617,32 @@ export class EdgePrintClient {
515
617
  resolve: (v) => { clearTimeout(timeout); resolve(v); },
516
618
  reject: (e) => { clearTimeout(timeout); reject(e); },
517
619
  });
620
+ // Serialised outside the try below: a payload that cannot be stringified
621
+ // — a circular reference, a BigInt — is a bad request, not a dead socket,
622
+ // and labelling it `connection_closed` would make `retryable` true for a
623
+ // failure that reproduces identically on every retry.
624
+ let frame;
625
+ try {
626
+ frame = JSON.stringify({ type, id, ...payload });
627
+ }
628
+ catch (err) {
629
+ clearTimeout(timeout);
630
+ this.pending.delete(id);
631
+ reject(new EdgePrintError('invalid_request', messageOf(err), { requestId: id }));
632
+ return;
633
+ }
518
634
  try {
519
- this.ws.send(JSON.stringify({ type, id, ...payload }));
635
+ this.ws.send(frame);
520
636
  }
521
637
  catch (err) {
522
638
  clearTimeout(timeout);
523
639
  this.pending.delete(id);
524
- reject(err instanceof Error ? err : new Error(String(err)));
640
+ // Coded like everything else this client raises, so a caller never has
641
+ // to ask whether `e.code` exists before reading it. `messageOf` rather
642
+ // than `String(err)`: the latter prepends the constructor name, so a
643
+ // caller's `e.message` would gain a "TypeError: " it never had before
644
+ // codes existed — the one thing extending `Error` is here to preserve.
645
+ reject(new EdgePrintError('connection_closed', messageOf(err), { requestId: id }));
525
646
  }
526
647
  });
527
648
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gohcltech/edge-print-client",
3
- "version": "2.0.54-develop",
3
+ "version": "2.0.61-develop",
4
4
  "description": "Browser client for the Edge Printing WebSocket agent",
5
5
  "license": "MIT",
6
6
  "repository": {