@tdxvolt/volt-client-grpc 0.16.0-beta.1 → 0.16.0-beta.12

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.
@@ -2,7 +2,6 @@
2
2
  import debug from "debug";
3
3
  import ip from "ip";
4
4
  import bent from "bent";
5
- import forge from "node-forge";
6
5
  import lodash from "lodash";
7
6
  import VoltConnection from "./volt-connection.js";
8
7
  import { join } from "path";
@@ -17,8 +16,12 @@ import { constants } from "./constants.js";
17
16
  import GRPCCall from "./grpc-call.js";
18
17
  import RpcInvocation from "./rpc-invocation.js";
19
18
  import { v4 as generateId } from "uuid";
19
+ import {
20
+ publicKeyToPem,
21
+ removeCarriageReturn,
22
+ signBase64
23
+ } from "@tdxvolt/volt-client-web";
20
24
 
21
- const { pki } = forge;
22
25
  const { filter, pick } = lodash;
23
26
 
24
27
  const log = debug("volt-client-grpc:volt-client-internal");
@@ -54,8 +57,15 @@ function issueAuthenticate(authenticateRequest, ttl) {
54
57
  // This is the signing CA used by the volt.
55
58
  this._credential.cache.ca = authenticateResponse.chain;
56
59
 
57
- // Identity resource id is assigned by the volt.
58
- this._credential.cache.client_id = authenticateResponse.session_id;
60
+ // The session id is assigned by the volt.
61
+ this._credential.cache.session_id = authenticateResponse.session_id;
62
+
63
+ if (!this._credential.cache.identity_did) {
64
+ // Identity DID has been assigned by the volt.
65
+ this._credential.cache.identity_did =
66
+ authenticateResponse.identity_did;
67
+ }
68
+
59
69
  this._credential.saveCache();
60
70
  break;
61
71
  }
@@ -66,64 +76,32 @@ function issueAuthenticate(authenticateRequest, ttl) {
66
76
  case "POLICY_DECISION_PROMPT":
67
77
  case "POLICY_DECISION_PENDING": {
68
78
  log("access pending approval - waiting...");
69
- break;
70
- }
71
- default:
72
- log(
73
- "ignoring unknown bind descision %s",
74
- authenticateResponse.status
75
- );
76
- break;
77
- }
78
- }
79
-
80
- return authenticateResponse.decision;
81
- });
82
- }
83
79
 
84
- function issueBind(bindRequest, ttl) {
85
- // eslint-disable-next-line no-use-before-define
86
- return unaryCallInternal
87
- .call(this, "Bind", bindRequest)
88
- .then((bindResponse) => {
89
- log("got binding request response %j", bindResponse);
90
- if (bindResponse.status?.code) {
91
- log("error in bind response: %s", bindResponse.status.message);
92
- return Promise.reject(new Error(bindResponse.status.message));
93
- } else {
94
- switch (bindResponse.decision) {
95
- case "POLICY_DECISION_PERMIT": {
96
- //
97
- // The request status is permit => cache the bind response info.
98
- //
80
+ // This is the signing CA used by the volt.
81
+ this._credential.cache.ca = authenticateResponse.chain;
99
82
 
100
- // This is the certificate assigned to us by the volt.
101
- this._credential.cache.cert = bindResponse.cert;
83
+ // The session id is assigned by the volt.
84
+ this._credential.cache.session_id = authenticateResponse.session_id;
102
85
 
103
- // This is the signing CA used by the volt.
104
- this._credential.cache.ca = bindResponse.chain;
86
+ if (!this._credential.cache.identity_did) {
87
+ // Identity DID has been assigned by the volt.
88
+ this._credential.cache.identity_did =
89
+ authenticateResponse.identity_did;
90
+ }
105
91
 
106
- // Identity resource id is assigned by the volt.
107
- this._credential.cache.client_id = bindResponse.identity_id;
108
92
  this._credential.saveCache();
109
93
  break;
110
94
  }
111
- case "POLICY_DECISION_DENY": {
112
- log(">>>>>>>>>>>>>>> access request is DENIED <<<<<<<<<<<<<<<<<<<");
113
- break;
114
- }
115
- case "POLICY_DECISION_PROMPT":
116
- case "POLICY_DECISION_PENDING": {
117
- log("access pending approval - waiting...");
118
- break;
119
- }
120
95
  default:
121
- log("ignoring unknown bind descision %s", bindResponse.status);
96
+ log(
97
+ "ignoring unknown bind descision %s",
98
+ authenticateResponse.status
99
+ );
122
100
  break;
123
101
  }
124
102
  }
125
103
 
126
- return bindResponse.decision;
104
+ return authenticateResponse.decision;
127
105
  });
128
106
  }
