@gohcltech/edge-print-client 2.0.31-develop → 2.0.42-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/LICENSE +21 -0
- package/README.md +50 -6
- package/dist/edge-print.d.ts +88 -3
- package/dist/edge-print.js +203 -22
- package/package.json +17 -7
- package/src/edge-print.ts +0 -484
- package/tsconfig.json +0 -13
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Health Care Logistics, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -56,11 +56,12 @@ Opens a WebSocket connection to the agent and authenticates. Retries automatical
|
|
|
56
56
|
|
|
57
57
|
```ts
|
|
58
58
|
await ep.connect({
|
|
59
|
-
host: '127.0.0.1',
|
|
60
|
-
port: 8181,
|
|
61
|
-
token: 'abc123',
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
host: '127.0.0.1', // default
|
|
60
|
+
port: 8181, // default
|
|
61
|
+
token: 'abc123', // required — from the agent settings window
|
|
62
|
+
clientName: 'Warehouse', // recommended — shown in the agent's approval prompt
|
|
63
|
+
retries: 3, // default — additional attempts after the first failure
|
|
64
|
+
retryDelay: 1000, // default — ms between retries
|
|
64
65
|
})
|
|
65
66
|
```
|
|
66
67
|
|
|
@@ -69,11 +70,22 @@ await ep.connect({
|
|
|
69
70
|
| `host` | `string` | `'127.0.0.1'` | Hostname or IP of the agent machine |
|
|
70
71
|
| `port` | `number` | `8181` | Port the agent listens on |
|
|
71
72
|
| `token` | `string` | _(required)_ | API token from the Edge Printing settings window |
|
|
73
|
+
| `clientName` | `string?` | _(none)_ | Name shown in the agent's Clients tab and approval prompt |
|
|
72
74
|
| `retries` | `number` | `3` | Max additional connection attempts after the first failure |
|
|
73
75
|
| `retryDelay` | `number` | `1000` | Ms to wait between retry attempts |
|
|
74
76
|
|
|
77
|
+
Pass `clientName` unless you have a reason not to. Connecting with an unapproved
|
|
78
|
+
token prompts the user to approve the application by name; without it they are
|
|
79
|
+
asked to trust an "Unknown client".
|
|
80
|
+
|
|
75
81
|
Throws if the agent is unreachable or the token is rejected after all retries.
|
|
76
82
|
|
|
83
|
+
Calling `connect()` on an already-connected client **replaces** the connection —
|
|
84
|
+
the existing socket is torn down before the new one is attempted, and its
|
|
85
|
+
in-flight requests reject. If the new attempt then fails there is no connection
|
|
86
|
+
left. Guard a connect-on-demand helper with `isConnected()` rather than
|
|
87
|
+
reconnecting unconditionally.
|
|
88
|
+
|
|
77
89
|
---
|
|
78
90
|
|
|
79
91
|
### `printers()`
|
|
@@ -132,6 +144,29 @@ Submits a print job. Returns the job ID assigned by the agent.
|
|
|
132
144
|
const jobId = await ep.print(config, data)
|
|
133
145
|
```
|
|
134
146
|
|
|
147
|
+
Rejects if the job is refused, and also if it fails on the way to the spooler —
|
|
148
|
+
where it previously resolved with a job id regardless. Always handle the
|
|
149
|
+
rejection:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
try {
|
|
153
|
+
const jobId = await ep.print(config, data)
|
|
154
|
+
} catch (err) {
|
|
155
|
+
// printer not found, decode failure, spooler error …
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Two limits worth knowing before you build on this:
|
|
160
|
+
|
|
161
|
+
- **Resolved means spooled, not printed.** The agent resolves once the OS print
|
|
162
|
+
spooler accepts the job. A printer that is offline, jammed, or out of paper
|
|
163
|
+
after that point still resolves, so keep whatever confirmation step your
|
|
164
|
+
workflow needs.
|
|
165
|
+
- **Rejected does not prove nothing printed.** Requests time out after 30
|
|
166
|
+
seconds, and a large PDF can take longer than that to spool — the rejection
|
|
167
|
+
arrives while the agent is still working. Never resubmit a print
|
|
168
|
+
automatically on rejection; surface it and let a person decide.
|
|
169
|
+
|
|
135
170
|
**`config`** — `PrintConfig`
|
|
136
171
|
|
|
137
172
|
| Field | Type | Description |
|
|
@@ -207,13 +242,22 @@ if (!ep.isConnected()) {
|
|
|
207
242
|
|
|
208
243
|
Registers a callback that fires whenever the connection closes — network drop, agent restart, or an explicit `disconnect()` call. Multiple listeners are supported.
|
|
209
244
|
|
|
245
|
+
Returns a function that removes the listener:
|
|
246
|
+
|
|
210
247
|
```ts
|
|
211
|
-
ep.onClose(() => {
|
|
248
|
+
const stop = ep.onClose(() => {
|
|
212
249
|
console.warn('Connection lost — reconnecting…')
|
|
213
250
|
reconnect()
|
|
214
251
|
})
|
|
252
|
+
|
|
253
|
+
// later, e.g. when the component unmounts
|
|
254
|
+
stop()
|
|
215
255
|
```
|
|
216
256
|
|
|
257
|
+
Register once for the life of the client where you can. Registering inside a
|
|
258
|
+
reconnect path without unsubscribing stacks a duplicate listener on every
|
|
259
|
+
attempt, and they all fire on the next close.
|
|
260
|
+
|
|
217
261
|
---
|
|
218
262
|
|
|
219
263
|
## Using a custom instance
|
package/dist/edge-print.d.ts
CHANGED
|
@@ -209,6 +209,14 @@ export interface ConnectOptions {
|
|
|
209
209
|
* any other requests are accepted.
|
|
210
210
|
*/
|
|
211
211
|
token: string;
|
|
212
|
+
/**
|
|
213
|
+
* Name shown for this application in the agent's Clients tab and in the
|
|
214
|
+
* approval prompt the user sees when connecting with an unapproved token.
|
|
215
|
+
*
|
|
216
|
+
* Strongly recommended: without it the user is asked to trust an
|
|
217
|
+
* "Unknown client", which is not much of a decision.
|
|
218
|
+
*/
|
|
219
|
+
clientName?: string;
|
|
212
220
|
/**
|
|
213
221
|
* Maximum number of additional connection attempts after the first failure.
|
|
214
222
|
* @default 3
|
|
@@ -243,6 +251,21 @@ export interface ConnectOptions {
|
|
|
243
251
|
export declare class EdgePrintClient {
|
|
244
252
|
private ws;
|
|
245
253
|
private pending;
|
|
254
|
+
/** Rejects the connect currently waiting on `onopen`, if there is one. */
|
|
255
|
+
private pendingOpen;
|
|
256
|
+
/** Bumped per connect, so a superseded one can tell it is no longer current. */
|
|
257
|
+
private connectGeneration;
|
|
258
|
+
/**
|
|
259
|
+
* Whether a connection currently exists from the application's point of view:
|
|
260
|
+
* a socket that opened *and* authenticated, and has not since gone away.
|
|
261
|
+
*
|
|
262
|
+
* Distinct from `authenticated`, which tracks one socket. This tracks whether
|
|
263
|
+
* there is anything for `onClose` to report the loss of, and is the single
|
|
264
|
+
* rule every teardown path consults — the alternative was each path deciding
|
|
265
|
+
* for itself, which is how the same event came to be announced in one place
|
|
266
|
+
* and swallowed in another.
|
|
267
|
+
*/
|
|
268
|
+
private established;
|
|
246
269
|
private authenticated;
|
|
247
270
|
private closeListeners;
|
|
248
271
|
/**
|
|
@@ -252,6 +275,13 @@ export declare class EdgePrintClient {
|
|
|
252
275
|
* waiting `options.retryDelay` ms (default 1 000) between attempts. If all
|
|
253
276
|
* attempts fail the last error is re-thrown.
|
|
254
277
|
*
|
|
278
|
+
* Calling this on an already-connected client **replaces** the connection:
|
|
279
|
+
* the existing socket is torn down before the new one is attempted, and its
|
|
280
|
+
* in-flight requests reject. If the new attempt then fails there is no
|
|
281
|
+
* connection left, and close listeners are notified. Guard a
|
|
282
|
+
* connect-on-demand helper with {@link EdgePrintClient.isConnected} rather
|
|
283
|
+
* than reconnecting unconditionally.
|
|
284
|
+
*
|
|
255
285
|
* @throws {Error} If the agent is unreachable or the token is rejected after
|
|
256
286
|
* all retries are exhausted.
|
|
257
287
|
*
|
|
@@ -295,7 +325,28 @@ export declare class EdgePrintClient {
|
|
|
295
325
|
* @param data - One or more content items to print (pages, labels, …).
|
|
296
326
|
* @returns The job ID assigned by the agent.
|
|
297
327
|
*
|
|
298
|
-
* @throws {Error} If not connected,
|
|
328
|
+
* @throws {Error} If not connected, if the agent rejects the job, or if the
|
|
329
|
+
* job fails on the way to the spooler.
|
|
330
|
+
*
|
|
331
|
+
* A resolved promise means the job was handed to the OS print spooler — not
|
|
332
|
+
* that paper came out. A printer that is offline, jammed or out of paper
|
|
333
|
+
* after the spooler accepts the job still resolves.
|
|
334
|
+
*
|
|
335
|
+
* A rejection is also not proof that nothing printed: a request that exceeds
|
|
336
|
+
* the client's 30 s timeout rejects while the agent may still be spooling it.
|
|
337
|
+
* Do not resubmit a print automatically on rejection.
|
|
338
|
+
*
|
|
339
|
+
* When the agent had already created a job before it failed, the thrown error
|
|
340
|
+
* carries a `jobId` property matching the entry in the agent's job history —
|
|
341
|
+
* useful when surfacing a failure someone has to chase:
|
|
342
|
+
*
|
|
343
|
+
* ```ts
|
|
344
|
+
* try {
|
|
345
|
+
* await ep.print(config, data)
|
|
346
|
+
* } catch (err) {
|
|
347
|
+
* const jobId = (err as Error & { jobId?: string }).jobId
|
|
348
|
+
* }
|
|
349
|
+
* ```
|
|
299
350
|
*
|
|
300
351
|
* @example Print a PDF
|
|
301
352
|
* ```ts
|
|
@@ -334,15 +385,49 @@ export declare class EdgePrintClient {
|
|
|
334
385
|
*
|
|
335
386
|
* Multiple listeners can be registered; all are called in registration order.
|
|
336
387
|
*
|
|
388
|
+
* @returns A function that removes this listener. Registering inside a
|
|
389
|
+
* reconnect path without unsubscribing stacks a duplicate listener on every
|
|
390
|
+
* attempt, so hold onto this if the caller can register more than once.
|
|
391
|
+
*
|
|
337
392
|
* @example
|
|
338
393
|
* ```ts
|
|
339
|
-
* ep.onClose(() => {
|
|
394
|
+
* const stop = ep.onClose(() => {
|
|
340
395
|
* console.warn('Lost connection to Edge Printing agent — reconnecting…')
|
|
341
396
|
* reconnect()
|
|
342
397
|
* })
|
|
398
|
+
*
|
|
399
|
+
* // later, e.g. when the component unmounts
|
|
400
|
+
* stop()
|
|
343
401
|
* ```
|
|
344
402
|
*/
|
|
345
|
-
onClose(fn: () => void): void;
|
|
403
|
+
onClose(fn: () => void): () => void;
|
|
404
|
+
/**
|
|
405
|
+
* Announces the loss of an established connection, exactly once.
|
|
406
|
+
*
|
|
407
|
+
* A socket that never authenticated was never a connection, so its going
|
|
408
|
+
* away is not something `onClose` reports — announcing it would have an
|
|
409
|
+
* onClose-driven reconnect racing the retry loop already running.
|
|
410
|
+
*/
|
|
411
|
+
private markClosed;
|
|
412
|
+
/**
|
|
413
|
+
* Tears down the current socket so it cannot reach this client again.
|
|
414
|
+
*
|
|
415
|
+
* Handlers are detached before closing, because the close event arrives in a
|
|
416
|
+
* later task — by which time the socket may have been replaced, and its
|
|
417
|
+
* `onclose` would otherwise report the *replacement* as disconnected.
|
|
418
|
+
*
|
|
419
|
+
* Detaching means the teardown that handler would have done has to happen
|
|
420
|
+
* here instead: clearing `authenticated`, settling in-flight requests, and
|
|
421
|
+
* settling a connect still waiting on `onopen`. That last one is easy to
|
|
422
|
+
* miss — a connect whose handlers are removed before either fires has
|
|
423
|
+
* nothing left to settle it, and `request()`'s timeout does not cover it, so
|
|
424
|
+
* it would wait forever.
|
|
425
|
+
*
|
|
426
|
+
* Close listeners are not fired from here. Whether a caller hears about a
|
|
427
|
+
* lost connection is `markClosed`'s decision, because only it knows whether
|
|
428
|
+
* there was an established connection to lose.
|
|
429
|
+
*/
|
|
430
|
+
private discardSocket;
|
|
346
431
|
private openSocket;
|
|
347
432
|
private handleMessage;
|
|
348
433
|
private request;
|
package/dist/edge-print.js
CHANGED
|
@@ -40,6 +40,21 @@ export class EdgePrintClient {
|
|
|
40
40
|
constructor() {
|
|
41
41
|
this.ws = null;
|
|
42
42
|
this.pending = new Map();
|
|
43
|
+
/** Rejects the connect currently waiting on `onopen`, if there is one. */
|
|
44
|
+
this.pendingOpen = null;
|
|
45
|
+
/** Bumped per connect, so a superseded one can tell it is no longer current. */
|
|
46
|
+
this.connectGeneration = 0;
|
|
47
|
+
/**
|
|
48
|
+
* Whether a connection currently exists from the application's point of view:
|
|
49
|
+
* a socket that opened *and* authenticated, and has not since gone away.
|
|
50
|
+
*
|
|
51
|
+
* Distinct from `authenticated`, which tracks one socket. This tracks whether
|
|
52
|
+
* there is anything for `onClose` to report the loss of, and is the single
|
|
53
|
+
* rule every teardown path consults — the alternative was each path deciding
|
|
54
|
+
* for itself, which is how the same event came to be announced in one place
|
|
55
|
+
* and swallowed in another.
|
|
56
|
+
*/
|
|
57
|
+
this.established = false;
|
|
43
58
|
this.authenticated = false;
|
|
44
59
|
this.closeListeners = [];
|
|
45
60
|
}
|
|
@@ -50,6 +65,13 @@ export class EdgePrintClient {
|
|
|
50
65
|
* waiting `options.retryDelay` ms (default 1 000) between attempts. If all
|
|
51
66
|
* attempts fail the last error is re-thrown.
|
|
52
67
|
*
|
|
68
|
+
* Calling this on an already-connected client **replaces** the connection:
|
|
69
|
+
* the existing socket is torn down before the new one is attempted, and its
|
|
70
|
+
* in-flight requests reject. If the new attempt then fails there is no
|
|
71
|
+
* connection left, and close listeners are notified. Guard a
|
|
72
|
+
* connect-on-demand helper with {@link EdgePrintClient.isConnected} rather
|
|
73
|
+
* than reconnecting unconditionally.
|
|
74
|
+
*
|
|
53
75
|
* @throws {Error} If the agent is unreachable or the token is rejected after
|
|
54
76
|
* all retries are exhausted.
|
|
55
77
|
*
|
|
@@ -65,17 +87,44 @@ export class EdgePrintClient {
|
|
|
65
87
|
* ```
|
|
66
88
|
*/
|
|
67
89
|
async connect(options) {
|
|
68
|
-
const { host = '127.0.0.1', port = 8181, token, retries = 3, retryDelay = 1000, } = options;
|
|
69
|
-
|
|
90
|
+
const { host = '127.0.0.1', port = 8181, token, clientName, retries = 3, retryDelay = 1000, } = options;
|
|
91
|
+
// A negative count would skip the loop altogether and resolve without ever
|
|
92
|
+
// opening a socket, leaving the caller believing it is connected while
|
|
93
|
+
// every later call rejects with "Not connected".
|
|
94
|
+
const attempts = Math.max(0, retries);
|
|
95
|
+
const generation = ++this.connectGeneration;
|
|
96
|
+
for (let attempt = 0; attempt <= attempts; attempt++) {
|
|
97
|
+
// Checked before every attempt, not only after a failed one. A connect
|
|
98
|
+
// sleeping between retries would otherwise wake and open a socket —
|
|
99
|
+
// discarding whatever connection was established while it slept, and
|
|
100
|
+
// reconnecting after an explicit disconnect.
|
|
101
|
+
if (this.connectGeneration !== generation) {
|
|
102
|
+
throw new Error('Connection superseded');
|
|
103
|
+
}
|
|
70
104
|
try {
|
|
71
105
|
await this.openSocket(`wss://${host}:${port}`);
|
|
72
|
-
|
|
106
|
+
// Omitted entirely when unset, rather than sent as undefined/null —
|
|
107
|
+
// the agent treats a missing key as "no name given".
|
|
108
|
+
await this.request('auth', clientName ? { token, clientName } : { token });
|
|
73
109
|
this.authenticated = true;
|
|
110
|
+
this.established = true;
|
|
74
111
|
return;
|
|
75
112
|
}
|
|
76
113
|
catch (err) {
|
|
77
|
-
|
|
114
|
+
// A newer connect has taken over since this attempt began. Its socket
|
|
115
|
+
// is not this attempt's to tear down.
|
|
116
|
+
if (this.connectGeneration !== generation)
|
|
117
|
+
throw err;
|
|
118
|
+
this.discardSocket(new Error('Connection closed'));
|
|
119
|
+
// Every failure retries, including a refused token. The agent answers a
|
|
120
|
+
// token still awaiting approval with the same "Invalid token" it gives
|
|
121
|
+
// a bad one, so the client cannot tell them apart — and retrying while
|
|
122
|
+
// someone clicks Approve is the documented flow. Repeat attempts only
|
|
123
|
+
// bump a counter on the pending entry; the notification fires once.
|
|
124
|
+
if (attempt === attempts) {
|
|
125
|
+
this.markClosed();
|
|
78
126
|
throw err;
|
|
127
|
+
}
|
|
79
128
|
await sleep(retryDelay);
|
|
80
129
|
}
|
|
81
130
|
}
|
|
@@ -114,7 +163,28 @@ export class EdgePrintClient {
|
|
|
114
163
|
* @param data - One or more content items to print (pages, labels, …).
|
|
115
164
|
* @returns The job ID assigned by the agent.
|
|
116
165
|
*
|
|
117
|
-
* @throws {Error} If not connected,
|
|
166
|
+
* @throws {Error} If not connected, if the agent rejects the job, or if the
|
|
167
|
+
* job fails on the way to the spooler.
|
|
168
|
+
*
|
|
169
|
+
* A resolved promise means the job was handed to the OS print spooler — not
|
|
170
|
+
* that paper came out. A printer that is offline, jammed or out of paper
|
|
171
|
+
* after the spooler accepts the job still resolves.
|
|
172
|
+
*
|
|
173
|
+
* A rejection is also not proof that nothing printed: a request that exceeds
|
|
174
|
+
* the client's 30 s timeout rejects while the agent may still be spooling it.
|
|
175
|
+
* Do not resubmit a print automatically on rejection.
|
|
176
|
+
*
|
|
177
|
+
* When the agent had already created a job before it failed, the thrown error
|
|
178
|
+
* carries a `jobId` property matching the entry in the agent's job history —
|
|
179
|
+
* useful when surfacing a failure someone has to chase:
|
|
180
|
+
*
|
|
181
|
+
* ```ts
|
|
182
|
+
* try {
|
|
183
|
+
* await ep.print(config, data)
|
|
184
|
+
* } catch (err) {
|
|
185
|
+
* const jobId = (err as Error & { jobId?: string }).jobId
|
|
186
|
+
* }
|
|
187
|
+
* ```
|
|
118
188
|
*
|
|
119
189
|
* @example Print a PDF
|
|
120
190
|
* ```ts
|
|
@@ -143,9 +213,16 @@ export class EdgePrintClient {
|
|
|
143
213
|
* disconnected.
|
|
144
214
|
*/
|
|
145
215
|
disconnect() {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
216
|
+
// Routed through the same teardown as a replacement: nulling the reference
|
|
217
|
+
// alone leaves the socket's handlers attached, and its close then arrives
|
|
218
|
+
// later and tears down whatever connection has taken its place.
|
|
219
|
+
// Invalidates any connect still running, so a retry cannot wake up after
|
|
220
|
+
// this and quietly reconnect.
|
|
221
|
+
this.connectGeneration++;
|
|
222
|
+
this.discardSocket(new Error('Disconnected'));
|
|
223
|
+
// Detaching the handlers means the socket's own close never arrives, so
|
|
224
|
+
// without this the loss would go unannounced.
|
|
225
|
+
this.markClosed();
|
|
149
226
|
}
|
|
150
227
|
/**
|
|
151
228
|
* `true` when the WebSocket is open and the session is authenticated.
|
|
@@ -162,29 +239,107 @@ export class EdgePrintClient {
|
|
|
162
239
|
*
|
|
163
240
|
* Multiple listeners can be registered; all are called in registration order.
|
|
164
241
|
*
|
|
242
|
+
* @returns A function that removes this listener. Registering inside a
|
|
243
|
+
* reconnect path without unsubscribing stacks a duplicate listener on every
|
|
244
|
+
* attempt, so hold onto this if the caller can register more than once.
|
|
245
|
+
*
|
|
165
246
|
* @example
|
|
166
247
|
* ```ts
|
|
167
|
-
* ep.onClose(() => {
|
|
248
|
+
* const stop = ep.onClose(() => {
|
|
168
249
|
* console.warn('Lost connection to Edge Printing agent — reconnecting…')
|
|
169
250
|
* reconnect()
|
|
170
251
|
* })
|
|
252
|
+
*
|
|
253
|
+
* // later, e.g. when the component unmounts
|
|
254
|
+
* stop()
|
|
171
255
|
* ```
|
|
172
256
|
*/
|
|
173
257
|
onClose(fn) {
|
|
174
258
|
this.closeListeners.push(fn);
|
|
259
|
+
return () => {
|
|
260
|
+
const i = this.closeListeners.indexOf(fn);
|
|
261
|
+
if (i !== -1)
|
|
262
|
+
this.closeListeners.splice(i, 1);
|
|
263
|
+
};
|
|
175
264
|
}
|
|
176
265
|
// ── internals ────────────────────────────────────────────────────────────
|
|
266
|
+
/**
|
|
267
|
+
* Announces the loss of an established connection, exactly once.
|
|
268
|
+
*
|
|
269
|
+
* A socket that never authenticated was never a connection, so its going
|
|
270
|
+
* away is not something `onClose` reports — announcing it would have an
|
|
271
|
+
* onClose-driven reconnect racing the retry loop already running.
|
|
272
|
+
*/
|
|
273
|
+
markClosed() {
|
|
274
|
+
if (!this.established)
|
|
275
|
+
return;
|
|
276
|
+
this.established = false;
|
|
277
|
+
[...this.closeListeners].forEach(fn => fn());
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Tears down the current socket so it cannot reach this client again.
|
|
281
|
+
*
|
|
282
|
+
* Handlers are detached before closing, because the close event arrives in a
|
|
283
|
+
* later task — by which time the socket may have been replaced, and its
|
|
284
|
+
* `onclose` would otherwise report the *replacement* as disconnected.
|
|
285
|
+
*
|
|
286
|
+
* Detaching means the teardown that handler would have done has to happen
|
|
287
|
+
* here instead: clearing `authenticated`, settling in-flight requests, and
|
|
288
|
+
* settling a connect still waiting on `onopen`. That last one is easy to
|
|
289
|
+
* miss — a connect whose handlers are removed before either fires has
|
|
290
|
+
* nothing left to settle it, and `request()`'s timeout does not cover it, so
|
|
291
|
+
* it would wait forever.
|
|
292
|
+
*
|
|
293
|
+
* Close listeners are not fired from here. Whether a caller hears about a
|
|
294
|
+
* lost connection is `markClosed`'s decision, because only it knows whether
|
|
295
|
+
* there was an established connection to lose.
|
|
296
|
+
*/
|
|
297
|
+
discardSocket(reason) {
|
|
298
|
+
const ws = this.ws;
|
|
299
|
+
const settleOpen = this.pendingOpen;
|
|
300
|
+
this.ws = null;
|
|
301
|
+
this.pendingOpen = null;
|
|
302
|
+
this.authenticated = false;
|
|
303
|
+
if (ws) {
|
|
304
|
+
ws.onopen = null;
|
|
305
|
+
ws.onmessage = null;
|
|
306
|
+
ws.onclose = null;
|
|
307
|
+
ws.onerror = null;
|
|
308
|
+
try {
|
|
309
|
+
ws.close();
|
|
310
|
+
}
|
|
311
|
+
catch { /* already closing or closed */ }
|
|
312
|
+
}
|
|
313
|
+
settleOpen?.(reason);
|
|
314
|
+
this.rejectPending(reason);
|
|
315
|
+
}
|
|
177
316
|
openSocket(url) {
|
|
178
317
|
return new Promise((resolve, reject) => {
|
|
318
|
+
// Never hold two sockets. This covers a retry, a reconnect on a live
|
|
319
|
+
// client, and a second connect racing the first.
|
|
320
|
+
this.discardSocket(new Error('Connection superseded'));
|
|
179
321
|
const ws = new WebSocket(url);
|
|
180
|
-
|
|
181
|
-
|
|
322
|
+
this.ws = ws;
|
|
323
|
+
this.pendingOpen = reject;
|
|
324
|
+
const settled = () => { this.pendingOpen = null; };
|
|
325
|
+
ws.onopen = () => { settled(); resolve(); };
|
|
326
|
+
ws.onerror = () => {
|
|
327
|
+
settled();
|
|
328
|
+
reject(new Error(`Cannot reach Edge Printing agent at ${url}`));
|
|
329
|
+
};
|
|
182
330
|
ws.onmessage = (ev) => this.handleMessage(String(ev.data));
|
|
183
331
|
ws.onclose = () => {
|
|
332
|
+
// A socket can close during the handshake, before either onopen or
|
|
333
|
+
// onerror fires. Clearing the pending open without settling it would
|
|
334
|
+
// leave this connect waiting forever — request()'s timeout does not
|
|
335
|
+
// cover the open.
|
|
336
|
+
const openWaiting = this.pendingOpen;
|
|
337
|
+
settled();
|
|
338
|
+
openWaiting?.(new Error(`Cannot reach Edge Printing agent at ${url}`));
|
|
184
339
|
this.ws = null;
|
|
185
340
|
this.authenticated = false;
|
|
186
341
|
this.rejectPending(new Error('Connection closed'));
|
|
187
|
-
this.
|
|
342
|
+
this.markClosed();
|
|
188
343
|
};
|
|
189
344
|
});
|
|
190
345
|
}
|
|
@@ -201,8 +356,24 @@ export class EdgePrintClient {
|
|
|
201
356
|
return;
|
|
202
357
|
const { resolve, reject } = this.pending.get(id);
|
|
203
358
|
this.pending.delete(id);
|
|
204
|
-
|
|
205
|
-
|
|
359
|
+
// `error` means the request never became a job. `print_error` means a job
|
|
360
|
+
// was created and then failed — it carries a real jobId, which is why
|
|
361
|
+
// matching only on `error` let failed prints resolve as successes.
|
|
362
|
+
// Matched by suffix so error types added later reject by default rather
|
|
363
|
+
// than silently resolving.
|
|
364
|
+
const type = String(msg['type'] ?? '');
|
|
365
|
+
if (type === 'error' || type.endsWith('_error')) {
|
|
366
|
+
const failure = new Error(msg['message'] ?? 'Unknown error');
|
|
367
|
+
// `print_error` carries the id of the job the agent created and then
|
|
368
|
+
// failed, which is the handle a caller needs to find it in the agent's
|
|
369
|
+
// job history. Attached rather than given an exported error type: the
|
|
370
|
+
// typed client error that formalises this arrives later, and a second
|
|
371
|
+
// error shape now would only have to be reconciled with it.
|
|
372
|
+
const jobId = msg['jobId'];
|
|
373
|
+
if (typeof jobId === 'string' && jobId.length > 0) {
|
|
374
|
+
Object.assign(failure, { jobId });
|
|
375
|
+
}
|
|
376
|
+
reject(failure);
|
|
206
377
|
}
|
|
207
378
|
else {
|
|
208
379
|
resolve(msg);
|
|
@@ -215,20 +386,30 @@ export class EdgePrintClient {
|
|
|
215
386
|
return;
|
|
216
387
|
}
|
|
217
388
|
const id = crypto.randomUUID();
|
|
218
|
-
this.pending.set(id, { resolve, reject });
|
|
219
389
|
const timeout = setTimeout(() => {
|
|
220
|
-
if (this.pending.
|
|
221
|
-
this.pending.delete(id);
|
|
390
|
+
if (this.pending.delete(id)) {
|
|
222
391
|
reject(new Error(`Request "${type}" timed out`));
|
|
223
392
|
}
|
|
224
393
|
}, 30000);
|
|
225
|
-
|
|
226
|
-
//
|
|
227
|
-
|
|
394
|
+
// Registered once, already wrapped. The previous version inserted the
|
|
395
|
+
// raw handlers, sent, then replaced the entry with wrapped ones that
|
|
396
|
+
// closed over what it read back out of the map. A close in that window
|
|
397
|
+
// made rejectPending clear the map first, so the replacement re-inserted
|
|
398
|
+
// an entry whose captured handlers were undefined — poisoning the map for
|
|
399
|
+
// the next rejectPending, which then threw mid-loop and left every later
|
|
400
|
+
// request unsettled.
|
|
228
401
|
this.pending.set(id, {
|
|
229
|
-
resolve: (v) => { clearTimeout(timeout);
|
|
230
|
-
reject: (e) => { clearTimeout(timeout);
|
|
402
|
+
resolve: (v) => { clearTimeout(timeout); resolve(v); },
|
|
403
|
+
reject: (e) => { clearTimeout(timeout); reject(e); },
|
|
231
404
|
});
|
|
405
|
+
try {
|
|
406
|
+
this.ws.send(JSON.stringify({ type, id, ...payload }));
|
|
407
|
+
}
|
|
408
|
+
catch (err) {
|
|
409
|
+
clearTimeout(timeout);
|
|
410
|
+
this.pending.delete(id);
|
|
411
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
412
|
+
}
|
|
232
413
|
});
|
|
233
414
|
}
|
|
234
415
|
rejectPending(err) {
|
package/package.json
CHANGED
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gohcltech/edge-print-client",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.42-develop",
|
|
4
4
|
"description": "Browser client for the Edge Printing WebSocket agent",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://bitbucket.org/gohcl/edge-printing.git",
|
|
9
|
+
"directory": "client"
|
|
10
|
+
},
|
|
5
11
|
"type": "module",
|
|
6
|
-
"main": "./dist/edge-print.cjs",
|
|
7
12
|
"module": "./dist/edge-print.js",
|
|
8
13
|
"types": "./dist/edge-print.d.ts",
|
|
9
14
|
"exports": {
|
|
10
15
|
".": {
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"types": "./dist/edge-print.d.ts"
|
|
16
|
+
"types": "./dist/edge-print.d.ts",
|
|
17
|
+
"import": "./dist/edge-print.js"
|
|
14
18
|
}
|
|
15
19
|
},
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
16
24
|
"scripts": {
|
|
17
|
-
"build": "tsc"
|
|
25
|
+
"build": "tsc",
|
|
26
|
+
"test": "tsc -p tsconfig.test.json && vitest run"
|
|
18
27
|
},
|
|
19
28
|
"devDependencies": {
|
|
20
|
-
"typescript": "^5.5.0"
|
|
29
|
+
"typescript": "^5.5.0",
|
|
30
|
+
"vitest": "^3.0.0"
|
|
21
31
|
}
|
|
22
32
|
}
|
package/src/edge-print.ts
DELETED
|
@@ -1,484 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module edge-print
|
|
3
|
-
*
|
|
4
|
-
* Browser-side WebSocket client for the Edge Printing agent.
|
|
5
|
-
* Drop-in replacement for qz-tray.js with a simpler token-based auth model.
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* ```ts
|
|
9
|
-
* import ep from '@gohcltech/edge-print-client'
|
|
10
|
-
*
|
|
11
|
-
* await ep.connect({ token: 'your-api-token' })
|
|
12
|
-
* const printers = await ep.printers()
|
|
13
|
-
* const jobId = await ep.print(
|
|
14
|
-
* { printer: 'Office Laser' },
|
|
15
|
-
* [{ type: 'pixel', format: 'pdf', flavor: 'base64', data: pdfBase64 }],
|
|
16
|
-
* )
|
|
17
|
-
* ```
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Identifies the target printer.
|
|
22
|
-
*
|
|
23
|
-
* - Pass a `string` to use a printer by its OS name (e.g. `"Office Laser"`).
|
|
24
|
-
* - Pass `{ host, port }` to route to a network printer by address.
|
|
25
|
-
*/
|
|
26
|
-
export type PrinterTarget = string | { host: string; port: number }
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Configuration options for a print job.
|
|
30
|
-
*
|
|
31
|
-
* All fields except `printer` are optional; omitted fields use the printer's
|
|
32
|
-
* own defaults.
|
|
33
|
-
*/
|
|
34
|
-
export interface PrintConfig {
|
|
35
|
-
/** Target printer — OS name string or remote `{ host, port }` object. */
|
|
36
|
-
printer: PrinterTarget
|
|
37
|
-
/** Number of copies to print (default: 1). */
|
|
38
|
-
copies?: number
|
|
39
|
-
/**
|
|
40
|
-
* Duplex (double-sided) mode.
|
|
41
|
-
* - `'none'` — single-sided
|
|
42
|
-
* - `'long-edge'` — flip on the long edge (standard book binding)
|
|
43
|
-
* - `'short-edge'` — flip on the short edge (calendar binding)
|
|
44
|
-
*/
|
|
45
|
-
duplex?: 'none' | 'long-edge' | 'short-edge'
|
|
46
|
-
/** Page orientation (default: determined by document content). */
|
|
47
|
-
orientation?: 'portrait' | 'landscape'
|
|
48
|
-
/** Human-readable name shown in the OS print queue. */
|
|
49
|
-
jobName?: string
|
|
50
|
-
/**
|
|
51
|
-
* Color output mode.
|
|
52
|
-
* - `'color'` — full color
|
|
53
|
-
* - `'grayscale'` — color converted to gray
|
|
54
|
-
* - `'black-white'` — pure monochrome (fastest on mono printers)
|
|
55
|
-
*/
|
|
56
|
-
colorType?: 'color' | 'grayscale' | 'black-white'
|
|
57
|
-
/**
|
|
58
|
-
* Paper size, exactly as returned by {@link PrinterInfo.papers}
|
|
59
|
-
* (e.g. `"Letter"`, `"A4"`). Omit to use the printer's current default.
|
|
60
|
-
*/
|
|
61
|
-
paperSize?: string
|
|
62
|
-
/**
|
|
63
|
-
* Input tray, exactly as returned by {@link TrayInfo.name} inside
|
|
64
|
-
* {@link PrinterInfo.trays} (e.g. `"Tray 1"`, `"Auto"`).
|
|
65
|
-
* Omit to use the printer's current default.
|
|
66
|
-
*/
|
|
67
|
-
tray?: string
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* A single unit of content to be printed.
|
|
72
|
-
*
|
|
73
|
-
* Pass an array of `PrintData` to {@link EdgePrintClient.print} — each item
|
|
74
|
-
* maps to one page or label in the job.
|
|
75
|
-
*
|
|
76
|
-
* @example PDF from a base64 string
|
|
77
|
-
* ```ts
|
|
78
|
-
* const page: PrintData = {
|
|
79
|
-
* type: 'pixel',
|
|
80
|
-
* format: 'pdf',
|
|
81
|
-
* flavor: 'base64',
|
|
82
|
-
* data: '<base64-encoded PDF>',
|
|
83
|
-
* }
|
|
84
|
-
* ```
|
|
85
|
-
*
|
|
86
|
-
* @example ZPL label (raw command)
|
|
87
|
-
* ```ts
|
|
88
|
-
* const label: PrintData = {
|
|
89
|
-
* type: 'raw',
|
|
90
|
-
* format: 'command',
|
|
91
|
-
* flavor: 'plain',
|
|
92
|
-
* data: '^XA^FO50,50^ADN,36,20^FDHello^FS^XZ',
|
|
93
|
-
* }
|
|
94
|
-
* ```
|
|
95
|
-
*/
|
|
96
|
-
export interface PrintData {
|
|
97
|
-
/**
|
|
98
|
-
* Rendering pipeline.
|
|
99
|
-
* - `'raw'` — bytes are forwarded to the printer verbatim (ESC/POS, ZPL, EPL, …)
|
|
100
|
-
* - `'pixel'` — the agent rasterises the content before printing (PDF, HTML, image)
|
|
101
|
-
*/
|
|
102
|
-
type: 'raw' | 'pixel'
|
|
103
|
-
/**
|
|
104
|
-
* Content format.
|
|
105
|
-
* - `'command'` — raw printer command language
|
|
106
|
-
* - `'pdf'` — Portable Document Format
|
|
107
|
-
* - `'html'` — HTML markup (agent renders to pixels)
|
|
108
|
-
* - `'image'` — raster image (PNG, JPEG, …)
|
|
109
|
-
*/
|
|
110
|
-
format: 'command' | 'pdf' | 'html' | 'image'
|
|
111
|
-
/**
|
|
112
|
-
* How `data` is encoded.
|
|
113
|
-
* - `'plain'` — UTF-8 text
|
|
114
|
-
* - `'base64'` — Base64-encoded binary
|
|
115
|
-
* - `'hex'` — hex-encoded binary
|
|
116
|
-
* - `'file'` — absolute file path on the agent machine (agent reads the file)
|
|
117
|
-
*/
|
|
118
|
-
flavor: 'plain' | 'base64' | 'hex' | 'file'
|
|
119
|
-
/** The content, encoded according to `flavor`. */
|
|
120
|
-
data: string
|
|
121
|
-
/** Format-specific extra options forwarded to the agent renderer. */
|
|
122
|
-
options?: Record<string, unknown>
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* An input tray/bin reported by the printer driver.
|
|
127
|
-
*
|
|
128
|
-
* Returned inside {@link PrinterInfo.trays}.
|
|
129
|
-
*/
|
|
130
|
-
export interface TrayInfo {
|
|
131
|
-
/** Tray name exactly as the driver reports it (e.g. `"Tray 1"`, `"Auto"`). */
|
|
132
|
-
name: string
|
|
133
|
-
/**
|
|
134
|
-
* Paper sizes this tray can hold, when the driver reports per-tray
|
|
135
|
-
* capability. Empty when the driver does not distinguish by tray —
|
|
136
|
-
* fall back to {@link PrinterInfo.papers} in that case.
|
|
137
|
-
*/
|
|
138
|
-
papers?: string[]
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Metadata about a printer available on the agent machine.
|
|
143
|
-
*
|
|
144
|
-
* Returned by {@link EdgePrintClient.printers}.
|
|
145
|
-
* Use {@link name} as the value for {@link PrintConfig.printer}.
|
|
146
|
-
*/
|
|
147
|
-
export interface PrinterInfo {
|
|
148
|
-
/** OS-assigned printer name. Pass this to {@link PrintConfig.printer}. */
|
|
149
|
-
name: string
|
|
150
|
-
/** `true` if this is the system default printer. */
|
|
151
|
-
isDefault: boolean
|
|
152
|
-
/**
|
|
153
|
-
* Current printer status reported by the OS.
|
|
154
|
-
*
|
|
155
|
-
* Possible values: `"idle"` | `"printing"` | `"offline"` | `"error"` |
|
|
156
|
-
* `"paused"` | `"paper_jam"` | `"paper_out"` | `"disabled"` | `"unknown"`
|
|
157
|
-
*/
|
|
158
|
-
status: string
|
|
159
|
-
/** Driver name as reported by the OS (present when available). */
|
|
160
|
-
driver?: string
|
|
161
|
-
/** Port or URI the printer is connected on (e.g. `"USB001"`, `"ipp://…"`). */
|
|
162
|
-
port?: string
|
|
163
|
-
/** Physical location string set in the printer properties. */
|
|
164
|
-
location?: string
|
|
165
|
-
/** Freeform comment from the printer properties. */
|
|
166
|
-
comment?: string
|
|
167
|
-
/** Windows share name, if the printer is shared on the network. */
|
|
168
|
-
shareName?: string
|
|
169
|
-
/** `true` if the printer supports color output. */
|
|
170
|
-
color?: boolean
|
|
171
|
-
/** `true` if the printer supports duplex (double-sided) printing. */
|
|
172
|
-
duplex?: boolean
|
|
173
|
-
/**
|
|
174
|
-
* Input trays available on this printer.
|
|
175
|
-
* Pass a {@link TrayInfo.name} value to {@link PrintConfig.tray}.
|
|
176
|
-
*/
|
|
177
|
-
trays?: TrayInfo[]
|
|
178
|
-
/**
|
|
179
|
-
* All paper sizes the printer supports, regardless of tray.
|
|
180
|
-
* Pass one of these values to {@link PrintConfig.paperSize}.
|
|
181
|
-
*/
|
|
182
|
-
papers?: string[]
|
|
183
|
-
/** Maximum number of copies the driver accepts in a single job. */
|
|
184
|
-
copiesMax?: number
|
|
185
|
-
/** Page orientations the driver supports (e.g. `["portrait", "landscape"]`). */
|
|
186
|
-
orientations?: string[]
|
|
187
|
-
/**
|
|
188
|
-
* `true` for Developer (virtual) printers — injected by the agent when
|
|
189
|
-
* Developer Printers mode is enabled in Settings. These printers capture
|
|
190
|
-
* jobs to disk or render a ZPL preview rather than sending to physical hardware.
|
|
191
|
-
*/
|
|
192
|
-
virtualPrinter?: boolean
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Options passed to {@link EdgePrintClient.connect}.
|
|
197
|
-
*/
|
|
198
|
-
export interface ConnectOptions {
|
|
199
|
-
/**
|
|
200
|
-
* Hostname or IP of the machine running the Edge Printing agent.
|
|
201
|
-
* @default '127.0.0.1'
|
|
202
|
-
*/
|
|
203
|
-
host?: string
|
|
204
|
-
/**
|
|
205
|
-
* Port the agent is listening on.
|
|
206
|
-
* @default 8181
|
|
207
|
-
*/
|
|
208
|
-
port?: number
|
|
209
|
-
/**
|
|
210
|
-
* API token shown in the Edge Printing settings window.
|
|
211
|
-
* Every WebSocket session must authenticate with this token before
|
|
212
|
-
* any other requests are accepted.
|
|
213
|
-
*/
|
|
214
|
-
token: string
|
|
215
|
-
/**
|
|
216
|
-
* Maximum number of additional connection attempts after the first failure.
|
|
217
|
-
* @default 3
|
|
218
|
-
*/
|
|
219
|
-
retries?: number
|
|
220
|
-
/**
|
|
221
|
-
* Milliseconds to wait between retry attempts.
|
|
222
|
-
* @default 1000
|
|
223
|
-
*/
|
|
224
|
-
retryDelay?: number
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
type Pending = { resolve: (v: unknown) => void; reject: (e: Error) => void }
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* WebSocket client for the Edge Printing agent.
|
|
231
|
-
*
|
|
232
|
-
* Each instance manages a single persistent connection. For most applications
|
|
233
|
-
* the exported {@link ep} singleton is sufficient; create additional instances
|
|
234
|
-
* only when you need concurrent connections to different agents.
|
|
235
|
-
*
|
|
236
|
-
* ### Lifecycle
|
|
237
|
-
* ```
|
|
238
|
-
* connect() → printers() / print() / … → disconnect()
|
|
239
|
-
* ```
|
|
240
|
-
*
|
|
241
|
-
* @example
|
|
242
|
-
* ```ts
|
|
243
|
-
* import { EdgePrintClient } from '@gohcltech/edge-print-client'
|
|
244
|
-
*
|
|
245
|
-
* const client = new EdgePrintClient()
|
|
246
|
-
* await client.connect({ token: 'abc123' })
|
|
247
|
-
* ```
|
|
248
|
-
*/
|
|
249
|
-
export class EdgePrintClient {
|
|
250
|
-
private ws: WebSocket | null = null
|
|
251
|
-
private pending = new Map<string, Pending>()
|
|
252
|
-
private authenticated = false
|
|
253
|
-
private closeListeners: Array<() => void> = []
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Open a WebSocket connection to the Edge Printing agent and authenticate.
|
|
257
|
-
*
|
|
258
|
-
* On failure the client retries up to `options.retries` times (default 3),
|
|
259
|
-
* waiting `options.retryDelay` ms (default 1 000) between attempts. If all
|
|
260
|
-
* attempts fail the last error is re-thrown.
|
|
261
|
-
*
|
|
262
|
-
* @throws {Error} If the agent is unreachable or the token is rejected after
|
|
263
|
-
* all retries are exhausted.
|
|
264
|
-
*
|
|
265
|
-
* @example
|
|
266
|
-
* ```ts
|
|
267
|
-
* await ep.connect({
|
|
268
|
-
* host: '127.0.0.1',
|
|
269
|
-
* port: 8181,
|
|
270
|
-
* token: 'abc123',
|
|
271
|
-
* retries: 5,
|
|
272
|
-
* retryDelay: 2000,
|
|
273
|
-
* })
|
|
274
|
-
* ```
|
|
275
|
-
*/
|
|
276
|
-
async connect(options: ConnectOptions): Promise<void> {
|
|
277
|
-
const {
|
|
278
|
-
host = '127.0.0.1',
|
|
279
|
-
port = 8181,
|
|
280
|
-
token,
|
|
281
|
-
retries = 3,
|
|
282
|
-
retryDelay = 1000,
|
|
283
|
-
} = options
|
|
284
|
-
|
|
285
|
-
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
286
|
-
try {
|
|
287
|
-
await this.openSocket(`wss://${host}:${port}`)
|
|
288
|
-
await this.request('auth', { token })
|
|
289
|
-
this.authenticated = true
|
|
290
|
-
return
|
|
291
|
-
} catch (err) {
|
|
292
|
-
if (attempt === retries) throw err
|
|
293
|
-
await sleep(retryDelay)
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Return all printers available on the agent machine.
|
|
300
|
-
*
|
|
301
|
-
* @throws {Error} If not connected.
|
|
302
|
-
*
|
|
303
|
-
* @example
|
|
304
|
-
* ```ts
|
|
305
|
-
* const printers = await ep.printers()
|
|
306
|
-
* const colorPrinters = printers.filter(p => p.color)
|
|
307
|
-
* ```
|
|
308
|
-
*/
|
|
309
|
-
async printers(): Promise<PrinterInfo[]> {
|
|
310
|
-
const resp = await this.request('get_printers', {}) as { printers: PrinterInfo[] }
|
|
311
|
-
return resp.printers
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Return the name of the OS default printer.
|
|
316
|
-
*
|
|
317
|
-
* Cheaper than calling {@link printers} when you only need the default name
|
|
318
|
-
* and no other printer metadata.
|
|
319
|
-
*
|
|
320
|
-
* @throws {Error} If not connected.
|
|
321
|
-
*/
|
|
322
|
-
async defaultPrinter(): Promise<string> {
|
|
323
|
-
const resp = await this.request('get_default_printer', {}) as { name: string }
|
|
324
|
-
return resp.name
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Submit a print job to the agent.
|
|
329
|
-
*
|
|
330
|
-
* @param config - Printer selection and job settings.
|
|
331
|
-
* @param data - One or more content items to print (pages, labels, …).
|
|
332
|
-
* @returns The job ID assigned by the agent.
|
|
333
|
-
*
|
|
334
|
-
* @throws {Error} If not connected, or if the agent rejects the job.
|
|
335
|
-
*
|
|
336
|
-
* @example Print a PDF
|
|
337
|
-
* ```ts
|
|
338
|
-
* const jobId = await ep.print(
|
|
339
|
-
* { printer: 'Office Laser', copies: 2, duplex: 'long-edge' },
|
|
340
|
-
* [{ type: 'pixel', format: 'pdf', flavor: 'base64', data: pdfBase64 }],
|
|
341
|
-
* )
|
|
342
|
-
* ```
|
|
343
|
-
*
|
|
344
|
-
* @example Print a ZPL label
|
|
345
|
-
* ```ts
|
|
346
|
-
* await ep.print(
|
|
347
|
-
* { printer: 'Zebra ZT410' },
|
|
348
|
-
* [{ type: 'raw', format: 'command', flavor: 'plain', data: zplString }],
|
|
349
|
-
* )
|
|
350
|
-
* ```
|
|
351
|
-
*/
|
|
352
|
-
async print(config: PrintConfig, data: PrintData[]): Promise<string> {
|
|
353
|
-
const resp = await this.request('print', { config, data }) as { jobId: string }
|
|
354
|
-
return resp.jobId
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/**
|
|
358
|
-
* Close the WebSocket connection and reset client state.
|
|
359
|
-
*
|
|
360
|
-
* Any in-flight requests are rejected. Safe to call when already
|
|
361
|
-
* disconnected.
|
|
362
|
-
*/
|
|
363
|
-
disconnect(): void {
|
|
364
|
-
this.ws?.close()
|
|
365
|
-
this.ws = null
|
|
366
|
-
this.authenticated = false
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* `true` when the WebSocket is open and the session is authenticated.
|
|
371
|
-
*
|
|
372
|
-
* Use this to guard print calls in components that may render before the
|
|
373
|
-
* connection is established.
|
|
374
|
-
*/
|
|
375
|
-
isConnected(): boolean {
|
|
376
|
-
return this.ws?.readyState === WebSocket.OPEN && this.authenticated
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
/**
|
|
380
|
-
* Register a callback invoked whenever the connection closes — whether from
|
|
381
|
-
* a network drop, an agent restart, or an explicit {@link disconnect} call.
|
|
382
|
-
*
|
|
383
|
-
* Multiple listeners can be registered; all are called in registration order.
|
|
384
|
-
*
|
|
385
|
-
* @example
|
|
386
|
-
* ```ts
|
|
387
|
-
* ep.onClose(() => {
|
|
388
|
-
* console.warn('Lost connection to Edge Printing agent — reconnecting…')
|
|
389
|
-
* reconnect()
|
|
390
|
-
* })
|
|
391
|
-
* ```
|
|
392
|
-
*/
|
|
393
|
-
onClose(fn: () => void): void {
|
|
394
|
-
this.closeListeners.push(fn)
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// ── internals ────────────────────────────────────────────────────────────
|
|
398
|
-
|
|
399
|
-
private openSocket(url: string): Promise<void> {
|
|
400
|
-
return new Promise((resolve, reject) => {
|
|
401
|
-
const ws = new WebSocket(url)
|
|
402
|
-
ws.onopen = () => { this.ws = ws; resolve() }
|
|
403
|
-
ws.onerror = () => reject(new Error(`Cannot reach Edge Printing agent at ${url}`))
|
|
404
|
-
ws.onmessage = (ev) => this.handleMessage(String(ev.data))
|
|
405
|
-
ws.onclose = () => {
|
|
406
|
-
this.ws = null
|
|
407
|
-
this.authenticated = false
|
|
408
|
-
this.rejectPending(new Error('Connection closed'))
|
|
409
|
-
this.closeListeners.forEach(fn => fn())
|
|
410
|
-
}
|
|
411
|
-
})
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
private handleMessage(raw: string): void {
|
|
415
|
-
let msg: Record<string, unknown>
|
|
416
|
-
try { msg = JSON.parse(raw) } catch { return }
|
|
417
|
-
|
|
418
|
-
const id = msg['id'] as string | undefined
|
|
419
|
-
if (!id || !this.pending.has(id)) return
|
|
420
|
-
|
|
421
|
-
const { resolve, reject } = this.pending.get(id)!
|
|
422
|
-
this.pending.delete(id)
|
|
423
|
-
|
|
424
|
-
if (msg['type'] === 'error') {
|
|
425
|
-
reject(new Error((msg['message'] as string) ?? 'Unknown error'))
|
|
426
|
-
} else {
|
|
427
|
-
resolve(msg)
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
private request(type: string, payload: Record<string, unknown>): Promise<unknown> {
|
|
432
|
-
return new Promise((resolve, reject) => {
|
|
433
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
434
|
-
reject(new Error('Not connected to Edge Printing agent'))
|
|
435
|
-
return
|
|
436
|
-
}
|
|
437
|
-
const id = crypto.randomUUID()
|
|
438
|
-
this.pending.set(id, { resolve, reject })
|
|
439
|
-
|
|
440
|
-
const timeout = setTimeout(() => {
|
|
441
|
-
if (this.pending.has(id)) {
|
|
442
|
-
this.pending.delete(id)
|
|
443
|
-
reject(new Error(`Request "${type}" timed out`))
|
|
444
|
-
}
|
|
445
|
-
}, 30_000)
|
|
446
|
-
|
|
447
|
-
this.ws.send(JSON.stringify({ type, id, ...payload }))
|
|
448
|
-
|
|
449
|
-
// Wrap resolve/reject to also clear the timeout.
|
|
450
|
-
const original = this.pending.get(id)!
|
|
451
|
-
this.pending.set(id, {
|
|
452
|
-
resolve: (v) => { clearTimeout(timeout); original.resolve(v) },
|
|
453
|
-
reject: (e) => { clearTimeout(timeout); original.reject(e) },
|
|
454
|
-
})
|
|
455
|
-
})
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
private rejectPending(err: Error): void {
|
|
459
|
-
for (const { reject } of this.pending.values()) reject(err)
|
|
460
|
-
this.pending.clear()
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
function sleep(ms: number): Promise<void> {
|
|
465
|
-
return new Promise(resolve => setTimeout(resolve, ms))
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
/**
|
|
469
|
-
* Shared singleton `EdgePrintClient` instance.
|
|
470
|
-
*
|
|
471
|
-
* Suitable for most single-page applications. Call {@link EdgePrintClient.connect}
|
|
472
|
-
* once at app startup, then use `ep` from any module without passing the
|
|
473
|
-
* client around.
|
|
474
|
-
*
|
|
475
|
-
* @example
|
|
476
|
-
* ```ts
|
|
477
|
-
* import ep from '@gohcltech/edge-print-client'
|
|
478
|
-
*
|
|
479
|
-
* await ep.connect({ token: 'abc123' })
|
|
480
|
-
* await ep.print({ printer: 'Office Laser' }, [pdfData])
|
|
481
|
-
* ```
|
|
482
|
-
*/
|
|
483
|
-
export const ep = new EdgePrintClient()
|
|
484
|
-
export default ep
|
package/tsconfig.json
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2020",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "bundler",
|
|
6
|
-
"declaration": true,
|
|
7
|
-
"declarationDir": "./dist",
|
|
8
|
-
"outDir": "./dist",
|
|
9
|
-
"strict": true,
|
|
10
|
-
"lib": ["ES2020", "DOM"]
|
|
11
|
-
},
|
|
12
|
-
"include": ["src/**/*.ts"]
|
|
13
|
-
}
|