speechflow 1.3.0 → 1.3.2
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/CHANGELOG.md +15 -0
- package/README.md +165 -22
- package/dst/speechflow-node-a2a-gender.d.ts +2 -0
- package/dst/speechflow-node-a2a-gender.js +137 -59
- package/dst/speechflow-node-a2a-gender.js.map +1 -1
- package/dst/speechflow-node-a2a-meter.d.ts +3 -1
- package/dst/speechflow-node-a2a-meter.js +79 -35
- package/dst/speechflow-node-a2a-meter.js.map +1 -1
- package/dst/speechflow-node-a2a-mute.d.ts +1 -0
- package/dst/speechflow-node-a2a-mute.js +37 -11
- package/dst/speechflow-node-a2a-mute.js.map +1 -1
- package/dst/speechflow-node-a2a-vad.d.ts +3 -0
- package/dst/speechflow-node-a2a-vad.js +194 -96
- package/dst/speechflow-node-a2a-vad.js.map +1 -1
- package/dst/speechflow-node-a2a-wav.js +27 -11
- package/dst/speechflow-node-a2a-wav.js.map +1 -1
- package/dst/speechflow-node-a2t-deepgram.d.ts +4 -0
- package/dst/speechflow-node-a2t-deepgram.js +141 -43
- package/dst/speechflow-node-a2t-deepgram.js.map +1 -1
- package/dst/speechflow-node-t2a-elevenlabs.d.ts +2 -0
- package/dst/speechflow-node-t2a-elevenlabs.js +61 -12
- package/dst/speechflow-node-t2a-elevenlabs.js.map +1 -1
- package/dst/speechflow-node-t2a-kokoro.d.ts +1 -0
- package/dst/speechflow-node-t2a-kokoro.js +10 -4
- package/dst/speechflow-node-t2a-kokoro.js.map +1 -1
- package/dst/speechflow-node-t2t-deepl.js +8 -4
- package/dst/speechflow-node-t2t-deepl.js.map +1 -1
- package/dst/speechflow-node-t2t-format.js +2 -2
- package/dst/speechflow-node-t2t-format.js.map +1 -1
- package/dst/speechflow-node-t2t-ollama.js +1 -1
- package/dst/speechflow-node-t2t-ollama.js.map +1 -1
- package/dst/speechflow-node-t2t-openai.js +1 -1
- package/dst/speechflow-node-t2t-openai.js.map +1 -1
- package/dst/speechflow-node-t2t-sentence.d.ts +1 -1
- package/dst/speechflow-node-t2t-sentence.js +35 -24
- package/dst/speechflow-node-t2t-sentence.js.map +1 -1
- package/dst/speechflow-node-t2t-subtitle.js +85 -17
- package/dst/speechflow-node-t2t-subtitle.js.map +1 -1
- package/dst/speechflow-node-t2t-transformers.js +2 -2
- package/dst/speechflow-node-t2t-transformers.js.map +1 -1
- package/dst/speechflow-node-x2x-filter.js +4 -4
- package/dst/speechflow-node-x2x-trace.js +1 -1
- package/dst/speechflow-node-x2x-trace.js.map +1 -1
- package/dst/speechflow-node-xio-device.js +12 -8
- package/dst/speechflow-node-xio-device.js.map +1 -1
- package/dst/speechflow-node-xio-file.js +9 -3
- package/dst/speechflow-node-xio-file.js.map +1 -1
- package/dst/speechflow-node-xio-mqtt.js +5 -2
- package/dst/speechflow-node-xio-mqtt.js.map +1 -1
- package/dst/speechflow-node-xio-websocket.js +11 -11
- package/dst/speechflow-node-xio-websocket.js.map +1 -1
- package/dst/speechflow-utils.d.ts +5 -0
- package/dst/speechflow-utils.js +77 -44
- package/dst/speechflow-utils.js.map +1 -1
- package/dst/speechflow.js +104 -34
- package/dst/speechflow.js.map +1 -1
- package/etc/eslint.mjs +1 -2
- package/etc/speechflow.yaml +18 -7
- package/etc/stx.conf +3 -3
- package/package.json +14 -13
- package/src/speechflow-node-a2a-gender.ts +148 -64
- package/src/speechflow-node-a2a-meter.ts +87 -40
- package/src/speechflow-node-a2a-mute.ts +39 -11
- package/src/speechflow-node-a2a-vad.ts +206 -100
- package/src/speechflow-node-a2a-wav.ts +27 -11
- package/src/speechflow-node-a2t-deepgram.ts +148 -45
- package/src/speechflow-node-t2a-elevenlabs.ts +65 -12
- package/src/speechflow-node-t2a-kokoro.ts +11 -4
- package/src/speechflow-node-t2t-deepl.ts +9 -4
- package/src/speechflow-node-t2t-format.ts +2 -2
- package/src/speechflow-node-t2t-ollama.ts +1 -1
- package/src/speechflow-node-t2t-openai.ts +1 -1
- package/src/speechflow-node-t2t-sentence.ts +38 -27
- package/src/speechflow-node-t2t-subtitle.ts +62 -15
- package/src/speechflow-node-t2t-transformers.ts +4 -3
- package/src/speechflow-node-x2x-filter.ts +4 -4
- package/src/speechflow-node-x2x-trace.ts +1 -1
- package/src/speechflow-node-xio-device.ts +12 -8
- package/src/speechflow-node-xio-file.ts +9 -3
- package/src/speechflow-node-xio-mqtt.ts +5 -2
- package/src/speechflow-node-xio-websocket.ts +12 -12
- package/src/speechflow-utils.ts +78 -44
- package/src/speechflow.ts +117 -36
package/src/speechflow-utils.ts
CHANGED
|
@@ -24,11 +24,19 @@ export function audioBufferDuration (
|
|
|
24
24
|
channels = 1,
|
|
25
25
|
littleEndian = true
|
|
26
26
|
) {
|
|
27
|
+
/* sanity check parameters */
|
|
27
28
|
if (!Buffer.isBuffer(buffer))
|
|
28
29
|
throw new Error("invalid input (Buffer expected)")
|
|
29
30
|
if (littleEndian !== true)
|
|
30
31
|
throw new Error("only Little Endian supported")
|
|
32
|
+
if (sampleRate <= 0)
|
|
33
|
+
throw new Error("sample rate must be positive")
|
|
34
|
+
if (bitDepth <= 0 || bitDepth % 8 !== 0)
|
|
35
|
+
throw new Error("bit depth must be positive and multiple of 8")
|
|
36
|
+
if (channels <= 0)
|
|
37
|
+
throw new Error("channels must be positive")
|
|
31
38
|
|
|
39
|
+
/* calculate duration */
|
|
32
40
|
const bytesPerSample = bitDepth / 8
|
|
33
41
|
const totalSamples = buffer.length / (bytesPerSample * channels)
|
|
34
42
|
return totalSamples / sampleRate
|
|
@@ -40,12 +48,23 @@ export function audioArrayDuration (
|
|
|
40
48
|
sampleRate = 48000,
|
|
41
49
|
channels = 1
|
|
42
50
|
) {
|
|
51
|
+
/* sanity check parameters */
|
|
52
|
+
if (arr.length === 0)
|
|
53
|
+
return 0
|
|
54
|
+
if (sampleRate <= 0)
|
|
55
|
+
throw new Error("sample rate must be positive")
|
|
56
|
+
if (channels <= 0)
|
|
57
|
+
throw new Error("channels must be positive")
|
|
58
|
+
|
|
59
|
+
/* calculate duration */
|
|
43
60
|
const totalSamples = arr.length / channels
|
|
44
61
|
return totalSamples / sampleRate
|
|
45
62
|
}
|
|
46
63
|
|
|
47
64
|
/* helper function: convert Buffer in PCM/I16 to Float32Array in PCM/F32 format */
|
|
48
65
|
export function convertBufToF32 (buf: Buffer, littleEndian = true) {
|
|
66
|
+
if (buf.length % 2 !== 0)
|
|
67
|
+
throw new Error("buffer length must be even for 16-bit samples")
|
|
49
68
|
const dataView = new DataView(buf.buffer)
|
|
50
69
|
const arr = new Float32Array(buf.length / 2)
|
|
51
70
|
for (let i = 0; i < arr.length; i++)
|
|
@@ -55,9 +74,15 @@ export function convertBufToF32 (buf: Buffer, littleEndian = true) {
|
|
|
55
74
|
|
|
56
75
|
/* helper function: convert Float32Array in PCM/F32 to Buffer in PCM/I16 format */
|
|
57
76
|
export function convertF32ToBuf (arr: Float32Array) {
|
|
77
|
+
if (arr.length === 0)
|
|
78
|
+
return Buffer.alloc(0)
|
|
58
79
|
const int16Array = new Int16Array(arr.length)
|
|
59
|
-
for (let i = 0; i < arr.length; i++)
|
|
60
|
-
|
|
80
|
+
for (let i = 0; i < arr.length; i++) {
|
|
81
|
+
let sample = arr[i]
|
|
82
|
+
if (Number.isNaN(sample))
|
|
83
|
+
sample = 0
|
|
84
|
+
int16Array[i] = Math.max(-32768, Math.min(32767, Math.round(sample * 32768)))
|
|
85
|
+
}
|
|
61
86
|
return Buffer.from(int16Array.buffer)
|
|
62
87
|
}
|
|
63
88
|
|
|
@@ -274,26 +299,19 @@ export class QueuePointer<T extends QueueElement> extends EventEmitter {
|
|
|
274
299
|
}
|
|
275
300
|
position (index?: number): number {
|
|
276
301
|
if (index !== undefined) {
|
|
277
|
-
this.index = index
|
|
278
|
-
if (this.index < 0)
|
|
279
|
-
this.index = 0
|
|
280
|
-
else if (this.index >= this.queue.elements.length)
|
|
281
|
-
this.index = this.queue.elements.length
|
|
302
|
+
this.index = Math.max(0, Math.min(index, this.queue.elements.length))
|
|
282
303
|
this.emit("position", this.index)
|
|
283
304
|
}
|
|
284
305
|
return this.index
|
|
285
306
|
}
|
|
286
307
|
walk (num: number) {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
308
|
+
const indexOld = this.index
|
|
309
|
+
if (num > 0)
|
|
310
|
+
this.index = Math.min(this.index + num, this.queue.elements.length)
|
|
311
|
+
else if (num < 0)
|
|
312
|
+
this.index = Math.max(this.index + num, 0)
|
|
313
|
+
if (this.index !== indexOld)
|
|
290
314
|
this.emit("position", { start: this.index })
|
|
291
|
-
}
|
|
292
|
-
else if (num < 0) {
|
|
293
|
-
for (let i = 0; i < Math.abs(num) && this.index > 0; i++)
|
|
294
|
-
this.index--
|
|
295
|
-
this.emit("position", { start: this.index })
|
|
296
|
-
}
|
|
297
315
|
}
|
|
298
316
|
walkForwardUntil (type: T["type"]) {
|
|
299
317
|
while (this.index < this.queue.elements.length
|
|
@@ -330,12 +348,7 @@ export class QueuePointer<T extends QueueElement> extends EventEmitter {
|
|
|
330
348
|
peek (position?: number) {
|
|
331
349
|
if (position === undefined)
|
|
332
350
|
position = this.index
|
|
333
|
-
|
|
334
|
-
if (position < 0)
|
|
335
|
-
position = 0
|
|
336
|
-
else if (position > this.queue.elements.length)
|
|
337
|
-
position = this.queue.elements.length
|
|
338
|
-
}
|
|
351
|
+
position = Math.max(0, Math.min(position, this.queue.elements.length))
|
|
339
352
|
const element = this.queue.elements[position]
|
|
340
353
|
this.queue.emit("read", { start: position, end: position })
|
|
341
354
|
return element
|
|
@@ -351,11 +364,8 @@ export class QueuePointer<T extends QueueElement> extends EventEmitter {
|
|
|
351
364
|
let slice: T[]
|
|
352
365
|
const start = this.index
|
|
353
366
|
if (size !== undefined) {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
else if (size > this.queue.elements.length - this.index)
|
|
357
|
-
size = this.queue.elements.length - this.index
|
|
358
|
-
slice = this.queue.elements.slice(this.index, size)
|
|
367
|
+
size = Math.max(0, Math.min(size, this.queue.elements.length - this.index))
|
|
368
|
+
slice = this.queue.elements.slice(this.index, this.index + size)
|
|
359
369
|
this.index += size
|
|
360
370
|
}
|
|
361
371
|
else {
|
|
@@ -415,45 +425,58 @@ export class Queue<T extends QueueElement> extends EventEmitter {
|
|
|
415
425
|
min = pointer.position()
|
|
416
426
|
|
|
417
427
|
/* trim the maximum amount of first elements */
|
|
418
|
-
|
|
428
|
+
if (min > 0) {
|
|
429
|
+
this.elements.splice(0, min)
|
|
419
430
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
431
|
+
/* shift all pointers */
|
|
432
|
+
for (const pointer of this.pointers.values())
|
|
433
|
+
pointer.position(pointer.position() - min)
|
|
434
|
+
}
|
|
423
435
|
}
|
|
424
436
|
}
|
|
425
437
|
|
|
426
438
|
/* utility class for wrapping a custom stream into a regular Transform stream */
|
|
427
439
|
export class StreamWrapper extends Stream.Transform {
|
|
428
440
|
private foreignStream: any
|
|
441
|
+
private onData = (chunk: any) => { this.push(chunk) }
|
|
442
|
+
private onError = (err: Error) => { this.emit("error", err) }
|
|
443
|
+
private onEnd = () => { this.push(null) }
|
|
429
444
|
constructor (foreignStream: any, options: Stream.TransformOptions = {}) {
|
|
430
445
|
options.readableObjectMode = true
|
|
431
446
|
options.writableObjectMode = true
|
|
432
447
|
super(options)
|
|
433
448
|
this.foreignStream = foreignStream
|
|
434
|
-
this.foreignStream.on
|
|
435
|
-
this.
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
})
|
|
440
|
-
this.foreignStream.on("end", () => {
|
|
441
|
-
this.push(null)
|
|
442
|
-
})
|
|
449
|
+
if (typeof this.foreignStream.on === "function") {
|
|
450
|
+
this.foreignStream.on("data", this.onData)
|
|
451
|
+
this.foreignStream.on("error", this.onError)
|
|
452
|
+
this.foreignStream.on("end", this.onEnd)
|
|
453
|
+
}
|
|
443
454
|
}
|
|
444
455
|
_transform (chunk: any, encoding: BufferEncoding, callback: Stream.TransformCallback): void {
|
|
456
|
+
if (this.destroyed) {
|
|
457
|
+
callback(new Error("stream already destroyed"))
|
|
458
|
+
return
|
|
459
|
+
}
|
|
445
460
|
try {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
461
|
+
if (typeof this.foreignStream.write === "function") {
|
|
462
|
+
const canContinue = this.foreignStream.write(chunk)
|
|
463
|
+
if (canContinue)
|
|
464
|
+
callback()
|
|
465
|
+
else
|
|
466
|
+
this.foreignStream.once("drain", callback)
|
|
467
|
+
}
|
|
449
468
|
else
|
|
450
|
-
|
|
469
|
+
throw new Error("foreign stream lacks write method")
|
|
451
470
|
}
|
|
452
471
|
catch (err) {
|
|
453
472
|
callback(err as Error)
|
|
454
473
|
}
|
|
455
474
|
}
|
|
456
475
|
_flush (callback: Stream.TransformCallback): void {
|
|
476
|
+
if (this.destroyed) {
|
|
477
|
+
callback(new Error("stream already destroyed"))
|
|
478
|
+
return
|
|
479
|
+
}
|
|
457
480
|
try {
|
|
458
481
|
if (typeof this.foreignStream.end === "function")
|
|
459
482
|
this.foreignStream.end()
|
|
@@ -463,6 +486,14 @@ export class StreamWrapper extends Stream.Transform {
|
|
|
463
486
|
callback(err as Error)
|
|
464
487
|
}
|
|
465
488
|
}
|
|
489
|
+
_destroy (error: Error | null, callback: Stream.TransformCallback): void {
|
|
490
|
+
if (typeof this.foreignStream.removeListener === "function") {
|
|
491
|
+
this.foreignStream.removeListener("data", this.onData)
|
|
492
|
+
this.foreignStream.removeListener("error", this.onError)
|
|
493
|
+
this.foreignStream.removeListener("end", this.onEnd)
|
|
494
|
+
}
|
|
495
|
+
super._destroy(error, callback)
|
|
496
|
+
}
|
|
466
497
|
}
|
|
467
498
|
|
|
468
499
|
/* meta store */
|
|
@@ -485,4 +516,7 @@ export class TimeStore<T> extends EventEmitter {
|
|
|
485
516
|
if (interval.low < before && interval.high < before)
|
|
486
517
|
this.tree.remove(interval)
|
|
487
518
|
}
|
|
519
|
+
clear (): void {
|
|
520
|
+
this.tree = new IntervalTree.IntervalTree<TimeStoreInterval<T>>()
|
|
521
|
+
}
|
|
488
522
|
}
|
package/src/speechflow.ts
CHANGED
|
@@ -176,6 +176,21 @@ type wsPeerInfo = {
|
|
|
176
176
|
logPrefix: pkg.name
|
|
177
177
|
})
|
|
178
178
|
|
|
179
|
+
/* catch uncaught exceptions */
|
|
180
|
+
process.on("uncaughtException", (err) => {
|
|
181
|
+
cli!.log("error", `uncaught exception: ${err.message}`)
|
|
182
|
+
process.exit(1)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
/* catch unhandled promise rejections */
|
|
186
|
+
process.on("unhandledRejection", (reason) => {
|
|
187
|
+
if (reason instanceof Error)
|
|
188
|
+
cli!.log("error", `unhandled rejection: ${reason.message}`)
|
|
189
|
+
else
|
|
190
|
+
cli!.log("error", `unhandled rejection: ${reason}`)
|
|
191
|
+
process.exit(1)
|
|
192
|
+
})
|
|
193
|
+
|
|
179
194
|
/* provide startup information */
|
|
180
195
|
cli.log("info", `starting SpeechFlow ${pkg["x-stdver"]} (${pkg["x-release"]})`)
|
|
181
196
|
|
|
@@ -185,21 +200,6 @@ type wsPeerInfo = {
|
|
|
185
200
|
for (const key of Object.keys(result.parsed))
|
|
186
201
|
cli.log("info", `loaded environment variable "${key}" from ".env" files`)
|
|
187
202
|
|
|
188
|
-
/* handle uncaught exceptions */
|
|
189
|
-
process.on("uncaughtException", async (err: Error) => {
|
|
190
|
-
cli!.log("warning", `process crashed with a fatal error: ${err} ${err.stack}`)
|
|
191
|
-
process.exit(1)
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
/* handle unhandled promise rejections */
|
|
195
|
-
process.on("unhandledRejection", async (reason, promise) => {
|
|
196
|
-
if (reason instanceof Error)
|
|
197
|
-
cli!.log("error", `promise rejection not handled: ${reason.message}: ${reason.stack}`)
|
|
198
|
-
else
|
|
199
|
-
cli!.log("error", `promise rejection not handled: ${reason}`)
|
|
200
|
-
process.exit(1)
|
|
201
|
-
})
|
|
202
|
-
|
|
203
203
|
/* sanity check usage */
|
|
204
204
|
let n = 0
|
|
205
205
|
if (typeof args.e === "string" && args.e !== "") n++
|
|
@@ -220,7 +220,16 @@ type wsPeerInfo = {
|
|
|
220
220
|
throw new Error("invalid configuration file specification (expected \"<id>@<yaml-config-file>\")")
|
|
221
221
|
const [ , id, file ] = m
|
|
222
222
|
const yaml = await cli.input(file, { encoding: "utf8" })
|
|
223
|
-
|
|
223
|
+
let obj: any
|
|
224
|
+
try {
|
|
225
|
+
obj = jsYAML.load(yaml)
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
if (err instanceof Error)
|
|
229
|
+
throw new Error(`failed to parse YAML configuration: ${err.message}`)
|
|
230
|
+
else
|
|
231
|
+
throw new Error(`failed to parse YAML configuration: ${err}`)
|
|
232
|
+
}
|
|
224
233
|
if (obj[id] === undefined)
|
|
225
234
|
throw new Error(`no such id "${id}" found in configuration file`)
|
|
226
235
|
config = obj[id] as string
|
|
@@ -308,7 +317,14 @@ type wsPeerInfo = {
|
|
|
308
317
|
for (const name of Object.keys(nodes)) {
|
|
309
318
|
cli!.log("info", `gathering status of node <${name}>`)
|
|
310
319
|
const node = new nodes[name](name, cfg, {}, [])
|
|
311
|
-
const status = await
|
|
320
|
+
const status = await Promise.race<{ [ key: string ]: string | number }>([
|
|
321
|
+
node.status(),
|
|
322
|
+
new Promise((resolve, reject) => setTimeout(() =>
|
|
323
|
+
reject(new Error("timeout")), 10 * 1000))
|
|
324
|
+
]).catch((err: Error) => {
|
|
325
|
+
cli!.log("warning", `[${node.id}]: failed to gather status of node <${node.id}>: ${err.message}`)
|
|
326
|
+
return {} as { [ key: string ]: string | number }
|
|
327
|
+
})
|
|
312
328
|
if (Object.keys(status).length > 0) {
|
|
313
329
|
let first = true
|
|
314
330
|
for (const key of Object.keys(status)) {
|
|
@@ -337,9 +353,9 @@ type wsPeerInfo = {
|
|
|
337
353
|
}
|
|
338
354
|
catch (err) {
|
|
339
355
|
if (err instanceof Error && err.name === "FlowLinkError")
|
|
340
|
-
cli!.log("error", `failed to parse SpeechFlow configuration: ${err.toString()}
|
|
356
|
+
cli!.log("error", `failed to parse SpeechFlow configuration: ${err.toString()}`)
|
|
341
357
|
else if (err instanceof Error)
|
|
342
|
-
cli!.log("error", `failed to parse SpeechFlow configuration: ${err.message}
|
|
358
|
+
cli!.log("error", `failed to parse SpeechFlow configuration: ${err.message}`)
|
|
343
359
|
else
|
|
344
360
|
cli!.log("error", "failed to parse SpeechFlow configuration: internal error")
|
|
345
361
|
process.exit(1)
|
|
@@ -438,9 +454,13 @@ type wsPeerInfo = {
|
|
|
438
454
|
/* open node */
|
|
439
455
|
cli!.log("info", `open node <${node.id}>`)
|
|
440
456
|
node.setTimeZero(timeZero)
|
|
441
|
-
await
|
|
442
|
-
|
|
443
|
-
|
|
457
|
+
await Promise.race<void>([
|
|
458
|
+
node.open(),
|
|
459
|
+
new Promise((resolve, reject) => setTimeout(() =>
|
|
460
|
+
reject(new Error("timeout")), 10 * 1000))
|
|
461
|
+
]).catch((err: Error) => {
|
|
462
|
+
cli!.log("error", `[${node.id}]: failed to open node <${node.id}>: ${err.message}`)
|
|
463
|
+
throw new Error(`failed to open node <${node.id}>: ${err.message}`)
|
|
444
464
|
})
|
|
445
465
|
}
|
|
446
466
|
|
|
@@ -462,9 +482,10 @@ type wsPeerInfo = {
|
|
|
462
482
|
}
|
|
463
483
|
}
|
|
464
484
|
|
|
465
|
-
/* graph processing: PASS
|
|
485
|
+
/* graph processing: PASS 5: track stream finishing */
|
|
466
486
|
const activeNodes = new Set<SpeechFlowNode>()
|
|
467
487
|
const finishEvents = new EventEmitter()
|
|
488
|
+
finishEvents.setMaxListeners(graphNodes.size + 10)
|
|
468
489
|
for (const node of graphNodes) {
|
|
469
490
|
if (node.stream === null)
|
|
470
491
|
throw new Error(`stream of node <${node.id}> still not initialized`)
|
|
@@ -507,12 +528,17 @@ type wsPeerInfo = {
|
|
|
507
528
|
const name = req.node as string
|
|
508
529
|
const args = req.args as any[]
|
|
509
530
|
const foundNode = Array.from(graphNodes).find((node) => node.id === name)
|
|
510
|
-
if (foundNode === undefined)
|
|
531
|
+
if (foundNode === undefined) {
|
|
511
532
|
cli!.log("warning", `external request failed: no such node <${name}>`)
|
|
533
|
+
throw new Error(`external request failed: no such node <${name}>`)
|
|
534
|
+
}
|
|
512
535
|
else {
|
|
513
|
-
await
|
|
514
|
-
|
|
515
|
-
|
|
536
|
+
await Promise.race<void>([
|
|
537
|
+
foundNode.receiveRequest(args),
|
|
538
|
+
new Promise((resolve, reject) => setTimeout(() =>
|
|
539
|
+
reject(new Error("timeout")), 10 * 1000))
|
|
540
|
+
]).catch((err: Error) => {
|
|
541
|
+
cli!.log("warning", `external request to node <${name}> failed: ${err.message}`)
|
|
516
542
|
})
|
|
517
543
|
}
|
|
518
544
|
}
|
|
@@ -621,7 +647,7 @@ type wsPeerInfo = {
|
|
|
621
647
|
}
|
|
622
648
|
})
|
|
623
649
|
await hapi.start()
|
|
624
|
-
cli!.log("info", `HAPI: started REST/WebSocket network service: http://${args.
|
|
650
|
+
cli!.log("info", `HAPI: started REST/WebSocket network service: http://${args.a}:${args.p}`)
|
|
625
651
|
|
|
626
652
|
/* hook for sendResponse method of nodes */
|
|
627
653
|
for (const node of graphNodes) {
|
|
@@ -629,7 +655,8 @@ type wsPeerInfo = {
|
|
|
629
655
|
const data = JSON.stringify({ response: "NOTIFY", node: node.id, args })
|
|
630
656
|
for (const [ peer, info ] of wsPeers.entries()) {
|
|
631
657
|
cli!.log("info", `HAPI: peer ${peer}: ${data}`)
|
|
632
|
-
info.ws.
|
|
658
|
+
if (info.ws.readyState === WebSocket.OPEN)
|
|
659
|
+
info.ws.send(data)
|
|
633
660
|
}
|
|
634
661
|
})
|
|
635
662
|
}
|
|
@@ -651,9 +678,39 @@ type wsPeerInfo = {
|
|
|
651
678
|
cli!.log("warning", `**** received signal ${signal} -- shutting down service ****`)
|
|
652
679
|
|
|
653
680
|
/* shutdown HAPI service */
|
|
654
|
-
cli!.log("info", `HAPI: stopping REST/WebSocket network service: http://${args.
|
|
681
|
+
cli!.log("info", `HAPI: stopping REST/WebSocket network service: http://${args.a}:${args.p}`)
|
|
655
682
|
await hapi.stop({ timeout: 2000 })
|
|
656
683
|
|
|
684
|
+
/* clear WebSocket connections */
|
|
685
|
+
if (wsPeers.size > 0) {
|
|
686
|
+
cli!.log("info", "HAPI: closing WebSocket connections")
|
|
687
|
+
const closePromises: Promise<void>[] = []
|
|
688
|
+
for (const [ peer, info ] of wsPeers.entries()) {
|
|
689
|
+
closePromises.push(new Promise<void>((resolve, reject) => {
|
|
690
|
+
if (info.ws.readyState !== WebSocket.OPEN)
|
|
691
|
+
resolve()
|
|
692
|
+
else {
|
|
693
|
+
const timeout = setTimeout(() => {
|
|
694
|
+
reject(new Error(`timeout for peer ${peer}`))
|
|
695
|
+
}, 2 * 1000)
|
|
696
|
+
info.ws.once("close", () => {
|
|
697
|
+
clearTimeout(timeout)
|
|
698
|
+
resolve()
|
|
699
|
+
})
|
|
700
|
+
info.ws.close()
|
|
701
|
+
}
|
|
702
|
+
}))
|
|
703
|
+
}
|
|
704
|
+
await Promise.race([
|
|
705
|
+
Promise.all(closePromises),
|
|
706
|
+
new Promise((resolve, reject) =>
|
|
707
|
+
setTimeout(() => reject(new Error("timeout for all peers")), 5 * 1000))
|
|
708
|
+
]).catch((err) => {
|
|
709
|
+
cli!.log("warning", `HAPI: WebSockets failed to close: ${err}`)
|
|
710
|
+
})
|
|
711
|
+
wsPeers.clear()
|
|
712
|
+
}
|
|
713
|
+
|
|
657
714
|
/* graph processing: PASS 1: disconnect node streams */
|
|
658
715
|
for (const node of graphNodes) {
|
|
659
716
|
if (node.stream === null) {
|
|
@@ -683,8 +740,12 @@ type wsPeerInfo = {
|
|
|
683
740
|
/* graph processing: PASS 2: close nodes */
|
|
684
741
|
for (const node of graphNodes) {
|
|
685
742
|
cli!.log("info", `close node <${node.id}>`)
|
|
686
|
-
await
|
|
687
|
-
|
|
743
|
+
await Promise.race<void>([
|
|
744
|
+
node.close(),
|
|
745
|
+
new Promise((resolve, reject) => setTimeout(() =>
|
|
746
|
+
reject(new Error("timeout")), 10 * 1000))
|
|
747
|
+
]).catch((err: Error) => {
|
|
748
|
+
cli!.log("warning", `node <${node.id}> failed to close: ${err.message}`)
|
|
688
749
|
})
|
|
689
750
|
}
|
|
690
751
|
|
|
@@ -703,6 +764,12 @@ type wsPeerInfo = {
|
|
|
703
764
|
graphNodes.delete(node)
|
|
704
765
|
}
|
|
705
766
|
|
|
767
|
+
/* clear event emitters */
|
|
768
|
+
finishEvents.removeAllListeners()
|
|
769
|
+
|
|
770
|
+
/* clear active nodes */
|
|
771
|
+
activeNodes.clear()
|
|
772
|
+
|
|
706
773
|
/* terminate process */
|
|
707
774
|
if (signal === "finished") {
|
|
708
775
|
cli!.log("info", "terminate process (exit code 0)")
|
|
@@ -713,24 +780,38 @@ type wsPeerInfo = {
|
|
|
713
780
|
process.exit(1)
|
|
714
781
|
}
|
|
715
782
|
}
|
|
783
|
+
|
|
784
|
+
/* hook into regular finish */
|
|
716
785
|
finishEvents.on("finished", () => { shutdown("finished") })
|
|
786
|
+
|
|
787
|
+
/* hook into process signals */
|
|
717
788
|
process.on("SIGINT", () => { shutdown("SIGINT") })
|
|
718
789
|
process.on("SIGUSR1", () => { shutdown("SIGUSR1") })
|
|
719
790
|
process.on("SIGUSR2", () => { shutdown("SIGUSR2") })
|
|
720
791
|
process.on("SIGTERM", () => { shutdown("SIGTERM") })
|
|
792
|
+
|
|
793
|
+
/* re-hook into uncaught exception handler */
|
|
794
|
+
process.removeAllListeners("uncaughtException")
|
|
721
795
|
process.on("uncaughtException", (err) => {
|
|
722
|
-
cli!.log("error", `uncaught exception: ${err}`)
|
|
796
|
+
cli!.log("error", `uncaught exception: ${err.message}`)
|
|
723
797
|
shutdown("exception")
|
|
724
798
|
})
|
|
799
|
+
|
|
800
|
+
/* re-hook into unhandled promise rejection handler */
|
|
801
|
+
process.removeAllListeners("unhandledRejection")
|
|
725
802
|
process.on("unhandledRejection", (reason) => {
|
|
726
|
-
|
|
803
|
+
if (reason instanceof Error)
|
|
804
|
+
cli!.log("error", `unhandled rejection: ${reason.message}`)
|
|
805
|
+
else
|
|
806
|
+
cli!.log("error", `unhandled rejection: ${reason}`)
|
|
727
807
|
shutdown("exception")
|
|
728
808
|
})
|
|
729
809
|
})().catch((err: Error) => {
|
|
810
|
+
/* top-level exception handling */
|
|
730
811
|
if (cli !== null)
|
|
731
|
-
cli.log("error", err.message)
|
|
812
|
+
cli.log("error", `${err.message}:\n${err.stack}`)
|
|
732
813
|
else
|
|
733
|
-
process.stderr.write(`${pkg.name}: ${chalk.red("ERROR")}: ${err.message}
|
|
814
|
+
process.stderr.write(`${pkg.name}: ${chalk.red("ERROR")}: ${err.message}\n${err.stack}\n`)
|
|
734
815
|
process.exit(1)
|
|
735
816
|
})
|
|
736
817
|
|