@tdxvolt/volt-client-grpc 0.1.0
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 +5 -0
- package/lib/index.cjs +1883 -0
- package/package.json +50 -0
- package/protobuf/README.md +4 -0
- package/protobuf/tdx/volt_api/data/v1/sqlite.proto +43 -0
- package/protobuf/tdx/volt_api/data/v1/sqlite_database_api.proto +105 -0
- package/protobuf/tdx/volt_api/data/v1/sqlite_server_api.proto +42 -0
- package/protobuf/tdx/volt_api/ssi/v1/ssi_api.proto +37 -0
- package/protobuf/tdx/volt_api/sync/v1/sync.proto +14 -0
- package/protobuf/tdx/volt_api/tunnel/v1/proxy_api.proto +9 -0
- package/protobuf/tdx/volt_api/tunnel/v1/tunnel_api.proto +77 -0
- package/protobuf/tdx/volt_api/volt/v1/discovery_api.proto +39 -0
- package/protobuf/tdx/volt_api/volt/v1/file.proto +17 -0
- package/protobuf/tdx/volt_api/volt/v1/file_api.proto +117 -0
- package/protobuf/tdx/volt_api/volt/v1/remote.proto +94 -0
- package/protobuf/tdx/volt_api/volt/v1/status.proto +15 -0
- package/protobuf/tdx/volt_api/volt/v1/volt.proto +563 -0
- package/protobuf/tdx/volt_api/volt/v1/volt_api.proto +943 -0
- package/protobuf/tdx/volt_api/volt/v1/wire_api.proto +50 -0
- package/src/constants.js +75 -0
- package/src/grpc-call.js +254 -0
- package/src/grpc-utils.js +197 -0
- package/src/index.js +7 -0
- package/src/proto-utils.js +81 -0
- package/src/tunnel.pb.js +0 -0
- package/src/utils.js +105 -0
- package/src/volt-client-internal.js +288 -0
- package/src/volt-client.js +491 -0
- package/src/volt-connection.js +157 -0
- package/src/volt-crypto.js +213 -0
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import debug from "debug";
|
|
2
|
+
import EventEmitter from "events";
|
|
3
|
+
import {VoltCrypto} from "./volt-crypto.js";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import {getServiceDescriptors} from "./proto-utils.js";
|
|
6
|
+
import {createClient as createGrpcClient} from "./grpc-utils.js";
|
|
7
|
+
import {
|
|
8
|
+
bindInternal,
|
|
9
|
+
connectInternal,
|
|
10
|
+
fetchVoltConfig,
|
|
11
|
+
getVoltAPIClientInternal,
|
|
12
|
+
unaryCall,
|
|
13
|
+
} from "./volt-client-internal.js";
|
|
14
|
+
import fs from "fs";
|
|
15
|
+
import GRPCCall from "./grpc-call.js";
|
|
16
|
+
|
|
17
|
+
const {readFile} = fs.promises;
|
|
18
|
+
const log = debug("volt-client-grpc:volt-client");
|
|
19
|
+
|
|
20
|
+
export class VoltClient extends EventEmitter {
|
|
21
|
+
/**
|
|
22
|
+
* @param {object} grpc - an instance of the grpc module (see https://www.npmjs.com/package/grpc)
|
|
23
|
+
*/
|
|
24
|
+
constructor(grpc) {
|
|
25
|
+
super();
|
|
26
|
+
|
|
27
|
+
if (!grpc) {
|
|
28
|
+
throw new Error("invalid arguments, grpc is required");
|
|
29
|
+
}
|
|
30
|
+
this._grpc = grpc;
|
|
31
|
+
this._cachedClient = null;
|
|
32
|
+
this._cachedRemoteClient = null;
|
|
33
|
+
this._session = null;
|
|
34
|
+
this._config = null;
|
|
35
|
+
this._options = null;
|
|
36
|
+
this._voltConnection = null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get config() {
|
|
40
|
+
return this._config;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get crypto() {
|
|
44
|
+
return this._crypto;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get grpc() {
|
|
48
|
+
return this._grpc;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get options() {
|
|
52
|
+
return this._options;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Initialises a binding and connection to the volt.
|
|
57
|
+
* @param {string} [config] - the location of the Volt configuration file, or a configuration object
|
|
58
|
+
*/
|
|
59
|
+
async initialise(config, extras = {}) {
|
|
60
|
+
try {
|
|
61
|
+
if (!config) {
|
|
62
|
+
throw new Error("configPath argument is required to be a non-empty string");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let configPath;
|
|
66
|
+
let configJSON;
|
|
67
|
+
if (typeof config === "string") {
|
|
68
|
+
configPath = path.resolve(config);
|
|
69
|
+
log("Attempt to load config from file %s", configPath);
|
|
70
|
+
try {
|
|
71
|
+
const configContents = await readFile(new URL(configPath, import.meta.url));
|
|
72
|
+
configJSON = JSON.parse(configContents);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
log("failure loading config file %s [%s]", configPath, err.message);
|
|
75
|
+
throw new Error(`failed to load configuration - check JSON format in ${configPath}`);
|
|
76
|
+
}
|
|
77
|
+
} else if (typeof config === "object") {
|
|
78
|
+
configJSON = config;
|
|
79
|
+
} else {
|
|
80
|
+
throw new Error("config must be either a file path or a config object");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!configJSON || typeof configJSON !== "object") {
|
|
84
|
+
throw new Error("config argument is required to be an object");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!configJSON.client_name || typeof configJSON.client_name !== "string") {
|
|
88
|
+
throw new Error("client_name property required in config");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
this._config = {...configJSON, ...extras};
|
|
92
|
+
|
|
93
|
+
if (this._config.volt_did && !this._config.volt) {
|
|
94
|
+
// No Volt configuration, attempt to discover it via the given DID.
|
|
95
|
+
const didConfig = await fetchVoltConfig.call(this, this._config.volt_did);
|
|
96
|
+
log("resolved config from DID: %s", JSON.stringify(didConfig, null, 2));
|
|
97
|
+
this._config = {...this._config, ...didConfig};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
this._options = this._config.volt;
|
|
101
|
+
|
|
102
|
+
if (!this._options || typeof this._options !== "object") {
|
|
103
|
+
throw new Error("configuration is missing the 'volt' object");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!this._options.id || typeof this._options.id !== "string") {
|
|
107
|
+
throw new Error("'id' property is missing in Volt configuration");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
this._crypto = new VoltCrypto(this._config, configPath);
|
|
111
|
+
|
|
112
|
+
await this._crypto.initialise();
|
|
113
|
+
|
|
114
|
+
if (!this.isRemote) {
|
|
115
|
+
// If we're not connecting via a tunnel, we must have the address and CA.
|
|
116
|
+
if (!this._options.local_address || typeof this._options.local_address !== "string") {
|
|
117
|
+
throw new Error("'local_address' property missing in Volt configuration");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!this._options.ca_pem || typeof this._options.ca_pem !== "string") {
|
|
121
|
+
throw new Error("'ca_pPem' property missing in Volt configuration");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let isBound;
|
|
126
|
+
if (!this._crypto._cryptoCache.cert) {
|
|
127
|
+
// No certificate present in the cache yet => we need to bind.
|
|
128
|
+
try {
|
|
129
|
+
isBound = await bindInternal.call(this);
|
|
130
|
+
} catch (bindErr) {
|
|
131
|
+
if (bindErr.message === "invalid arguments") {
|
|
132
|
+
// This is usually the result of a missing challenge signature or credential.
|
|
133
|
+
log("failure binding to Volt - did you supply a challenge code or verified credential?");
|
|
134
|
+
throw bindErr;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
isBound = true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!isBound) {
|
|
142
|
+
// This is usually because of request being denied.
|
|
143
|
+
throw new Error("Volt binding failed");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return Promise.resolve();
|
|
147
|
+
} catch (err) {
|
|
148
|
+
log("Failure starting Volt client [%s]", err.message);
|
|
149
|
+
throw err;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
initialiseAndConnect(configPath, extras = {}) {
|
|
154
|
+
return this.initialise(configPath, extras)
|
|
155
|
+
.then(() => {
|
|
156
|
+
return connectInternal.call(this);
|
|
157
|
+
})
|
|
158
|
+
.catch((err) => {
|
|
159
|
+
log("startAndConnect failure: %s", err.message);
|
|
160
|
+
return Promise.reject(err);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Creates a GRPC client for the given Volt service description.
|
|
166
|
+
*
|
|
167
|
+
* Automatically handles remote connections.
|
|
168
|
+
*
|
|
169
|
+
* n.b. for this javascript client Protobuf definitions should conform to the recommended
|
|
170
|
+
* structure, which is that a package proto file resides in a folder path that matches the
|
|
171
|
+
* package name, and services contained in the package are named in in Pascal Case and using
|
|
172
|
+
* the suffix 'API'. For example the WebcamControlAPI in package tdx.volt_api.webcam.v1 should reside
|
|
173
|
+
* in a protobuf file at tdx/api/webcam/v1/webcam_control_api.proto
|
|
174
|
+
*
|
|
175
|
+
* @param {object} service a service description.
|
|
176
|
+
* @param {object} descriptors the service descriptors, must be specified unless the service
|
|
177
|
+
* packages are well-known Volt APIs.
|
|
178
|
+
*/
|
|
179
|
+
createServiceClient(service, descriptors) {
|
|
180
|
+
const pkg = service.service_description.service_api;
|
|
181
|
+
|
|
182
|
+
let serviceDescriptors;
|
|
183
|
+
if (!descriptors) {
|
|
184
|
+
serviceDescriptors = {};
|
|
185
|
+
pkg.forEach((svc) => {
|
|
186
|
+
log("adding service %s to service client", svc);
|
|
187
|
+
const packageDescriptors = getServiceDescriptors(this._grpc, svc);
|
|
188
|
+
serviceDescriptors = {...serviceDescriptors, ...packageDescriptors};
|
|
189
|
+
});
|
|
190
|
+
} else {
|
|
191
|
+
serviceDescriptors = descriptors;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (this.isRemote) {
|
|
195
|
+
if (!this._crypto.cache.tunnel_address) {
|
|
196
|
+
throw new Error("Remote connection enabled but no tunnel address - have you called start()?");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// In remote mode => create a client using the discovered tunnel address **and** passing the service reourceId
|
|
200
|
+
return createGrpcClient(
|
|
201
|
+
this._grpc,
|
|
202
|
+
serviceDescriptors,
|
|
203
|
+
this._crypto.cache.tunnel_address,
|
|
204
|
+
this._crypto.cache,
|
|
205
|
+
false,
|
|
206
|
+
service.volt_id,
|
|
207
|
+
service.id
|
|
208
|
+
);
|
|
209
|
+
} else {
|
|
210
|
+
return createGrpcClient(
|
|
211
|
+
this._grpc,
|
|
212
|
+
serviceDescriptors,
|
|
213
|
+
service.service_description.local_address,
|
|
214
|
+
this._crypto.cache
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
get isRemote() {
|
|
220
|
+
return (this._config.cloud_host || this._config.tunnel_host) && this._options.id;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
get isVoltTunnel() {
|
|
224
|
+
return this.isRemote && this._config.tunnel_host;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
connect() {
|
|
228
|
+
return connectInternal.call(this);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
disconnect() {
|
|
232
|
+
if (this._voltConnection) {
|
|
233
|
+
this._voltConnection.disconnect();
|
|
234
|
+
this._voltConnection = null;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
getVoltAPIClient() {
|
|
239
|
+
return getVoltAPIClientInternal.call(this);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Request resource access.
|
|
244
|
+
* @param {*} targetResourceId
|
|
245
|
+
* @param {*} accessType - see vault.proto for enum values.
|
|
246
|
+
*/
|
|
247
|
+
async RequestAccessBlocking(targetResourceId, accessType = "VOLT_ACCESS_READ") {
|
|
248
|
+
try {
|
|
249
|
+
const accessRequest = {
|
|
250
|
+
access: accessType,
|
|
251
|
+
resource_id: targetResourceId,
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// This will block while the request is pending.
|
|
255
|
+
// TODO - implement streamed rpc on volt? Not sure there is a need really...
|
|
256
|
+
const pollInterval = 10000;
|
|
257
|
+
let decision = "";
|
|
258
|
+
do {
|
|
259
|
+
if (decision) {
|
|
260
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
261
|
+
}
|
|
262
|
+
const accessResponse = await this.RequestAccess(accessRequest);
|
|
263
|
+
decision = accessResponse.decision;
|
|
264
|
+
log("access decision: %s", decision);
|
|
265
|
+
} while (decision === "POLICY_DECISION_PROMPT" || decision === "POLICY_DECISION_PENDING");
|
|
266
|
+
|
|
267
|
+
log("requestResourceAccess result is %j", decision);
|
|
268
|
+
return decision === "POLICY_DECISION_PERMIT";
|
|
269
|
+
} catch (err) {
|
|
270
|
+
log("failure requesting resource access [%s]", err.message);
|
|
271
|
+
return Promise.reject(err);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* VoltAPI - resource management
|
|
277
|
+
*/
|
|
278
|
+
|
|
279
|
+
CanAccessResource(request) {
|
|
280
|
+
return unaryCall.call(this, "CanAccessResource", request);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
DeleteResource(request) {
|
|
284
|
+
return unaryCall.call(this, "DeleteResource", request);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
DiscoverServices(request) {
|
|
288
|
+
return unaryCall.call(this, "DiscoverServices", request);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
GetResource(request) {
|
|
292
|
+
return unaryCall.call(this, "GetResource", request);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
GetResources(request) {
|
|
296
|
+
return unaryCall.call(this, "GetResources", request);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
GetResourceAncestors(request) {
|
|
300
|
+
return unaryCall.call(this, "GetResourceAncestors", request);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
GetResourceDescendants(request) {
|
|
304
|
+
return unaryCall.call(this, "GetResourceDescendants", request);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
RequestAccess(request) {
|
|
308
|
+
return unaryCall.call(this, "RequestAccess", request);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
SaveResource(request) {
|
|
312
|
+
return unaryCall.call(this, "SaveResource", request);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
SaveResourceAttribute(request) {
|
|
316
|
+
return unaryCall.call(this, "SaveResourceAttribute", request);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
SetServiceStatus(request) {
|
|
320
|
+
return unaryCall.call(this, "SetServiceStatus", request);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* VoltAPI - volt management
|
|
325
|
+
*/
|
|
326
|
+
|
|
327
|
+
Bind(request) {
|
|
328
|
+
return unaryCall.call(this, "Bind", request);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
DeleteAccess(request) {
|
|
332
|
+
return unaryCall.call(this, "DeleteAccess", request);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
DeleteVolt(request) {
|
|
336
|
+
return unaryCall.call(this, "DeleteVolt", request);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
GetAccess(request) {
|
|
340
|
+
return unaryCall.call(this, "GetAccess", request);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
GetBindings(request) {
|
|
344
|
+
return unaryCall.call(this, "GetBindings", request);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
GetIdentities(request) {
|
|
348
|
+
return unaryCall.call(this, "GetIdentities", request);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
GetIdentity(request) {
|
|
352
|
+
return unaryCall.call(this, "GetIdentity", request);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
GetIdentityToken(request) {
|
|
356
|
+
return unaryCall.call(this, "GetIdentityToken", request);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
GetPolicy(request) {
|
|
360
|
+
return unaryCall.call(this, "GetPolicy", request);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
GetSettings(request) {
|
|
364
|
+
return unaryCall.call(this, "GetSettings", request);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
SaveAccess(request) {
|
|
368
|
+
return unaryCall.call(this, "SaveAccess", request);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
SaveCloudConnection(request) {
|
|
372
|
+
return unaryCall.call(this, "SaveCloudConnection", request);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
SaveIdentity(request) {
|
|
376
|
+
return unaryCall.call(this, "SaveIdentity", request);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
SaveSettings(request) {
|
|
380
|
+
return unaryCall.call(this, "SaveSettings", request);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
SetAccessRequestDecision(request) {
|
|
384
|
+
return unaryCall.call(this, "SetAccessRequestDecision", request);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
SetBindingDecision(request) {
|
|
388
|
+
return unaryCall.call(this, "SetBindingDecision", request);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
Shutdown(request) {
|
|
392
|
+
return unaryCall.call(this, "Shutdown", request);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
SignVerify(request) {
|
|
396
|
+
return unaryCall.call(this, "SignVerify", request);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* FileAPI
|
|
401
|
+
*/
|
|
402
|
+
GetFileDescendants(request) {
|
|
403
|
+
return unaryCall.call(this, "GetFileDescendants", request);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Asynchronous file download
|
|
408
|
+
* @param {*} request
|
|
409
|
+
* @returns stream
|
|
410
|
+
*/
|
|
411
|
+
DownloadFile(request) {
|
|
412
|
+
const grpcClient = this.getVoltAPIClient();
|
|
413
|
+
|
|
414
|
+
return new Promise((resolve, reject) => {
|
|
415
|
+
const meta = this._crypto.getIdentityMetadata(this._grpc, this._options.id, this.isRemote);
|
|
416
|
+
resolve(grpcClient.DownloadFile(request, meta.metadata));
|
|
417
|
+
}).catch((err) => {
|
|
418
|
+
log("error during %s [%s]", method, err.message);
|
|
419
|
+
return Promise.reject(err);
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
UploadFile(request, callback) {
|
|
424
|
+
const grpcClient = this.getVoltAPIClient();
|
|
425
|
+
|
|
426
|
+
const uploadCall = new GRPCCall(this, "UploadFile", "METHOD_TYPE_BIDI");
|
|
427
|
+
return uploadCall.start(grpcClient, request, callback);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Synchronous file download
|
|
432
|
+
* @param {*} request
|
|
433
|
+
* @returns file buffer, base64 encoded
|
|
434
|
+
*/
|
|
435
|
+
DownloadFileSync(request) {
|
|
436
|
+
const grpcClient = this.getVoltAPIClient();
|
|
437
|
+
|
|
438
|
+
const blocks = [];
|
|
439
|
+
|
|
440
|
+
const call = new GRPCCall(this, "DownloadFile", "METHOD_TYPE_SERVER_STREAM");
|
|
441
|
+
call.start(grpcClient, request, (err, response) => {
|
|
442
|
+
if (err) {
|
|
443
|
+
return call.reject(err);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
switch (response.payload) {
|
|
447
|
+
case "block":
|
|
448
|
+
blocks.push(response.block);
|
|
449
|
+
break;
|
|
450
|
+
case "status":
|
|
451
|
+
if (response.status.message) {
|
|
452
|
+
return call.reject(new Error(response.status.message));
|
|
453
|
+
} else {
|
|
454
|
+
// Non-error response status => do nothing and wait for the stream to end.
|
|
455
|
+
}
|
|
456
|
+
break;
|
|
457
|
+
default:
|
|
458
|
+
log("unrecognised response payload %s", response.payload);
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
return call
|
|
464
|
+
.wait()
|
|
465
|
+
.then(() => {
|
|
466
|
+
log("Download finished");
|
|
467
|
+
return {buffer: Buffer.concat(blocks).toString("base64")};
|
|
468
|
+
})
|
|
469
|
+
.catch((err) => {
|
|
470
|
+
log("error during DownloadFile [%s]", err.message);
|
|
471
|
+
return Promise.reject(err);
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* WireAPI
|
|
477
|
+
*/
|
|
478
|
+
PublishWire(request, callback) {
|
|
479
|
+
const grpcClient = this.getVoltAPIClient();
|
|
480
|
+
|
|
481
|
+
const publishCall = new GRPCCall(this, "PublishWire", "METHOD_TYPE_CLIENT_STREAM");
|
|
482
|
+
return publishCall.start(grpcClient, request, callback);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
SubscribeWire(request, callback) {
|
|
486
|
+
const grpcClient = this.getVoltAPIClient();
|
|
487
|
+
|
|
488
|
+
const subscribeCall = new GRPCCall(this, "SubscribeWire", "METHOD_TYPE_BIDI");
|
|
489
|
+
return subscribeCall.start(grpcClient, request, callback);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/* eslint-disable no-use-before-define */
|
|
2
|
+
/* eslint-disable no-underscore-dangle */
|
|
3
|
+
import debug from "debug";
|
|
4
|
+
import EventEmitter from "events";
|
|
5
|
+
import GRPCCall from "./grpc-call.js";
|
|
6
|
+
|
|
7
|
+
const log = debug("volt-client-grpc:volt-connection");
|
|
8
|
+
|
|
9
|
+
function _cleanUp(immediateReconnect) {
|
|
10
|
+
this._session = false;
|
|
11
|
+
|
|
12
|
+
if (this._pingTimer) {
|
|
13
|
+
clearTimeout(this._pingTimer);
|
|
14
|
+
this._pingTimer = 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (this._pingTimeoutTimer) {
|
|
18
|
+
clearTimeout(this._pingTimeoutTimer);
|
|
19
|
+
this._pingTimeoutTimer = 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!this._dying && (immediateReconnect || (this._autoRetry && !this._reconnectTimer))) {
|
|
23
|
+
this._reconnectTimer = setTimeout(
|
|
24
|
+
() => {
|
|
25
|
+
this._reconnectTimer = 0;
|
|
26
|
+
_doConnect.call(this);
|
|
27
|
+
},
|
|
28
|
+
immediateReconnect ? 0 : this._reconnectInterval
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (this.call && this.call.writable) {
|
|
33
|
+
this.call.end();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function _connectionTimeoutHandler() {
|
|
38
|
+
log("server timed out");
|
|
39
|
+
this.emit("connected", false);
|
|
40
|
+
_cleanUp.call(this);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function _doConnect() {
|
|
44
|
+
this.call = new GRPCCall(this._voltClient, "Connect", "METHOD_TYPE_BIDI");
|
|
45
|
+
|
|
46
|
+
const grpcClient = this._voltClient.getVoltAPIClient();
|
|
47
|
+
const helloPayload = {
|
|
48
|
+
hello: {},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
this.call.start(grpcClient, helloPayload, (err, response) => {
|
|
52
|
+
if (err) {
|
|
53
|
+
log("error on connection stream [%s]", err.message);
|
|
54
|
+
_cleanUp.call(this);
|
|
55
|
+
this.emit("error", err);
|
|
56
|
+
} else {
|
|
57
|
+
switch (response.payload) {
|
|
58
|
+
case "ping": {
|
|
59
|
+
log("pinged");
|
|
60
|
+
|
|
61
|
+
// Received ping response from server => clear timeout.
|
|
62
|
+
if (this._pingTimeoutTimer) {
|
|
63
|
+
clearTimeout(this._pingTimeoutTimer);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Schedule the next ping.
|
|
67
|
+
_startPingTimer.call(this);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
case "hello":
|
|
71
|
+
if (!this._session) {
|
|
72
|
+
this._session = true;
|
|
73
|
+
this.emit("connected", this._session);
|
|
74
|
+
}
|
|
75
|
+
break;
|
|
76
|
+
case "evt":
|
|
77
|
+
log("received event %s", response.evt.event_type);
|
|
78
|
+
this.emit("evt", response.evt);
|
|
79
|
+
break;
|
|
80
|
+
default:
|
|
81
|
+
log("unimplemented connect response payload: %s", response.payload);
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
this.call
|
|
88
|
+
.wait()
|
|
89
|
+
.then(() => {
|
|
90
|
+
if (this._session) {
|
|
91
|
+
this.emit("connected", false);
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
.catch((err) => {
|
|
95
|
+
log("unexpected error on connect call [%s]", err.message);
|
|
96
|
+
})
|
|
97
|
+
.finally(() => {
|
|
98
|
+
_cleanUp.call(this);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
_startPingTimer.call(this);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function _startPingTimer() {
|
|
105
|
+
if (this._pingTimer) {
|
|
106
|
+
log("ping timer running");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
this._pingTimer = setTimeout(() => {
|
|
111
|
+
// Send next ping to server.
|
|
112
|
+
const request = {ping: {timestamp: Date.now()}};
|
|
113
|
+
|
|
114
|
+
log("pinging Volt");
|
|
115
|
+
this.call.write(request);
|
|
116
|
+
|
|
117
|
+
this._pingTimer = 0;
|
|
118
|
+
|
|
119
|
+
// Schedule a timeout if we don't hear back from the server.
|
|
120
|
+
if (this._pingTimeoutTimer) {
|
|
121
|
+
clearTimeout(this._pingTimeoutTimer);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
this._pingTimeoutTimer = setTimeout(_connectionTimeoutHandler.bind(this), this._timeoutInterval);
|
|
125
|
+
}, this._pingInterval);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export default class VoltConnection extends EventEmitter {
|
|
129
|
+
constructor(voltClient, grpc, options = {}) {
|
|
130
|
+
super();
|
|
131
|
+
|
|
132
|
+
this._grpc = grpc;
|
|
133
|
+
this._autoRetry = options.autoReconnect === undefined ? true : !!options.autoReconnect;
|
|
134
|
+
this._dying = false;
|
|
135
|
+
this._voltClient = voltClient;
|
|
136
|
+
this._options = options;
|
|
137
|
+
this._pingTimer = 0;
|
|
138
|
+
this._pingTimeoutTimer = 0;
|
|
139
|
+
this._session = false;
|
|
140
|
+
this._pingInterval = options.pingInterval || 10000;
|
|
141
|
+
this._reconnectInterval = options.reconnectInterval || 5000;
|
|
142
|
+
this._timeoutInterval = options.timeoutInterval || 60000;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
connect(throwOnFail) {
|
|
146
|
+
if (throwOnFail) {
|
|
147
|
+
return _doConnect.call(this);
|
|
148
|
+
} else {
|
|
149
|
+
return Promise.resolve(_cleanUp.call(this, true));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
disconnect() {
|
|
154
|
+
this._dying = true;
|
|
155
|
+
_cleanUp.call(this);
|
|
156
|
+
}
|
|
157
|
+
}
|