@holochain/client 0.20.4-rc.0 → 0.20.5

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.
@@ -23,6 +23,7 @@ export declare class WsClient extends Emittery {
23
23
  private pendingRequests;
24
24
  private index;
25
25
  private authenticationToken;
26
+ private reconnectPromise;
26
27
  constructor(socket: IsoWebSocket, url?: URL, options?: WsClientOptions);
27
28
  /**
28
29
  * Instance factory for creating WsClients.
@@ -53,8 +54,21 @@ export declare class WsClient extends Emittery {
53
54
  /**
54
55
  * Send requests to the connected websocket.
55
56
  *
57
+ * If the underlying socket is closed when this method is called, the
58
+ * client transparently reconnects and re-authenticates using the cached
59
+ * token. Transient reconnect errors are surfaced as a `ConnectionError`
60
+ * and the cached token is retained, so a subsequent call can retry the
61
+ * reconnect.
62
+ *
63
+ * If the conductor rejects the cached token during the reconnect
64
+ * (signalled by an immediate close after the `authenticate` handshake),
65
+ * the cached token is cleared and the call rejects with an
66
+ * `InvalidTokenError`. The consumer must rebuild the `AppWebsocket`
67
+ * with a fresh token; further calls on this client will reject with
68
+ * `WebsocketClosedError`.
69
+ *
56
70
  * @param request - The request to send over the websocket.
57
- * @returns
71
+ * @returns The decoded response payload.
58
72
  */
59
73
  request<Response>(request: unknown): Promise<Response>;
60
74
  private exchange;
package/lib/api/client.js CHANGED
@@ -19,6 +19,7 @@ export class WsClient extends Emittery {
19
19
  pendingRequests;
20
20
  index;
21
21
  authenticationToken;
22
+ reconnectPromise;
22
23
  constructor(socket, url, options) {
23
24
  super();
24
25
  this.registerMessageListener(socket);
@@ -100,8 +101,21 @@ export class WsClient extends Emittery {
100
101
  /**
101
102
  * Send requests to the connected websocket.
102
103
  *
104
+ * If the underlying socket is closed when this method is called, the
105
+ * client transparently reconnects and re-authenticates using the cached
106
+ * token. Transient reconnect errors are surfaced as a `ConnectionError`
107
+ * and the cached token is retained, so a subsequent call can retry the
108
+ * reconnect.
109
+ *
110
+ * If the conductor rejects the cached token during the reconnect
111
+ * (signalled by an immediate close after the `authenticate` handshake),
112
+ * the cached token is cleared and the call rejects with an
113
+ * `InvalidTokenError`. The consumer must rebuild the `AppWebsocket`
114
+ * with a fresh token; further calls on this client will reject with
115
+ * `WebsocketClosedError`.
116
+ *
103
117
  * @param request - The request to send over the websocket.
104
- * @returns
118
+ * @returns The decoded response payload.
105
119
  */
106
120
  async request(request) {
107
121
  return this.exchange(request, this.sendMessage.bind(this));
@@ -114,7 +128,15 @@ export class WsClient extends Emittery {
114
128
  return promise;
115
129
  }
116
130
  else if (this.url && this.authenticationToken) {
117
- await this.reconnectWebsocket(this.url, this.authenticationToken);
131
+ // Dedupe concurrent reconnect attempts. The first caller into this
132
+ // branch starts a single reconnect; further callers await the same
133
+ // promise so we never create multiple sockets in parallel.
134
+ if (!this.reconnectPromise) {
135
+ this.reconnectPromise = this.reconnectWebsocket(this.url, this.authenticationToken).finally(() => {
136
+ this.reconnectPromise = undefined;
137
+ });
138
+ }
139
+ await this.reconnectPromise;
118
140
  this.registerMessageListener(this.socket);
119
141
  this.registerCloseListener(this.socket);
120
142
  const promise = new Promise((resolve, reject) => sendHandler(request, resolve, reject));
@@ -204,19 +226,28 @@ export class WsClient extends Emittery {
204
226
  async reconnectWebsocket(url, token) {
205
227
  return new Promise((resolve, reject) => {
206
228
  this.socket = new IsoWebSocket(url, this.options);
229
+ // Track whether the "open" event has fired. The invalidTokenCloseListener
230
+ // must only clear the token when the connection did open and the conductor
231
+ // then closed it quickly.
232
+ let openFired = false;
207
233
  // This error event never occurs in tests. Could be removed?
208
234
  this.socket.addEventListener("error", (errorEvent) => {
209
- this.authenticationToken = undefined;
210
235
  reject(new HolochainError("ConnectionError", `could not connect to Holochain Conductor API at ${url} - ${errorEvent.message}`));
211
236
  }, { once: true });
212
237
  const invalidTokenCloseListener = (closeEvent) => {
213
- this.authenticationToken = undefined;
214
- reject(new HolochainError("InvalidTokenError", `could not connect to ${this.url} due to an invalid app authentication token - close code ${closeEvent.code}`));
238
+ if (openFired) {
239
+ this.authenticationToken = undefined;
240
+ reject(new HolochainError("InvalidTokenError", `could not connect to ${this.url} due to an invalid app authentication token - close code ${closeEvent.code}`));
241
+ }
242
+ else {
243
+ reject(new HolochainError("ConnectionError", `could not connect to Holochain Conductor API at ${url} - close code ${closeEvent.code}`));
244
+ }
215
245
  };
216
246
  this.socket.addEventListener("close", invalidTokenCloseListener, {
217
247
  once: true,
218
248
  });
219
249
  this.socket.addEventListener("open", async () => {
250
+ openFired = true;
220
251
  const encodedMsg = encode({
221
252
  type: "authenticate",
222
253
  data: encode({ token }),
@@ -1,10 +1,21 @@
1
- import { type KeyPair } from "libsodium-wrappers";
2
1
  import type { CapSecret } from "../hdk/capabilities.js";
3
2
  import type { AgentPubKey, CellId } from "../types.js";
4
3
  /**
5
4
  * @public
6
5
  */
7
6
  export type Nonce256Bit = Uint8Array;
7
+ /**
8
+ * @public
9
+ */
10
+ export declare const _kp: () => {
11
+ publicKey: Uint8Array;
12
+ privateKey: Uint8Array;
13
+ keyType: string;
14
+ };
15
+ /**
16
+ * @public
17
+ */
18
+ export type KeyPair = ReturnType<typeof _kp>;
8
19
  /**
9
20
  * @public
10
21
  */
@@ -1,5 +1,9 @@
1
1
  import _sodium from "libsodium-wrappers";
2
2
  import { encodeHashToBase64 } from "../utils/base64.js";
3
+ /**
4
+ * @public
5
+ */
6
+ export const _kp = () => _sodium.crypto_sign_keypair();
3
7
  const signingCredentials = new Map();
4
8
  /**
5
9
  * Get credentials for signing zome calls.
@@ -1,4 +1,4 @@
1
- import { AgentPubKey, DhtOpHash, Timestamp } from "../types";
1
+ import { AgentPubKey, DhtOpHash, Timestamp } from "../types.js";
2
2
  /**
3
3
  * @public
4
4
  */
@@ -5,7 +5,7 @@
5
5
  "toolPackages": [
6
6
  {
7
7
  "packageName": "@microsoft/api-extractor",
8
- "packageVersion": "7.55.1"
8
+ "packageVersion": "7.58.7"
9
9
  }
10
10
  ]
11
11
  }
@@ -122,7 +122,7 @@ export declare class HoloHashMap<K extends HoloHash, V> implements Map<K, V> {
122
122
  * @param thisArg - Optional value to use as 'this' when executing the callback
123
123
  */
124
124
  forEach(callbackfn: (value: V, key: K, map: Map<K, V>) => void, thisArg?: any): void;
125
- [Symbol.iterator](): IterableIterator<[K, V]>;
125
+ [Symbol.iterator](): MapIterator<[K, V]>;
126
126
  get [Symbol.toStringTag](): string;
127
127
  /**
128
128
  * The number of entries in the map.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holochain/client",
3
- "version": "0.20.4-rc.0",
3
+ "version": "0.20.5",
4
4
  "description": "A JavaScript client for the Holochain Conductor API",
5
5
  "author": "Holochain Foundation <info@holochain.org> (https://holochain.org)",
6
6
  "license": "CAL-1.0",
@@ -48,7 +48,7 @@
48
48
  "isomorphic-ws": "^5.0.0",
49
49
  "js-base64": "^3.7.8",
50
50
  "js-sha512": "^0.9.0",
51
- "libsodium-wrappers": "^0.8",
51
+ "libsodium-wrappers": "^0.8.4",
52
52
  "lodash-es": "^4.17.21",
53
53
  "ws": "^8.18.3"
54
54
  },