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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -80,6 +80,11 @@ asked to trust an "Unknown client".
80
80
 
81
81
  Throws if the agent is unreachable or the token is rejected after all retries.
82
82
 
83
+ The client sends its wire protocol version automatically as part of the auth
84
+ handshake; it is not a `connect()` option. If the agent speaks a different
85
+ version, `connect()` throws a message naming both versions and which side to
86
+ update — see [`protocolVersion`](#protocolversion).
87
+
83
88
  Calling `connect()` on an already-connected client **replaces** the connection —
84
89
  the existing socket is torn down before the new one is attempted, and its
85
90
  in-flight requests reject. If the new attempt then fails there is no connection
@@ -117,6 +122,15 @@ Each `PrinterInfo` object includes:
117
122
  | `orientations` | `string[]?` | Supported orientations (e.g. `["portrait", "landscape"]`) |
118
123
  | `virtualPrinter` | `boolean?` | `true` for Developer (virtual) printers injected by the agent |
119
124
 
125
+ 1.x agents sent `is_default`, `share_name`, `copies_max` and `virtual_printer`.
126
+ Those spellings are still accepted on read and mapped onto the camelCase fields
127
+ above, so nothing downstream sees the old names. **Removed in 3.0.**
128
+
129
+ This is not a promise that a 1.x agent works: `connect()` refuses a protocol-1
130
+ agent before `printers()` is ever reached. It is there so that a 2.x agent which
131
+ regresses a field name degrades to a missing value rather than a silent
132
+ `undefined`.
133
+
120
134
  `TrayInfo`:
121
135
 
122
136
  | Field | Type | Description |
@@ -238,6 +252,41 @@ if (!ep.isConnected()) {
238
252
 
239
253
  ---
240
254
 
255
+ ### `agentVersion`
256
+
257
+ Read-only. Version of the agent this client is connected to, as the agent
258
+ reported it during `connect()`.
259
+
260
+ ```ts
261
+ console.log(`Edge Printing agent ${ep.agentVersion}`) // "2.0.7"
262
+ ```
263
+
264
+ `undefined` until a connection is authenticated, and again once it closes —
265
+ including the gap during a reconnect, where the old socket is torn down before
266
+ the new one authenticates. It is also `undefined` on a live connection if the
267
+ agent did not report a version, so treat it as optional even when
268
+ `isConnected()` is `true`. Use it for diagnostics and support output, not to
269
+ branch behaviour on.
270
+
271
+ ---
272
+
273
+ ### `protocolVersion`
274
+
275
+ Read-only. Wire protocol version the connected agent speaks.
276
+
277
+ ```ts
278
+ console.log(ep.protocolVersion) // 2
279
+ ```
280
+
281
+ `undefined` until a connection is authenticated, and again once it closes. When
282
+ it is set it always equals this client's own protocol version — `connect()`
283
+ refuses any other — so it is a diagnostic rather than a capability check.
284
+
285
+ The protocol version is independent of the package version: 2.0 and 2.4 of this
286
+ library both speak protocol `2`.
287
+
288
+ ---
289
+
241
290
  ### `onClose(fn)`
242
291
 
243
292
  Registers a callback that fires whenever the connection closes — network drop, agent restart, or an explicit `disconnect()` call. Multiple listeners are supported.
@@ -334,6 +383,22 @@ If you see a security warning, the certificate is not yet trusted.
334
383
 
335
384
  The token was rejected. Generate a new one from the Edge Printing settings window.
336
385
 
386
+ ### `connect()` throws "Protocol version mismatch"
387
+
388
+ The agent and this library speak different wire protocol versions. The message
389
+ names both and says which side to update:
390
+
391
+ - **"Update the Edge Printing agent"** — the agent is older than this library.
392
+ Install a newer agent build on the user's machine.
393
+ - **"Update the Edge Printing client library"** — the agent is newer. Upgrade
394
+ `@gohcltech/edge-print-client` in the web app.
395
+
396
+ An agent older than 2.0 has no handshake at all and reports as protocol `1`.
397
+
398
+ A mismatch is deterministic, so `connect()`'s retries cannot clear it — it still
399
+ exhausts them (about 3 seconds by default) before throwing. Pass `retries: 0`
400
+ when probing for a usable agent. One side has to be upgraded.
401
+
337
402
  ### Requests time out after 30 seconds
338
403
 
339
404
  The agent accepted the connection but stopped responding. Restart the agent. If
@@ -268,6 +268,8 @@ export declare class EdgePrintClient {
268
268
  private established;
269
269
  private authenticated;
270
270
  private closeListeners;
271
+ private agentVersionValue;
272
+ private protocolVersionValue;
271
273
  /**
272
274
  * Open a WebSocket connection to the Edge Printing agent and authenticate.
273
275
  *
@@ -379,6 +381,31 @@ export declare class EdgePrintClient {
379
381
  * connection is established.
380
382
  */
381
383
  isConnected(): boolean;
384
+ /**
385
+ * Version of the Edge Printing agent this client is connected to, as the
386
+ * agent reported it during {@link connect}.
387
+ *
388
+ * `undefined` until a connection is authenticated, and again once it closes —
389
+ * including for the gap during a reconnect, where the old socket is torn down
390
+ * before the new one authenticates. Also `undefined` on a live connection if
391
+ * the agent reported no version, so it stays optional even when
392
+ * {@link isConnected} is `true`. Read it for diagnostics and support output,
393
+ * not to branch behaviour on: the agent's *protocol* version, which
394
+ * {@link protocolVersion} reports, is what determines what the wire supports.
395
+ */
396
+ get agentVersion(): string | undefined;
397
+ /**
398
+ * Wire protocol version the connected agent speaks.
399
+ *
400
+ * `undefined` until a connection is authenticated, and again once it closes.
401
+ * When it is set it always equals this client's own protocol version —
402
+ * {@link connect} refuses any other — so it is a diagnostic, not a
403
+ * capability check.
404
+ *
405
+ * This client sends its protocol version automatically; it is not a
406
+ * {@link ConnectOptions} field.
407
+ */
408
+ get protocolVersion(): number | undefined;
382
409
  /**
383
410
  * Register a callback invoked whenever the connection closes — whether from
384
411
  * a network drop, an agent restart, or an explicit {@link disconnect} call.
@@ -431,6 +458,16 @@ export declare class EdgePrintClient {
431
458
  private openSocket;
432
459
  private handleMessage;
433
460
  private request;
461
+ /**
462
+ * Clears the state that belongs to one socket rather than to the client.
463
+ *
464
+ * Invariant: when {@link isConnected} is `false`, both version getters read
465
+ * `undefined`. Two paths tear a socket down — {@link discardSocket} and
466
+ * `openSocket`'s `onclose`, which does *not* route through it — and both must
467
+ * call this. Clearing in only one leaves a client whose agent restarted still
468
+ * reporting a version for a connection that is gone.
469
+ */
470
+ private clearSessionState;
434
471
  private rejectPending;
435
472
  }
436
473
  /**
@@ -16,6 +16,55 @@
16
16
  * )
17
17
  * ```
18
18
  */
19
+ /**
20
+ * The wire protocol this client speaks.
21
+ *
22
+ * Deliberately independent of the package version and hand-maintained: 2.0 and
23
+ * 2.4 of this library both speak protocol 2, and a patch release must not read
24
+ * as a protocol change. Bump it only when the wire format itself changes, in
25
+ * lockstep with the agent's `PROTOCOL_VERSION`.
26
+ */
27
+ const PROTOCOL_VERSION = 2;
28
+ /**
29
+ * An `auth_ok` from an agent that predates the handshake carries no version.
30
+ * Such an agent is, by definition, speaking protocol 1.
31
+ */
32
+ const LEGACY_PROTOCOL_VERSION = 1;
33
+ /**
34
+ * Maps a printer object off the wire onto {@link PrinterInfo}.
35
+ *
36
+ * Belt-and-braces, not a promise of 1.x agent support: the version handshake in
37
+ * {@link EdgePrintClient.connect} refuses a protocol-1 agent outright, so a real
38
+ * 1.x agent never reaches this function. It exists so that a *2.x* agent which
39
+ * regresses a field name — or a future decision to tolerate older agents —
40
+ * degrades to a missing value rather than a silent `undefined` in a UI. Do not
41
+ * read support for old agents into it and relax the handshake.
42
+ *
43
+ * Deprecated on arrival: the snake_case half is removed in 3.0.
44
+ */
45
+ function normalisePrinter(p) {
46
+ // Destructured out rather than spread over, so the result carries no
47
+ // snake_case keys. `{ ...p, isDefault }` would leave `is_default` sitting on
48
+ // an object whose declared type says it cannot be there.
49
+ const { is_default, share_name, copies_max, virtual_printer, ...rest } = p;
50
+ const shareName = p.shareName ?? share_name;
51
+ const copiesMax = p.copiesMax ?? copies_max;
52
+ const virtualPrinter = p.virtualPrinter ?? virtual_printer;
53
+ return {
54
+ ...rest,
55
+ // Required and a boolean, so it gets a default — `undefined` reaching a
56
+ // template renders "default: undefined".
57
+ isDefault: p.isDefault ?? is_default ?? false,
58
+ // Spread conditionally, not assigned. `shareName: undefined` still creates
59
+ // an own key, so a plain assignment would make `'virtualPrinter' in p` true
60
+ // for every printer — and a caller uses that to tell "the agent did not
61
+ // report this" from "reported false". The agent omits absent optionals, so
62
+ // this keeps the shim's output the same shape as an unshimmed payload.
63
+ ...(shareName !== undefined && { shareName }),
64
+ ...(copiesMax !== undefined && { copiesMax }),
65
+ ...(virtualPrinter !== undefined && { virtualPrinter }),
66
+ };
67
+ }
19
68
  /**
20
69
  * WebSocket client for the Edge Printing agent.
21
70
  *
@@ -103,9 +152,44 @@ export class EdgePrintClient {
103
152
  }
104
153
  try {
105
154
  await this.openSocket(`wss://${host}:${port}`);
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 });
155
+ // `clientName` is omitted entirely when unset, rather than sent as
156
+ // undefined/null — the agent treats a missing key as "no name given".
157
+ // Built as one object rather than a ternary over two literals, so
158
+ // `protocolVersion` cannot end up on only one of the branches.
159
+ const auth = {
160
+ token,
161
+ protocolVersion: PROTOCOL_VERSION,
162
+ ...(clientName ? { clientName } : {}),
163
+ };
164
+ const ack = await this.request('auth', auth);
165
+ // An agent that predates the handshake answers with a bare `auth_ok`.
166
+ // A non-integer version folds to the same place: `"2"` is a client bug
167
+ // we want reported, not coerced into a match.
168
+ const agentProtocol = Number.isInteger(ack['protocolVersion'])
169
+ ? ack['protocolVersion']
170
+ : LEGACY_PROTOCOL_VERSION;
171
+ if (agentProtocol !== PROTOCOL_VERSION) {
172
+ throw new Error(`Protocol version mismatch: this client speaks protocol version ` +
173
+ `${PROTOCOL_VERSION}, but the agent speaks ${agentProtocol}. ` +
174
+ (agentProtocol < PROTOCOL_VERSION
175
+ ? 'Update the Edge Printing agent.'
176
+ : 'Update the Edge Printing client library.'));
177
+ }
178
+ // Re-checked after the await: `request` suspends, and a newer connect
179
+ // (or a `disconnect`) can land while this one is waiting on the agent.
180
+ // Without this the superseded attempt goes on to publish its agent's
181
+ // versions and mark itself established over whatever replaced it — the
182
+ // same check the loop makes before each attempt and in the catch, which
183
+ // between them cover every path except this one.
184
+ if (this.connectGeneration !== generation) {
185
+ throw new Error('Connection superseded');
186
+ }
187
+ // Assigned together so the getters and `isConnected()` agree: either
188
+ // this attempt owns the connection and publishes all of it, or none.
189
+ this.agentVersionValue = typeof ack['agentVersion'] === 'string'
190
+ ? ack['agentVersion']
191
+ : undefined;
192
+ this.protocolVersionValue = agentProtocol;
109
193
  this.authenticated = true;
110
194
  this.established = true;
111
195
  return;
@@ -142,7 +226,7 @@ export class EdgePrintClient {
142
226
  */
143
227
  async printers() {
144
228
  const resp = await this.request('get_printers', {});
145
- return resp.printers;
229
+ return (resp.printers ?? []).map(normalisePrinter);
146
230
  }
147
231
  /**
148
232
  * Return the name of the OS default printer.
@@ -233,6 +317,35 @@ export class EdgePrintClient {
233
317
  isConnected() {
234
318
  return this.ws?.readyState === WebSocket.OPEN && this.authenticated;
235
319
  }
320
+ /**
321
+ * Version of the Edge Printing agent this client is connected to, as the
322
+ * agent reported it during {@link connect}.
323
+ *
324
+ * `undefined` until a connection is authenticated, and again once it closes —
325
+ * including for the gap during a reconnect, where the old socket is torn down
326
+ * before the new one authenticates. Also `undefined` on a live connection if
327
+ * the agent reported no version, so it stays optional even when
328
+ * {@link isConnected} is `true`. Read it for diagnostics and support output,
329
+ * not to branch behaviour on: the agent's *protocol* version, which
330
+ * {@link protocolVersion} reports, is what determines what the wire supports.
331
+ */
332
+ get agentVersion() {
333
+ return this.agentVersionValue;
334
+ }
335
+ /**
336
+ * Wire protocol version the connected agent speaks.
337
+ *
338
+ * `undefined` until a connection is authenticated, and again once it closes.
339
+ * When it is set it always equals this client's own protocol version —
340
+ * {@link connect} refuses any other — so it is a diagnostic, not a
341
+ * capability check.
342
+ *
343
+ * This client sends its protocol version automatically; it is not a
344
+ * {@link ConnectOptions} field.
345
+ */
346
+ get protocolVersion() {
347
+ return this.protocolVersionValue;
348
+ }
236
349
  /**
237
350
  * Register a callback invoked whenever the connection closes — whether from
238
351
  * a network drop, an agent restart, or an explicit {@link disconnect} call.
@@ -299,7 +412,7 @@ export class EdgePrintClient {
299
412
  const settleOpen = this.pendingOpen;
300
413
  this.ws = null;
301
414
  this.pendingOpen = null;
302
- this.authenticated = false;
415
+ this.clearSessionState();
303
416
  if (ws) {
304
417
  ws.onopen = null;
305
418
  ws.onmessage = null;
@@ -337,7 +450,7 @@ export class EdgePrintClient {
337
450
  settled();
338
451
  openWaiting?.(new Error(`Cannot reach Edge Printing agent at ${url}`));
339
452
  this.ws = null;
340
- this.authenticated = false;
453
+ this.clearSessionState();
341
454
  this.rejectPending(new Error('Connection closed'));
342
455
  this.markClosed();
343
456
  };
@@ -412,6 +525,20 @@ export class EdgePrintClient {
412
525
  }
413
526
  });
414
527
  }
528
+ /**
529
+ * Clears the state that belongs to one socket rather than to the client.
530
+ *
531
+ * Invariant: when {@link isConnected} is `false`, both version getters read
532
+ * `undefined`. Two paths tear a socket down — {@link discardSocket} and
533
+ * `openSocket`'s `onclose`, which does *not* route through it — and both must
534
+ * call this. Clearing in only one leaves a client whose agent restarted still
535
+ * reporting a version for a connection that is gone.
536
+ */
537
+ clearSessionState() {
538
+ this.authenticated = false;
539
+ this.agentVersionValue = undefined;
540
+ this.protocolVersionValue = undefined;
541
+ }
415
542
  rejectPending(err) {
416
543
  for (const { reject } of this.pending.values())
417
544
  reject(err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gohcltech/edge-print-client",
3
- "version": "2.0.50-develop",
3
+ "version": "2.0.54-develop",
4
4
  "description": "Browser client for the Edge Printing WebSocket agent",
5
5
  "license": "MIT",
6
6
  "repository": {