129
107
 
@@ -136,8 +114,10 @@ function findDIDDocumentService(document, serviceType) {
136
114
  return matches;
137
115
  }
138
116
 
139
-
140
- export async function authenticateInternal() {
117
+ export async function authenticateInternal(
118
+ sessionOnly = false,
119
+ ownDID = false
120
+ ) {
141
121
  try {
142
122
  // Check if we need to resolve a Relay volt.
143
123
  if (
@@ -149,7 +129,7 @@ export async function authenticateInternal() {
149
129
  log("attempting to retrieve Relay information from %s", relayURL);
150
130
  this._voltConfig.relay = await fetchVoltConfig.call(
151
131
  this,
152
- `${relayURL}/discovery`
132
+ `${relayURL}/discover`
153
133
  );
154
134
 
155
135
  if (this._voltConfig.relay.ca_pem) {
@@ -174,7 +154,9 @@ export async function authenticateInternal() {
174
154
  }
175
155
 
176
156
  const keyPair = this._credential.getKey();
177
- const publicKeyPem = pki.publicKeyToPem(keyPair.publicKey);
157
+ const publicKeyPem = removeCarriageReturn(
158
+ publicKeyToPem(keyPair.publicKey)
159
+ );
178
160
 
179
161
  if (!this._credential.cache.bindIp) {
180
162
  // Default the IP address if none is specified. This is used to set the SAN in the
@@ -186,13 +168,12 @@ export async function authenticateInternal() {
186
168
 
187
169
  // Form the request message.
188
170
  const authenticateRequest = {
189
- binding_name: this._config.client_name,
190
- public_key: publicKeyPem,
191
- host: this._credential.cache.bindIp,
171
+ client_name: this._config.client_name,
172
+ host: this._credential.cache.bindIp
192
173
  };
193
174
 
194
175
  if (this._voltConfig.challenge_code) {
195
- authenticateRequest.challenge = voltUtils.signBase64(
176
+ authenticateRequest.challenge = signBase64(
196
177
  keyPair.privateKey,
197
178
  this._voltConfig.challenge_code
198
179
  );
@@ -202,161 +183,61 @@ export async function authenticateInternal() {
202
183
  );
203
184
  }
204
185
 
205
- log("authenticate TTL is ", this._config.bindRequestTTL);
206
-
207
- // This will block while the request is pending.
208
- const pollInterval = 10000;
209
- let decision = "";
210
- do {
211
- if (decision) {
212
- // Wait a while before retrying.
213
- // eslint-disable-next-line no-await-in-loop
214
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
215
- }
216
- // eslint-disable-next-line no-await-in-loop
217
- decision = await issueAuthenticate.call(
218
- this,
219
- authenticateRequest,
220
- this._voltConfig.bindRequestTTL
221
- );
222
- log("authenticate decision: %s", decision);
223
- } while (
224
- decision === "POLICY_DECISION_PROMPT" ||
225
- decision === "POLICY_DECISION_PENDING"
226
- );
227
-
228
- return decision === "POLICY_DECISION_PERMIT";
229
- } catch (err) {
230
- log("failure binding to Volt [%s]", err.message);
231
- throw err;
232
- }
233
- }
234
-
235
- export async function bindInternal() {
236
- try {
237
- // Check if we need to resolve a Relay volt.
238
- if (
239
- typeof this._voltConfig?.relay === "string" ||
240
- this._voltConfig?.relay?.http_address
241
- ) {
242
- const relayURL =
243
- this._voltConfig.relay?.http_address || this._voltConfig.relay;
244
- log("attempting to retrieve Relay information from %s", relayURL);
245
- this._voltConfig.relay = await fetchVoltConfig.call(
246
- this,
247
- `${relayURL}/discovery`
248
- );
249
- if (this._voltConfig.relay.ca_pem) {
250
- log(
251
- "Auto-fetched Relay CA %s, remote address %s",
252
- this._voltConfig.relay.ca_pem,
253
- this._voltConfig.relay.address
254
- );
186
+ if (sessionOnly) {
187
+ // We want to create a session-based authentication based simply on our key, rather than a
188
+ // fully-blown DID-based authentication.
189
+ log("creating session-only request");
190
+ authenticateRequest.public_key = publicKeyPem;
191
+ } else if (!this._credential.cache.identity_did) {
192
+ if (!ownDID) {
193
+ // We don't want to have to manage our own DID, so we ask the Volt to do it for us.
194
+ // The Volt will create a new DID and return it to us in the response.
195
+ log("creating DID-based request");
196
+ authenticateRequest.did_public_key = publicKeyPem;
255
197
  } else {
256
- throw new Error("Unable to fetch Relay CA - cannot securely connect.");
257
- }
258
- }
259
-
260
- if (!this._credential.cache.ca) {
261
- this._credential.cache.ca = this._voltConfig.ca_pem;
262
- }
263
-
264
- if (!this._credential.cache.ca) {
265
- throw new Error(
266
- "No certificate authority found in configuration for target Volt - check configuration"
267
- );
268
- }
269
-
270
- const keyPair = this._credential.getKey();
271
- const publicKeyPem = voltUtils.removeCarriageReturn(
272
- pki.publicKeyToPem(keyPair.publicKey)
273
- );
274
-
275
- if (!this._credential.cache.bindIp) {
276
- // Default the IP address if none is specified. This is used to set the SAN in the
277
- // certificate issued by the Volt.
278
- this._credential.cache.bindIp = ip.address();
279
- }
280
-
281
- this._credential.saveCache();
282
-
283
- // Form the request message.
284
- const bindRequest = {
285
- binding_name: this._config.client_name,
286
- host: this._credential.cache.bindIp
287
- };
198
+ // We want to create and manage our own DID.
199
+ const did =
200
+ (this._credential.cache.identity_did = `did:volt:${generateId()}`);
201
+
202
+ // Order of properties in the DID document is important,
203
+ // they must be in alphabetical order throughout the document.
204
+ // This is because the DID document is signed and the signature
205
+ // is over the entire document.
206
+ const didDocument = {
207
+ "@context": "https://www.w3.org/ns/did/v1",
208
+ authentication: [`${did}#key-1`],
209
+ controller: did,
210
+ description: this._config.client_name,
211
+ id: did,
212
+ verificationMethod: [
213
+ {
214
+ id: `${did}#key-1`,
215
+ publicKeyPem: publicKeyPem,
216
+ type: "Ed25519VerificationKey2018"
217
+ }
218
+ ]
219
+ };
220
+
221
+ // Set the DID document with proof in the authenticate request.
222
+ authenticateRequest.did_document = JSON.stringify(didDocument, null, 2);
223
+
224
+ // Now sign the entire DID document, including the proof and set it in the
225
+ // bind request. This is required by the Volt to verify that we have the private key for the DID.
226
+ authenticateRequest.did_document_signature = signBase64(
227
+ keyPair.privateKey,
228
+ authenticateRequest.did_document
229
+ );
288
230
 
289
- if (this._voltConfig.challenge_code) {
290
- bindRequest.challenge = voltUtils.signBase64(
291
- keyPair.privateKey,
292
- this._voltConfig.challenge_code
293
- );
231
+ log("did document is %s", authenticateRequest.did_document);
232
+ log("did signature is %s", authenticateRequest.did_document_signature);
233
+ }
294
234
  } else {
295
- log(
296
- "****no Volt challenge code available**** => not sending challenge signature"
297
- );
235
+ // We already have a DID cached, so re-use it.
236
+ log("using cached DID %s", this._credential.cache.identity_did);
237
+ authenticateRequest.did = this._credential.cache.identity_did;
298
238
  }
299
239
 
300
- if (!this._credential.cache.client_id) {
301
- // If we don't have a client id, then we need to create a DID.
302
- this._credential.cache.client_id = `did:volt:${generateId()}`;
303
- }
304
-
305
- const did = this._credential.cache.client_id;
306
-
307
- // Order of properties in the DID document is important,
308
- // they must be in alphabetical order throughout the document.
309
- // This is because the DID document is signed and the signature
310
- // is over the entire document.
311
- const didDocument = {
312
- "@context": "https://www.w3.org/ns/did/v1",
313
- authentication: [`${did}#key-1`],
314
- description: this._config.client_name,
315
- id: did,
316
- verificationMethod: [
317
- {
318
- controller: did,
319
- id: `${did}#key-1`,
320
- publicKeyPem: publicKeyPem,
321
- type: "RsaSignature2018"
322
- }
323
- ]
324
- };
325
-
326
- const signDocument = JSON.stringify(didDocument, null, 2);
327
- log("signing did document");
328
- log(signDocument);
329
-
330
- // Sign the DID document.
331
- const documentDigest = voltUtils.signBase64(
332
- keyPair.privateKey,
333
- signDocument
334
- );
335
-
336
- const proofSignature = voltUtils.jwsSignature(documentDigest);
337
-
338
- // Create the DID document proof.
339
- didDocument.proof = {
340
- created: new Date().toISOString(),
341
- jws: proofSignature,
342
- proofPurpose: "assertionMethod",
343
- type: "RsaSignature2018",
344
- verificationMethod: `${did}#key-1`
345
- };
346
-
347
- // Set the DID document with proof in the bind request.
348
- bindRequest.did_document = JSON.stringify(didDocument, null, 2);
349
-
350
- // Now sign the entire DID document, including the proof and set it in the
351
- // bind request. This is required by the Volt to verify that we have the private key for the DID.
352
- bindRequest.did_document_signature = voltUtils.signBase64(
353
- keyPair.privateKey,
354
- bindRequest.did_document
355
- );
356
-
357
- log("did document is %s", bindRequest.did_document);
358
- log("did signature is %s", bindRequest.did_document_signature);
359
- log("bind TTL is ", this._config.bindRequestTTL);
240
+ log("authenticate TTL is ", this._config.bindRequestTTL);
360
241
 
361
242
  // This will block while the request is pending.
362
243
  const pollInterval = 10000;
@@ -368,12 +249,12 @@ export async function bindInternal() {
368
249
  await new Promise((resolve) => setTimeout(resolve, pollInterval));
369
250
  }
370
251
  // eslint-disable-next-line no-await-in-loop
371
- decision = await issueBind.call(
252
+ decision = await issueAuthenticate.call(
372
253
  this,
373
- bindRequest,
254
+ authenticateRequest,
374
255
  this._voltConfig.bindRequestTTL
375
256
  );
376
- log("binding decision: %s", decision);
257
+ log("authenticate decision: %s", decision);
377
258
  } while (
378
259
  decision === "POLICY_DECISION_PROMPT" ||
379
260
  decision === "POLICY_DECISION_PENDING"
@@ -426,6 +307,15 @@ export function connectInternal(helloPayload) {
426
307
  onDisconnected();
427
308
  this.emit("connected", false);
428
309
  }
310
+
311
+ if (err.message.endsWith("session invalid")) {
312
+ log("Volt session invalid - clearing session");
313
+ delete this._credential.cache.session_id;
314
+ delete this._credential.cache.cert;
315
+ this._credential.saveCache();
316
+ }
317
+
318
+ this.emit("connectionError", err);
429
319
  });
430
320
 
431
321
  this._voltConnection.on("evt", (evt) => {
@@ -438,10 +328,24 @@ export function connectInternal(helloPayload) {
438
328
 
439
329
  this._voltConnection.on("invoke_request", (invoke_request) => {
440
330
  const invokeId = invoke_request.invoke_id;
441
- if (this._activeRPC[invokeId]) {
442
- this._activeRPC[invokeId].parsePayload(invoke_request);
331
+
332
+ let rpcInvocation = this._activeRPC[invokeId];
333
+
334
+ if (rpcInvocation) {
335
+ if (rpcInvocation.started) {
336
+ rpcInvocation.parsePayload(invoke_request);
337
+ } else {
338
+ rpcInvocation
339
+ .initialise(invoke_request)
340
+ .then(() => {
341
+ this.emit("invoke_request", rpcInvocation);
342
+ })
343
+ .catch((err) => {
344
+ log("invoke_request - error [%s]", err.message);
345
+ });
346
+ }
443
347
  } else {
444
- const rpcInvocation = new RpcInvocation(this, this._voltConnection);
348
+ rpcInvocation = new RpcInvocation(this, this._voltConnection);
445
349
 
446
350
  rpcInvocation.on("end", () => {
447
351
  log("removing active RPC [%s]", invokeId);
@@ -450,9 +354,11 @@ export function connectInternal(helloPayload) {
450
354
 
451
355
  rpcInvocation
452
356
  .initialise(invoke_request)
453
- .then(() => {
357
+ .then((response) => {
454
358
  this._activeRPC[invokeId] = rpcInvocation;
455
- this.emit("invoke_request", rpcInvocation);
359
+ if (!response.key_exchange) {
360
+ this.emit("invoke_request", rpcInvocation);
361
+ }
456
362
  })
457
363
  .catch((err) => {
458
364
  log("invoke_request - error [%s]", err.message);
@@ -598,6 +504,7 @@ export async function fetchVoltConfig(discovery_url) {
598
504
  "public_key",
599
505
  "fingerprint",
600
506
  "address",
507
+ "http_address",
601
508
  "ca_pem",
602
509
  "challenge_code",
603
510
  "cloud",
@@ -6,7 +6,6 @@ import { getServiceDescriptors } from "./proto-utils.js";
6
6
  import { createClient as createGrpcClient } from "./grpc-utils.js";
7
7
  import {
8
8
  authenticateInternal,
9
- bindInternal,
10
9
  connectInternal,
11
10
  fetchVoltConfig,
12
11
  fetchVoltConfigFromDID,
@@ -60,6 +59,7 @@ export class VoltClient extends EventEmitter {
60
59
  /**
61
60
  * Initialises a binding and connection to the volt.
62
61
  * @param {string} [config] - the location of the Volt configuration file, or a configuration object
62
+ * @param {object} [extras] - additional configuration properties
63
63
  */
64
64
  async initialise(config, extras = {}) {
65
65
  try {
@@ -75,10 +75,15 @@ export class VoltClient extends EventEmitter {
75
75
  configPath = path.resolve(config);
76
76
  log("Attempt to load config from file %s", configPath);
77
77
  try {
78
- const configContents = await readFile(
79
- new URL(configPath, import.meta.url)
80
- );
81
- configJSON = JSON.parse(configContents);
78
+ if (fs.existsSync(configPath)) {
79
+ const configContents = await readFile(
80
+ new URL(configPath, import.meta.url)
81
+ );
82
+ configJSON = JSON.parse(configContents);
83
+ } else {
84
+ // There is currently no file at the given configuration path.
85
+ configJSON = {};
86
+ }
82
87
  } catch (err) {
83
88
  log("failure loading config file %s [%s]", configPath, err.message);
84
89
  throw new Error(
@@ -95,15 +100,15 @@ export class VoltClient extends EventEmitter {
95
100
  throw new Error("config argument is required to be an object");
96
101
  }
97
102
 
103
+ this._config = { ...extras, ...configJSON };
104
+
98
105
  if (
99
- !configJSON.client_name ||
100
- typeof configJSON.client_name !== "string"
106
+ !this._config.client_name ||
107
+ typeof this._config.client_name !== "string"
101
108
  ) {
102
109
  throw new Error("client_name property required in config");
103
110
  }
104
111
 
105
- this._config = { ...configJSON, ...extras };
106
-
107
112
  let voltDID = "";
108
113
  let voltHttpAddress = "";
109
114
  if (typeof this._config.volt === "string") {
@@ -112,6 +117,7 @@ export class VoltClient extends EventEmitter {
112
117
  } else {
113
118
  voltHttpAddress = this._config.volt;
114
119
  }
120
+ delete this._config.volt;
115
121
  } else if (typeof this._config.volt !== "object") {
116
122
  throw new Error("configuration is missing the 'volt' object");
117
123
  } else {
@@ -126,7 +132,7 @@ export class VoltClient extends EventEmitter {
126
132
  } else if (voltHttpAddress && !this._config?.volt?.id) {
127
133
  const discoConfig = await fetchVoltConfig.call(
128
134
  this,
129
- `${voltHttpAddress}/discovery`
135
+ `${voltHttpAddress}/discover`
130
136
  );
131
137
  this._config = {
132
138
  ...this._config,
@@ -135,8 +141,12 @@ export class VoltClient extends EventEmitter {
135
141
  }
136
142
 
137
143
  this._voltConfig = this._config.volt;
138
- this._voltConfig.did = voltDID;
139
- this._voltConfig.http_address = voltHttpAddress;
144
+ if (voltDID) {
145
+ this._voltConfig.did = voltDID;
146
+ }
147
+ if (voltHttpAddress) {
148
+ this._voltConfig.http_address = voltHttpAddress;
149
+ }
140
150
 
141
151
  if (!this._voltConfig.id || typeof this._voltConfig.id !== "string") {
142
152
  throw new Error("'id' property is missing in Volt configuration");
@@ -184,9 +194,12 @@ export class VoltClient extends EventEmitter {
184
194
  if (!isBound) {
185
195
  // This is usually because of request being denied.
186
196
  throw new Error("Volt authentication failed");
197
+ } else {
198
+ // Perist the configuration.
199
+ this._config = this._credential.saveCache();
187
200
  }
188
201
 
189
- return Promise.resolve();
202
+ return Promise.resolve(this._config);
190
203
  } catch (err) {
191
204
  log("Failure starting Volt client [%s]", err.message);
192
205
  throw err;
@@ -655,6 +668,13 @@ export class VoltClient extends EventEmitter {
655
668
  });
656
669
  }
657
670
 
671
+ SqlTransaction(request) {
672
+ const grpcClient = this.getVoltAPIClient();
673
+
674
+ const call = new GRPCCall(this, "Transaction", "METHOD_TYPE_BIDI");
675
+ return call.start(grpcClient, request);
676
+ }
677
+
658
678
  /**
659
679
  * WireAPI
660
680
  */
@@ -19,10 +19,9 @@ function _cleanUp(immediateReconnect) {
19
19
  this._pingTimeoutTimer = 0;
20
20
  }
21
21
 
22
- if (
23
- !this._dying &&
24
- (immediateReconnect || (this._autoRetry && !this._reconnectTimer))
25
- ) {
22
+ if (this._dying) {
23
+ clearTimeout(this._reconnectTimer);
24
+ } else if (immediateReconnect || (this._autoRetry && !this._reconnectTimer)) {
26
25
  this._reconnectTimer = setTimeout(
27
26
  () => {
28
27
  this._reconnectTimer = 0;
@@ -48,13 +47,18 @@ function _doConnect(connectHello) {
48
47
 
49
48
  const grpcClient = this._voltClient.getVoltAPIClient();
50
49
 
51
- let helloPayload = connectHello;
52
- if (!helloPayload) {
53
- log("defaulting hello payload");
54
- helloPayload = {
55
- hello: {}
56
- };
57
- }
50
+ // Notify the Volt of our ping interval. If the Volt doesn't receive a ping from us
51
+ // within this time, it will assume the connection is dead.
52
+ const defaultHello = {
53
+ ping_interval: this._pingInterval,
54
+ timestamp: Date.now()
55
+ };
56
+
57
+ const helloPayload = { ...connectHello } || {};
58
+
59
+ helloPayload.hello = { ...defaultHello, ...connectHello?.hello };
60
+
61
+ log("hello is %j", helloPayload);
58
62
 
59
63
  this.call.on("error", (err) => {
60
64
  log("error on connection stream [%s]", err.message);
@@ -72,17 +76,22 @@ function _doConnect(connectHello) {
72
76
  clearTimeout(this._pingTimeoutTimer);
73
77
  }
74
78
 
75
- // Schedule the next ping.
76
- _startPingTimer.call(this);
77
-
78
79
  this.emit("ping", response.ping.timestamp);
79
80
  break;
80
81
  }
81
82
  case "acknowledge": {
82
83
  if (!this._connectionId) {
83
84
  this._connectionId = response.acknowledge.connection_id;
85
+
86
+ // Set the ping timeout interval to the value returned by the Volt. If we don't
87
+ // hear from the Volt in this time, we'll assume the connection is dead.
88
+ this._timeoutInterval = response.acknowledge.ping_interval;
89
+
84
90
  log("connection id is %s", this._connectionId);
85
91
  this.emit("connected", this._connectionId);
92
+
93
+ // Schedule the next ping.
94
+ _startPingTimer.call(this);
86
95
  }
87
96
  break;
88
97
  }
@@ -110,8 +119,6 @@ function _doConnect(connectHello) {
110
119
  });
111
120
 
112
121
  this.call.start(grpcClient, helloPayload);
113
-
114
- _startPingTimer.call(this);
115
122
  }
116
123
 
117
124
  function _startPingTimer() {
@@ -138,6 +145,8 @@ function _startPingTimer() {
138
145
  _connectionTimeoutHandler.bind(this),
139
146
  this._timeoutInterval
140
147
  );
148
+
149
+ _startPingTimer.call(this);
141
150
  }, this._pingInterval);
142
151
  }
143
152