@gohcltech/edge-print-client 2.0.57-develop → 2.0.70-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 +153 -13
- package/dist/edge-print.d.ts +104 -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()
|
|
@@ -118,10 +119,22 @@ Each `PrinterInfo` object includes:
|
|
|
118
119
|
| `duplex` | `boolean?` | `true` if the printer supports double-sided printing |
|
|
119
120
|
| `trays` | `TrayInfo[]?` | Input trays/bins (see below) |
|
|
120
121
|
| `papers` | `string[]?` | All supported paper sizes (e.g. `["Letter", "A4"]`) |
|
|
122
|
+
| `mediaTypes` | `string[]?` | Stock types the driver reports (e.g. `["Plain", "Bond"]`). Absent for most printers — see below |
|
|
121
123
|
| `copiesMax` | `number?` | Maximum copies the driver accepts in one job |
|
|
122
124
|
| `orientations` | `string[]?` | Supported orientations (e.g. `["portrait", "landscape"]`) |
|
|
123
125
|
| `virtualPrinter` | `boolean?` | `true` for Developer (virtual) printers injected by the agent |
|
|
124
126
|
|
|
127
|
+
**On `mediaTypes`.** The field is usually absent. Of eleven drivers surveyed,
|
|
128
|
+
eight listed no stock types at all, and a ninth listed them only under a
|
|
129
|
+
vendor-private key whose values are the driver's internal codes rather than
|
|
130
|
+
names you could choose from — the agent reports nothing in that case too. When
|
|
131
|
+
the field is absent the agent accepts whatever stock you ask for rather than
|
|
132
|
+
guessing. When it *is*
|
|
133
|
+
present the strings are the driver's own and are not always self-describing, so
|
|
134
|
+
read them off the target machine (the agent's Printers tab lists them) rather
|
|
135
|
+
than assuming a vocabulary. Passing a value the printer does not list rejects
|
|
136
|
+
with `unsupported_value`, and the error's `supported` array names the real ones.
|
|
137
|
+
|
|
125
138
|
1.x agents sent `is_default`, `share_name`, `copies_max` and `virtual_printer`.
|
|
126
139
|
Those spellings are still accepted on read and mapped onto the camelCase fields
|
|
127
140
|
above, so nothing downstream sees the old names. **Removed in 3.0.**
|
|
@@ -152,24 +165,53 @@ const name = await ep.defaultPrinter() // "Office Laser"
|
|
|
152
165
|
|
|
153
166
|
### `print(config, data)`
|
|
154
167
|
|
|
155
|
-
Submits a print job.
|
|
168
|
+
Submits a print job. Resolves to a `PrintResult`.
|
|
156
169
|
|
|
157
170
|
```ts
|
|
158
|
-
const jobId = await ep.print(config, data)
|
|
171
|
+
const { jobId, warnings } = await ep.print(config, data)
|
|
159
172
|
```
|
|
160
173
|
|
|
174
|
+
| Field | Type | Description |
|
|
175
|
+
|---|---|---|
|
|
176
|
+
| `jobId` | `string` | The agent's id for the job, matching its entry in the job history |
|
|
177
|
+
| `warnings` | `JobWarning[]` | Caveats the agent recorded about a job it printed anyway. Empty in the common case |
|
|
178
|
+
|
|
179
|
+
Each `JobWarning` is `{ code, message }`. Today the only code is
|
|
180
|
+
`capability_unverified` — the agent could not determine a capability, so it
|
|
181
|
+
skipped the check and passed the request through. Treat the set as open-ended:
|
|
182
|
+
log or surface `message` rather than branching on every code.
|
|
183
|
+
|
|
184
|
+
> **Breaking change.** `print()` used to resolve to the job id string. It now
|
|
185
|
+
> resolves to an object. Destructure it (`const { jobId } = await ep.print(…)`),
|
|
186
|
+
> or the value you pass on will be `[object Object]`. TypeScript callers get a
|
|
187
|
+
> compile error; JavaScript callers do not.
|
|
188
|
+
|
|
161
189
|
Rejects if the job is refused, and also if it fails on the way to the spooler —
|
|
162
190
|
where it previously resolved with a job id regardless. Always handle the
|
|
163
|
-
rejection:
|
|
191
|
+
rejection, and branch on `err.code` rather than the message text:
|
|
164
192
|
|
|
165
193
|
```ts
|
|
194
|
+
import { EdgePrintError } from '@gohcltech/edge-print-client'
|
|
195
|
+
|
|
166
196
|
try {
|
|
167
|
-
const jobId = await ep.print(config, data)
|
|
197
|
+
const { jobId } = await ep.print(config, data)
|
|
168
198
|
} catch (err) {
|
|
169
|
-
|
|
199
|
+
if (err instanceof EdgePrintError && err.code === 'unsupported_capability') {
|
|
200
|
+
// err.field names the setting the printer cannot do — 'colorType', 'duplex'
|
|
201
|
+
}
|
|
170
202
|
}
|
|
171
203
|
```
|
|
172
204
|
|
|
205
|
+
A job rejected by validation still gets a record in the agent's History, so an
|
|
206
|
+
`EdgePrintError` from `print()` usually carries a `jobId` you can quote to
|
|
207
|
+
support. A request refused before a job existed — one the agent could not parse
|
|
208
|
+
— has no `jobId`.
|
|
209
|
+
|
|
210
|
+
A rejection never carries warnings, even where the agent had already recorded
|
|
211
|
+
some before the rule that failed. They are dropped deliberately: a note saying
|
|
212
|
+
"we could not verify duplex" alongside a rejection for an unrelated reason is
|
|
213
|
+
noise, not help.
|
|
214
|
+
|
|
173
215
|
Two limits worth knowing before you build on this:
|
|
174
216
|
|
|
175
217
|
- **Resolved means spooled, not printed.** The agent resolves once the OS print
|
|
@@ -193,6 +235,7 @@ Two limits worth knowing before you build on this:
|
|
|
193
235
|
| `colorType` | `'color' \| 'grayscale' \| 'black-white'?` | Color mode |
|
|
194
236
|
| `paperSize` | `string?` | Paper size from `PrinterInfo.papers` (e.g. `"Letter"`) |
|
|
195
237
|
| `tray` | `string?` | Tray name from `TrayInfo.name` (e.g. `"Tray 1"`) |
|
|
238
|
+
| `mediaType` | `string?` | Stock type from `PrinterInfo.mediaTypes` (e.g. `"Bond"`). Rejected for `raw`/`html` content and network targets |
|
|
196
239
|
|
|
197
240
|
**`data`** — `PrintData[]`
|
|
198
241
|
|
|
@@ -309,6 +352,96 @@ attempt, and they all fire on the next close.
|
|
|
309
352
|
|
|
310
353
|
---
|
|
311
354
|
|
|
355
|
+
## Errors
|
|
356
|
+
|
|
357
|
+
Every rejection from this library is an `EdgePrintError` — including the ones
|
|
358
|
+
raised locally, before a request ever reaches the agent. It extends `Error`, so
|
|
359
|
+
`err.message` and `String(err)` read exactly as they did before codes existed.
|
|
360
|
+
|
|
361
|
+
```ts
|
|
362
|
+
import ep, { EdgePrintError } from '@gohcltech/edge-print-client'
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
await ep.print(config, data)
|
|
366
|
+
} catch (err) {
|
|
367
|
+
if (!(err instanceof EdgePrintError)) throw err
|
|
368
|
+
|
|
369
|
+
if (err.retryable) {
|
|
370
|
+
// transport trouble — the same request may well work on a second attempt
|
|
371
|
+
} else if (err.field) {
|
|
372
|
+
console.error(`${err.field}: ${err.message}`)
|
|
373
|
+
} else {
|
|
374
|
+
console.error(err.message)
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
| Property | Type | Description |
|
|
380
|
+
|---|---|---|
|
|
381
|
+
| `code` | `EdgePrintErrorCode` | The failure, from a closed set. `'unknown'` when the agent sent no code, or one this client predates |
|
|
382
|
+
| `message` | `string` | Human-readable. Do not match on it — that is what `code` is for |
|
|
383
|
+
| `field` | `string?` | The offending setting, when one setting is to blame |
|
|
384
|
+
| `supported` | `string[]?` | What the printer accepts, for the codes that can say |
|
|
385
|
+
| `requestId` | `string?` | The request this answers. Absent for failures raised before one existed |
|
|
386
|
+
| `jobId` | `string?` | Present when the agent created a job and then failed it |
|
|
387
|
+
| `retryable` | `boolean` | True only for transport failures — see below |
|
|
388
|
+
|
|
389
|
+
### Codes
|
|
390
|
+
|
|
391
|
+
Sent by the agent:
|
|
392
|
+
|
|
393
|
+
| Code | Raised when |
|
|
394
|
+
|---|---|
|
|
395
|
+
| `not_authenticated` | A request was sent before `connect()` finished authenticating |
|
|
396
|
+
| `invalid_token` | The token is unknown, or still awaiting approval in the agent UI |
|
|
397
|
+
| `invalid_request` | The agent could not parse the request |
|
|
398
|
+
| `unsupported_capability` | A capability the printer explicitly reports it lacks |
|
|
399
|
+
| `invalid_geometry` | A scale or margin outside what the protocol accepts |
|
|
400
|
+
| `unsupported_for_path` | A setting that cannot be honoured on the requested path |
|
|
401
|
+
| `printer_not_found` | `defaultPrinter()` found no default configured on the machine |
|
|
402
|
+
| `print_failed` | The backend failed after the job was accepted and recorded |
|
|
403
|
+
| `internal_error` | The agent itself failed — not the caller's doing |
|
|
404
|
+
|
|
405
|
+
Raised locally by this library, never sent by the agent:
|
|
406
|
+
|
|
407
|
+
| Code | Raised when |
|
|
408
|
+
|---|---|
|
|
409
|
+
| `not_connected` | A call was made while disconnected |
|
|
410
|
+
| `unreachable` | The WebSocket could not be opened — agent not running, or wrong host/port |
|
|
411
|
+
| `protocol_mismatch` | The agent and this library speak different wire protocol versions |
|
|
412
|
+
| `connection_closed` | The connection dropped with a request in flight |
|
|
413
|
+
| `timeout` | The agent accepted the request and did not answer within 30 seconds |
|
|
414
|
+
|
|
415
|
+
`invalid_request` is raised by **both** sides: by the agent for a request it
|
|
416
|
+
cannot parse, and here for a `config` or `data` value that cannot be serialised
|
|
417
|
+
to JSON at all — a circular reference, say. Neither is retryable.
|
|
418
|
+
|
|
419
|
+
The four codes in the table above are never accepted *from* the agent. A frame
|
|
420
|
+
claiming `code: "timeout"` is read as `unknown`, so a failure the agent
|
|
421
|
+
considers final cannot arrive labelled retryable.
|
|
422
|
+
|
|
423
|
+
Three more are **reserved**: `unsupported_value`, `content_overflow` and
|
|
424
|
+
`encrypted_pdf` are part of the closed set and typed here, so code written
|
|
425
|
+
today handles them, but no agent sends them yet. `supported` is populated only
|
|
426
|
+
by `unsupported_value`, so it is always `undefined` against an agent of this
|
|
427
|
+
generation.
|
|
428
|
+
|
|
429
|
+
And `unknown`, the fallback for a code this client does not recognise — which
|
|
430
|
+
is what lets a client built today keep working against a future agent. Treat an
|
|
431
|
+
unfamiliar code as a plain failure rather than a bug.
|
|
432
|
+
|
|
433
|
+
### `retryable`
|
|
434
|
+
|
|
435
|
+
`err.retryable` is true for `unreachable`, `connection_closed`, and `timeout`,
|
|
436
|
+
and false for everything else. A validation rejection is deterministic: retrying
|
|
437
|
+
it sends the same bad request again.
|
|
438
|
+
|
|
439
|
+
Note that `retryable` says the *request* may succeed — not that nothing
|
|
440
|
+
happened. See the `timeout` entry under Troubleshooting before you retry a
|
|
441
|
+
print.
|
|
442
|
+
|
|
443
|
+
---
|
|
444
|
+
|
|
312
445
|
## Using a custom instance
|
|
313
446
|
|
|
314
447
|
The default export `ep` is a module-level singleton. If you need to connect to multiple agents simultaneously, create separate instances:
|
|
@@ -374,16 +507,18 @@ To confirm the certificate is trusted, open `https://127.0.0.1:8181` in the
|
|
|
374
507
|
browser. If the page loads (even with an empty body) the certificate is fine.
|
|
375
508
|
If you see a security warning, the certificate is not yet trusted.
|
|
376
509
|
|
|
377
|
-
### `connect()`
|
|
510
|
+
### `connect()` rejects with `code: 'unreachable'`
|
|
378
511
|
|
|
379
512
|
- Confirm the Edge Printing agent is running (check the system tray / menu bar).
|
|
380
513
|
- Confirm `host` and `port` match the agent's configured values (default `127.0.0.1:8181`).
|
|
381
514
|
|
|
382
|
-
### `connect()`
|
|
515
|
+
### `connect()` rejects with `code: 'invalid_token'`
|
|
383
516
|
|
|
384
|
-
The token was rejected
|
|
517
|
+
The token was rejected, or it is new and still awaiting approval in the agent's
|
|
518
|
+
Clients tab. Approve it there, or generate a fresh one from the Edge Printing
|
|
519
|
+
settings window.
|
|
385
520
|
|
|
386
|
-
### `connect()`
|
|
521
|
+
### `connect()` rejects with `code: 'protocol_mismatch'`
|
|
387
522
|
|
|
388
523
|
The agent and this library speak different wire protocol versions. The message
|
|
389
524
|
names both and says which side to update:
|
|
@@ -399,10 +534,15 @@ A mismatch is deterministic, so `connect()`'s retries cannot clear it — it sti
|
|
|
399
534
|
exhausts them (about 3 seconds by default) before throwing. Pass `retries: 0`
|
|
400
535
|
when probing for a usable agent. One side has to be upgraded.
|
|
401
536
|
|
|
402
|
-
###
|
|
537
|
+
### A request rejects with `code: 'timeout'`
|
|
538
|
+
|
|
539
|
+
The agent accepted the connection and did not answer within 30 seconds. Restart
|
|
540
|
+
the agent. If the problem persists, file an issue with the agent log attached.
|
|
403
541
|
|
|
404
|
-
|
|
405
|
-
|
|
542
|
+
For a `print()`, a timeout does **not** prove nothing printed — a large PDF can
|
|
543
|
+
take longer than 30 seconds to spool, and the rejection arrives while the agent
|
|
544
|
+
is still working. Never resubmit a print automatically; surface it and let a
|
|
545
|
+
person decide.
|
|
406
546
|
|
|
407
547
|
---
|
|
408
548
|
|
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
|
* )
|
|
@@ -66,6 +66,16 @@ export interface PrintConfig {
|
|
|
66
66
|
* Omit to use the printer's current default.
|
|
67
67
|
*/
|
|
68
68
|
tray?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Media / stock type, exactly as returned by {@link PrinterInfo.mediaTypes}
|
|
71
|
+
* (e.g. `"Plain"`, `"Bond"`, `"Thermal"`). Changes how the printer feeds and
|
|
72
|
+
* fuses the page. Omit to use the printer's current default.
|
|
73
|
+
*
|
|
74
|
+
* Not supported for `raw` or `html` content, or a network target — those
|
|
75
|
+
* paths never reach the printer's driver. Requesting it there rejects with
|
|
76
|
+
* `unsupported_for_path`.
|
|
77
|
+
*/
|
|
78
|
+
mediaType?: string;
|
|
69
79
|
}
|
|
70
80
|
/**
|
|
71
81
|
* A single unit of content to be printed.
|
|
@@ -178,6 +188,18 @@ export interface PrinterInfo {
|
|
|
178
188
|
* Pass one of these values to {@link PrintConfig.paperSize}.
|
|
179
189
|
*/
|
|
180
190
|
papers?: string[];
|
|
191
|
+
/**
|
|
192
|
+
* Stock types the driver reports — card stock, labels, envelopes and so on.
|
|
193
|
+
* Pass one of these values to {@link PrintConfig.mediaType}.
|
|
194
|
+
*
|
|
195
|
+
* Absent for most printers — many drivers report no stock types, and some
|
|
196
|
+
* report them only under a vendor-private key whose values are internal
|
|
197
|
+
* codes rather than choosable names. The agent reports nothing in either
|
|
198
|
+
* case and then accepts whatever is asked rather than guessing. When the
|
|
199
|
+
* field *is* present, the strings are the driver's own and may not be
|
|
200
|
+
* self-describing.
|
|
201
|
+
*/
|
|
202
|
+
mediaTypes?: string[];
|
|
181
203
|
/** Maximum number of copies the driver accepts in a single job. */
|
|
182
204
|
copiesMax?: number;
|
|
183
205
|
/** Page orientations the driver supports (e.g. `["portrait", "landscape"]`). */
|
|
@@ -228,6 +250,65 @@ export interface ConnectOptions {
|
|
|
228
250
|
*/
|
|
229
251
|
retryDelay?: number;
|
|
230
252
|
}
|
|
253
|
+
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"];
|
|
254
|
+
export type EdgePrintErrorCode = typeof ERROR_CODES[number];
|
|
255
|
+
/** A warning the agent recorded about a job it nonetheless printed. */
|
|
256
|
+
export interface JobWarning {
|
|
257
|
+
/** e.g. `"capability_unverified"`. Open-ended: an agent may add codes. */
|
|
258
|
+
code: string;
|
|
259
|
+
message: string;
|
|
260
|
+
}
|
|
261
|
+
/** What {@link EdgePrintClient.print} resolves to. */
|
|
262
|
+
export interface PrintResult {
|
|
263
|
+
/** The agent's id for the job, matching its entry in the job history. */
|
|
264
|
+
jobId: string;
|
|
265
|
+
/** Empty when the agent had nothing to report, which is the common case. */
|
|
266
|
+
warnings: JobWarning[];
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* A failure from the Edge Printing agent or from this client.
|
|
270
|
+
*
|
|
271
|
+
* Extends `Error`, so `e.message` and `String(e)` keep working for code
|
|
272
|
+
* written before codes existed.
|
|
273
|
+
*
|
|
274
|
+
* **Do not set `name` on this class.** A class name does not set `.name` — it
|
|
275
|
+
* is inherited as `"Error"` — so `String(e)` stays `"Error: …"`. Setting it
|
|
276
|
+
* would change that string for every existing consumer, which is exactly what
|
|
277
|
+
* extending `Error` is here to avoid.
|
|
278
|
+
*/
|
|
279
|
+
export declare class EdgePrintError extends Error {
|
|
280
|
+
/** `'unknown'` when the agent sent no code, or one this client predates. */
|
|
281
|
+
readonly code: EdgePrintErrorCode;
|
|
282
|
+
/** The offending setting, when one setting is to blame. */
|
|
283
|
+
readonly field?: string;
|
|
284
|
+
/** What the printer accepts, for the codes that can say. */
|
|
285
|
+
readonly supported?: string[];
|
|
286
|
+
/** The request this answers. Absent for failures raised before one existed. */
|
|
287
|
+
readonly requestId?: string;
|
|
288
|
+
/** Present when the agent created a job and then failed it. */
|
|
289
|
+
readonly jobId?: string;
|
|
290
|
+
constructor(code: EdgePrintErrorCode, message: string, detail?: {
|
|
291
|
+
field?: string;
|
|
292
|
+
supported?: string[];
|
|
293
|
+
requestId?: string;
|
|
294
|
+
jobId?: string;
|
|
295
|
+
});
|
|
296
|
+
/**
|
|
297
|
+
* Builds an error from an agent failure frame.
|
|
298
|
+
*
|
|
299
|
+
* Everything is read defensively: an older agent sends no `code` at all, and
|
|
300
|
+
* a newer one may send a code this client has never heard of. Both become
|
|
301
|
+
* `'unknown'` rather than reaching a caller as an invalid value.
|
|
302
|
+
*/
|
|
303
|
+
static fromWire(msg: Record<string, unknown>): EdgePrintError;
|
|
304
|
+
/**
|
|
305
|
+
* Whether the same request might succeed on a retry.
|
|
306
|
+
*
|
|
307
|
+
* True only for transport failures. A validation rejection is deterministic:
|
|
308
|
+
* retrying it sends the same bad request again.
|
|
309
|
+
*/
|
|
310
|
+
get retryable(): boolean;
|
|
311
|
+
}
|
|
231
312
|
/**
|
|
232
313
|
* WebSocket client for the Edge Printing agent.
|
|
233
314
|
*
|
|
@@ -284,7 +365,7 @@ export declare class EdgePrintClient {
|
|
|
284
365
|
* connect-on-demand helper with {@link EdgePrintClient.isConnected} rather
|
|
285
366
|
* than reconnecting unconditionally.
|
|
286
367
|
*
|
|
287
|
-
* @throws {
|
|
368
|
+
* @throws {EdgePrintError} If the agent is unreachable or the token is rejected after
|
|
288
369
|
* all retries are exhausted.
|
|
289
370
|
*
|
|
290
371
|
* @example
|
|
@@ -302,7 +383,7 @@ export declare class EdgePrintClient {
|
|
|
302
383
|
/**
|
|
303
384
|
* Return all printers available on the agent machine.
|
|
304
385
|
*
|
|
305
|
-
* @throws {
|
|
386
|
+
* @throws {EdgePrintError} If not connected.
|
|
306
387
|
*
|
|
307
388
|
* @example
|
|
308
389
|
* ```ts
|
|
@@ -317,7 +398,7 @@ export declare class EdgePrintClient {
|
|
|
317
398
|
* Cheaper than calling {@link printers} when you only need the default name
|
|
318
399
|
* and no other printer metadata.
|
|
319
400
|
*
|
|
320
|
-
* @throws {
|
|
401
|
+
* @throws {EdgePrintError} If not connected.
|
|
321
402
|
*/
|
|
322
403
|
defaultPrinter(): Promise<string>;
|
|
323
404
|
/**
|
|
@@ -325,10 +406,13 @@ export declare class EdgePrintClient {
|
|
|
325
406
|
*
|
|
326
407
|
* @param config - Printer selection and job settings.
|
|
327
408
|
* @param data - One or more content items to print (pages, labels, …).
|
|
328
|
-
* @returns
|
|
409
|
+
* @returns A {@link PrintResult} — the agent's `jobId`, and any `warnings`
|
|
410
|
+
* it recorded about a job it printed anyway. `warnings` is empty in the
|
|
411
|
+
* common case.
|
|
329
412
|
*
|
|
330
|
-
* @throws {
|
|
331
|
-
* job fails on the way to the spooler.
|
|
413
|
+
* @throws {EdgePrintError} If not connected, if the agent rejects the job, or
|
|
414
|
+
* if the job fails on the way to the spooler. Branch on `err.code`, never
|
|
415
|
+
* on the message text.
|
|
332
416
|
*
|
|
333
417
|
* A resolved promise means the job was handed to the OS print spooler — not
|
|
334
418
|
* that paper came out. A printer that is offline, jammed or out of paper
|
|
@@ -338,24 +422,31 @@ export declare class EdgePrintClient {
|
|
|
338
422
|
* the client's 30 s timeout rejects while the agent may still be spooling it.
|
|
339
423
|
* Do not resubmit a print automatically on rejection.
|
|
340
424
|
*
|
|
341
|
-
*
|
|
342
|
-
* carries a `jobId`
|
|
343
|
-
* useful when surfacing a failure someone has to chase
|
|
425
|
+
* A job rejected by validation is still recorded, so the thrown error
|
|
426
|
+
* usually carries a `jobId` matching the entry in the agent's job history —
|
|
427
|
+
* useful when surfacing a failure someone has to chase. A request refused
|
|
428
|
+
* before a job existed at all, such as one the agent could not parse, has no
|
|
429
|
+
* `jobId` to carry.
|
|
430
|
+
*
|
|
431
|
+
* A rejection never carries warnings, even where the agent had accumulated
|
|
432
|
+
* some before the rule that failed: they are dropped deliberately rather
|
|
433
|
+
* than reported alongside an unrelated failure.
|
|
344
434
|
*
|
|
345
435
|
* ```ts
|
|
346
436
|
* try {
|
|
347
437
|
* await ep.print(config, data)
|
|
348
438
|
* } catch (err) {
|
|
349
|
-
*
|
|
439
|
+
* if (err instanceof EdgePrintError) console.error(err.code, err.jobId)
|
|
350
440
|
* }
|
|
351
441
|
* ```
|
|
352
442
|
*
|
|
353
443
|
* @example Print a PDF
|
|
354
444
|
* ```ts
|
|
355
|
-
* const jobId = await ep.print(
|
|
445
|
+
* const { jobId, warnings } = await ep.print(
|
|
356
446
|
* { printer: 'Office Laser', copies: 2, duplex: 'long-edge' },
|
|
357
447
|
* [{ type: 'pixel', format: 'pdf', flavor: 'base64', data: pdfBase64 }],
|
|
358
448
|
* )
|
|
449
|
+
* for (const w of warnings) console.warn(w.message)
|
|
359
450
|
* ```
|
|
360
451
|
*
|
|
361
452
|
* @example Print a ZPL label
|
|
@@ -366,7 +457,7 @@ export declare class EdgePrintClient {
|
|
|
366
457
|
* )
|
|
367
458
|
* ```
|
|
368
459
|
*/
|
|
369
|
-
print(config: PrintConfig, data: PrintData[]): Promise<
|
|
460
|
+
print(config: PrintConfig, data: PrintData[]): Promise<PrintResult>;
|
|
370
461
|
/**
|
|
371
462
|
* Close the WebSocket connection and reset client state.
|
|
372
463
|
*
|
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
|
}
|