@gohcltech/edge-print-client 2.0.57-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 +140 -13
- package/dist/edge-print.d.ts +82 -13
- package/dist/edge-print.js +156 -35
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
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()`
|
|
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()`
|
|
502
|
+
### `connect()` rejects with `code: 'invalid_token'`
|
|
383
503
|
|
|
384
|
-
The token was rejected
|
|
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()`
|
|
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
|
-
###
|
|
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
|
-
|
|
405
|
-
|
|
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
|
|
package/dist/edge-print.d.ts
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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 {
|
|
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
|
|
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 {
|
|
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
|
-
*
|
|
342
|
-
* carries a `jobId`
|
|
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
|
-
*
|
|
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<
|
|
438
|
+
print(config: PrintConfig, data: PrintData[]): Promise<PrintResult>;
|
|
370
439
|
/**
|
|
371
440
|
* Close the WebSocket connection and reset client state.
|
|
372
441
|
*
|
package/dist/edge-print.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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 {
|
|
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
|
|
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 {
|
|
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
|
-
*
|
|
262
|
-
* carries a `jobId`
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
483
|
-
//
|
|
484
|
-
|
|
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
|
|
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
|
|
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(
|
|
635
|
+
this.ws.send(frame);
|
|
520
636
|
}
|
|
521
637
|
catch (err) {
|
|
522
638
|
clearTimeout(timeout);
|
|
523
639
|
this.pending.delete(id);
|
|
524
|
-
|
|
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
|
}
|