aqualink 3.1.0 → 3.2.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/build/handlers/autoplay.js +67 -66
- package/build/index.d.ts +170 -133
- package/build/index.js +3 -3
- package/build/structures/Aqua.js +65 -41
- package/build/structures/AquaRecovery.js +41 -10
- package/build/structures/AqualinkEvents.js +2 -0
- package/build/structures/Connection.js +9 -1
- package/build/structures/ConnectionRecovery.js +3 -38
- package/build/structures/Node.js +158 -80
- package/build/structures/Player.js +54 -23
- package/build/structures/PlayerLifecycle.js +10 -1
- package/build/structures/Reporting.js +12 -0
- package/build/structures/Rest.js +48 -42
- package/build/structures/Track.js +10 -3
- package/package.json +70 -70
package/build/structures/Node.js
CHANGED
|
@@ -5,6 +5,7 @@ const WebSocketImpl = process.isBun ? globalThis.WebSocket : require('ws')
|
|
|
5
5
|
|
|
6
6
|
const Rest = require('./Rest')
|
|
7
7
|
const { AqualinkEvents } = require('./AqualinkEvents')
|
|
8
|
+
const { emitOperationalError } = require('./Reporting')
|
|
8
9
|
|
|
9
10
|
const privateData = new WeakMap()
|
|
10
11
|
|
|
@@ -36,6 +37,8 @@ const unrefTimer = (t) => {
|
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
const _functions = {
|
|
40
|
+
noop() {},
|
|
41
|
+
|
|
39
42
|
buildWsUrl(host, port, ssl) {
|
|
40
43
|
const needsBrackets = host.includes(':') && !host.startsWith('[')
|
|
41
44
|
return `ws${ssl ? 's' : ''}://${needsBrackets ? `[${host}]` : host}:${port}${WS_PATH}`
|
|
@@ -138,6 +141,7 @@ class Node {
|
|
|
138
141
|
this.reconnectTimeoutId = null
|
|
139
142
|
this.isDestroyed = false
|
|
140
143
|
this._isConnecting = false
|
|
144
|
+
this._connectPromise = null
|
|
141
145
|
this.isNodelink = false
|
|
142
146
|
|
|
143
147
|
this._wsIsBun = !!process.isBun
|
|
@@ -162,7 +166,7 @@ class Node {
|
|
|
162
166
|
error: this._handleError.bind(this),
|
|
163
167
|
message: this._handleMessage.bind(this),
|
|
164
168
|
close: this._handleClose.bind(this),
|
|
165
|
-
connect: this.connect.
|
|
169
|
+
connect: () => this.connect().catch(() => {})
|
|
166
170
|
}
|
|
167
171
|
})
|
|
168
172
|
}
|
|
@@ -307,7 +311,7 @@ class Node {
|
|
|
307
311
|
if (
|
|
308
312
|
code !== Node.WS_CLOSE_NORMAL &&
|
|
309
313
|
code !== 1001 &&
|
|
310
|
-
|
|
314
|
+
isFatal &&
|
|
311
315
|
this.sessionId
|
|
312
316
|
) {
|
|
313
317
|
this._clearSession()
|
|
@@ -393,16 +397,17 @@ class Node {
|
|
|
393
397
|
}
|
|
394
398
|
|
|
395
399
|
connect() {
|
|
396
|
-
if (this.isDestroyed
|
|
400
|
+
if (this.isDestroyed) return Promise.reject(new Error('Node is destroyed'))
|
|
401
|
+
if (this._connectPromise) return this._connectPromise
|
|
397
402
|
|
|
398
403
|
const state = this.ws?.readyState
|
|
399
404
|
if (state === WS_STATES.OPEN) {
|
|
400
405
|
this._emitDebug('WebSocket already connected')
|
|
401
|
-
return
|
|
406
|
+
return Promise.resolve()
|
|
402
407
|
}
|
|
403
408
|
if (state === WS_STATES.CONNECTING || state === WS_STATES.CLOSING) {
|
|
404
409
|
this._emitDebug('WebSocket is connecting/closing; skipping new connect')
|
|
405
|
-
return
|
|
410
|
+
return Promise.reject(new Error('WebSocket is closing'))
|
|
406
411
|
}
|
|
407
412
|
|
|
408
413
|
this._isConnecting = true
|
|
@@ -415,95 +420,165 @@ class Node {
|
|
|
415
420
|
})
|
|
416
421
|
}
|
|
417
422
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
423
|
+
let connectPromise
|
|
424
|
+
connectPromise = new Promise((resolve, reject) => {
|
|
425
|
+
let settled = false
|
|
426
|
+
let opened = false
|
|
427
|
+
let timer = null
|
|
428
|
+
const settle = (ok, error) => {
|
|
429
|
+
if (settled) return
|
|
430
|
+
settled = true
|
|
431
|
+
if (timer) clearTimeout(timer)
|
|
432
|
+
queueMicrotask(() => {
|
|
433
|
+
if (this._connectPromise === connectPromise)
|
|
434
|
+
this._connectPromise = null
|
|
435
|
+
})
|
|
436
|
+
this._rejectConnect = null
|
|
437
|
+
ok ? resolve() : reject(error)
|
|
438
|
+
}
|
|
439
|
+
timer = setTimeout(() => {
|
|
440
|
+
const error = new Error(`WebSocket open timeout: ${this.timeout}ms`)
|
|
441
|
+
this._isConnecting = false
|
|
442
|
+
this._cleanup()
|
|
443
|
+
settle(false, error)
|
|
444
|
+
this._scheduleReconnect()
|
|
445
|
+
}, this.timeout)
|
|
446
|
+
unrefTimer(timer)
|
|
447
|
+
this._rejectConnect = (error) => settle(false, error)
|
|
448
|
+
|
|
449
|
+
try {
|
|
450
|
+
const h = this._boundHandlers
|
|
451
|
+
const onOpen = () => {
|
|
452
|
+
opened = true
|
|
453
|
+
h.open()
|
|
454
|
+
settle(true)
|
|
455
|
+
}
|
|
456
|
+
const onError = (error) => {
|
|
457
|
+
h.error(error)
|
|
458
|
+
if (!opened) {
|
|
459
|
+
this._isConnecting = false
|
|
460
|
+
this._cleanup()
|
|
461
|
+
settle(false, error)
|
|
462
|
+
this._scheduleReconnect()
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
const onClose = (code, reason) => {
|
|
466
|
+
if (!opened) {
|
|
467
|
+
settle(
|
|
468
|
+
false,
|
|
469
|
+
new Error(
|
|
470
|
+
`WebSocket closed before open (code ${code}): ${_functions.reasonToString(reason)}`
|
|
471
|
+
)
|
|
472
|
+
)
|
|
473
|
+
}
|
|
474
|
+
h.close(code, reason)
|
|
441
475
|
}
|
|
442
476
|
|
|
443
|
-
|
|
477
|
+
if (this._wsIsBun) {
|
|
478
|
+
const ws = new WebSocketImpl(this.wsUrl, { headers: this._headers })
|
|
479
|
+
ws.binaryType = 'arraybuffer'
|
|
480
|
+
|
|
481
|
+
const offs = []
|
|
482
|
+
const add = (type, fn, once = false) => {
|
|
483
|
+
const wrapped = once
|
|
484
|
+
? (ev) => {
|
|
485
|
+
try {
|
|
486
|
+
ws.removeEventListener(type, wrapped)
|
|
487
|
+
} catch {}
|
|
488
|
+
fn(ev)
|
|
489
|
+
}
|
|
490
|
+
: fn
|
|
491
|
+
ws.addEventListener(type, wrapped)
|
|
492
|
+
offs.push(() => {
|
|
493
|
+
try {
|
|
494
|
+
ws.removeEventListener(type, wrapped)
|
|
495
|
+
} catch {}
|
|
496
|
+
})
|
|
497
|
+
}
|
|
444
498
|
|
|
445
|
-
|
|
446
|
-
'error',
|
|
447
|
-
(event) => {
|
|
448
|
-
const err = event?.error
|
|
449
|
-
h.error(err instanceof Error ? err : new Error('WebSocket error'))
|
|
450
|
-
},
|
|
451
|
-
true
|
|
452
|
-
)
|
|
499
|
+
add('open', onOpen, true)
|
|
453
500
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
501
|
+
add(
|
|
502
|
+
'error',
|
|
503
|
+
(event) => {
|
|
504
|
+
const err = event?.error
|
|
505
|
+
onError(err instanceof Error ? err : new Error('WebSocket error'))
|
|
506
|
+
},
|
|
507
|
+
false
|
|
508
|
+
)
|
|
459
509
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
h.
|
|
464
|
-
|
|
465
|
-
? event.code
|
|
466
|
-
: Node.WS_CLOSE_NORMAL,
|
|
467
|
-
typeof event?.reason === 'string' ? event.reason : ''
|
|
468
|
-
)
|
|
469
|
-
},
|
|
470
|
-
true
|
|
471
|
-
)
|
|
510
|
+
add('message', (event) => {
|
|
511
|
+
const data = event?.data
|
|
512
|
+
if (typeof data === 'string') h.message(data, false)
|
|
513
|
+
else h.message(data, true)
|
|
514
|
+
})
|
|
472
515
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
516
|
+
add(
|
|
517
|
+
'close',
|
|
518
|
+
(event) => {
|
|
519
|
+
onClose(
|
|
520
|
+
typeof event?.code === 'number'
|
|
521
|
+
? event.code
|
|
522
|
+
: Node.WS_CLOSE_NORMAL,
|
|
523
|
+
typeof event?.reason === 'string' ? event.reason : ''
|
|
524
|
+
)
|
|
525
|
+
},
|
|
526
|
+
true
|
|
527
|
+
)
|
|
479
528
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
})
|
|
529
|
+
this._bunCleanup = () => {
|
|
530
|
+
for (let i = 0; i < offs.length; i++) offs[i]()
|
|
531
|
+
}
|
|
532
|
+
this.ws = ws
|
|
533
|
+
return
|
|
534
|
+
}
|
|
487
535
|
|
|
488
|
-
|
|
536
|
+
const ws = new WebSocketImpl(this.wsUrl, {
|
|
537
|
+
headers: this._headers,
|
|
538
|
+
perMessageDeflate: false,
|
|
539
|
+
handshakeTimeout: this.timeout,
|
|
540
|
+
maxPayload: this.maxPayload,
|
|
541
|
+
skipUTF8Validation: this.skipUTF8Validation
|
|
542
|
+
})
|
|
489
543
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
544
|
+
ws.once('open', () => {
|
|
545
|
+
onOpen()
|
|
546
|
+
this._wsPingInterval = setInterval(() => {
|
|
547
|
+
if (ws.readyState === WS_STATES.OPEN) ws.ping?.()
|
|
548
|
+
}, 30000)
|
|
549
|
+
this._wsPingInterval.unref?.()
|
|
550
|
+
})
|
|
551
|
+
ws.on('error', onError)
|
|
552
|
+
ws.on('message', h.message)
|
|
553
|
+
ws.once('close', (...args) => {
|
|
554
|
+
if (this._wsPingInterval) {
|
|
555
|
+
clearInterval(this._wsPingInterval)
|
|
556
|
+
this._wsPingInterval = null
|
|
557
|
+
}
|
|
558
|
+
onClose(...args)
|
|
559
|
+
})
|
|
494
560
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
561
|
+
this.ws = ws
|
|
562
|
+
} catch (err) {
|
|
563
|
+
this._isConnecting = false
|
|
564
|
+
this._emitError(`Failed to create WebSocket: ${_functions.errMsg(err)}`)
|
|
565
|
+
settle(false, err)
|
|
566
|
+
this._scheduleReconnect()
|
|
567
|
+
}
|
|
568
|
+
})
|
|
569
|
+
this._connectPromise = connectPromise
|
|
570
|
+
return connectPromise
|
|
501
571
|
}
|
|
502
572
|
|
|
503
573
|
_cleanup() {
|
|
504
574
|
const ws = this.ws
|
|
505
575
|
if (!ws) return
|
|
506
576
|
|
|
577
|
+
if (this._wsPingInterval) {
|
|
578
|
+
clearInterval(this._wsPingInterval)
|
|
579
|
+
this._wsPingInterval = null
|
|
580
|
+
}
|
|
581
|
+
|
|
507
582
|
if (this._wsIsBun) {
|
|
508
583
|
try {
|
|
509
584
|
this._bunCleanup?.()
|
|
@@ -511,6 +586,7 @@ class Node {
|
|
|
511
586
|
this._bunCleanup = null
|
|
512
587
|
} else {
|
|
513
588
|
ws.removeAllListeners?.()
|
|
589
|
+
ws.on?.('error', _functions.noop)
|
|
514
590
|
}
|
|
515
591
|
|
|
516
592
|
try {
|
|
@@ -531,15 +607,17 @@ class Node {
|
|
|
531
607
|
if (this.isDestroyed) return
|
|
532
608
|
|
|
533
609
|
this.isDestroyed = true
|
|
610
|
+
this._rejectConnect?.(new Error('Node destroyed while connecting'))
|
|
534
611
|
this.state = NODE_STATE.IDLE
|
|
535
612
|
this._isConnecting = false
|
|
613
|
+
this._connectPromise = null
|
|
536
614
|
this._clearReconnectTimeout()
|
|
537
615
|
this._cleanup()
|
|
538
616
|
|
|
539
617
|
if (!clean) this.aqua.handleNodeFailover?.(this)
|
|
540
618
|
|
|
541
619
|
this.connected = false
|
|
542
|
-
this.aqua.
|
|
620
|
+
this.aqua._destroyNode?.(this.name)
|
|
543
621
|
this.aqua.emit(AqualinkEvents.NodeDestroy, this)
|
|
544
622
|
|
|
545
623
|
this.rest?.destroy?.()
|
|
@@ -792,7 +870,7 @@ class Node {
|
|
|
792
870
|
|
|
793
871
|
_emitError(error) {
|
|
794
872
|
const errorObj = error instanceof Error ? error : new Error(String(error))
|
|
795
|
-
this.aqua
|
|
873
|
+
emitOperationalError(this.aqua, this, errorObj)
|
|
796
874
|
}
|
|
797
875
|
|
|
798
876
|
_emitDebug(message) {
|
|
@@ -35,6 +35,7 @@ const EVENT_HANDLERS = Object.freeze({
|
|
|
35
35
|
PlayerDestroyedEvent: 'playerDestroyed',
|
|
36
36
|
LyricsNotFoundEvent: 'lyricsNotFound',
|
|
37
37
|
MixStartedEvent: 'mixStarted',
|
|
38
|
+
PlayerReconnectingEvent: 'PlayerReconnectingEvent',
|
|
38
39
|
MixEndedEvent: 'mixEnded'
|
|
39
40
|
})
|
|
40
41
|
|
|
@@ -957,22 +958,24 @@ class Player extends EventEmitter {
|
|
|
957
958
|
const prev = this.previous
|
|
958
959
|
const info = prev?.info
|
|
959
960
|
if (!info?.sourceName || !info.identifier) return this
|
|
960
|
-
const { sourceName, identifier, uri, author } = info
|
|
961
|
+
const { sourceName, identifier, uri, author, title } = info
|
|
961
962
|
this.isAutoplay = true
|
|
962
963
|
|
|
963
|
-
if (sourceName === 'spotify'
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
this.previousIdentifiers.
|
|
967
|
-
this.previousIdentifiers.
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
if (!this.autoplaySeed) {
|
|
971
|
-
this.autoplaySeed = {
|
|
972
|
-
trackId: identifier,
|
|
973
|
-
artistIds: Array.isArray(author) ? author.join(',') : author
|
|
964
|
+
if (sourceName === 'spotify') {
|
|
965
|
+
if (info.identifier && info.identifier !== 'local') {
|
|
966
|
+
this.previousIdentifiers.add(info.identifier)
|
|
967
|
+
if (this.previousIdentifiers.size > PREVIOUS_IDS_MAX) {
|
|
968
|
+
this.previousIdentifiers.delete(
|
|
969
|
+
this.previousIdentifiers.values().next().value
|
|
970
|
+
)
|
|
974
971
|
}
|
|
975
972
|
}
|
|
973
|
+
this.autoplaySeed = {
|
|
974
|
+
trackId: identifier,
|
|
975
|
+
isrc: prev.isrc || null,
|
|
976
|
+
title: title || null,
|
|
977
|
+
author: author || null
|
|
978
|
+
}
|
|
976
979
|
}
|
|
977
980
|
|
|
978
981
|
for (
|
|
@@ -1020,27 +1023,45 @@ class Player extends EventEmitter {
|
|
|
1020
1023
|
}
|
|
1021
1024
|
|
|
1022
1025
|
async _getAutoplayTrack(sourceName, identifier, uri, requester) {
|
|
1026
|
+
const seen = new Set(this.previousIdentifiers)
|
|
1027
|
+
const prevId = this.current?.identifier
|
|
1028
|
+
if (prevId) seen.add(prevId)
|
|
1029
|
+
|
|
1023
1030
|
if (sourceName === 'youtube' || sourceName === 'ytmusic') {
|
|
1024
1031
|
const res = await this.aqua.resolve({
|
|
1025
1032
|
query: `https://www.youtube.com/watch?v=${identifier}&list=RD${identifier}`,
|
|
1026
1033
|
source: 'ytmsearch',
|
|
1027
1034
|
requester
|
|
1028
1035
|
})
|
|
1029
|
-
|
|
1030
|
-
|
|
1036
|
+
if (_functions.isInvalidLoad(res) || !res.tracks?.length) return null
|
|
1037
|
+
const candidates = res.tracks.filter(
|
|
1038
|
+
(t) => t.identifier && !seen.has(t.identifier)
|
|
1039
|
+
)
|
|
1040
|
+
return candidates.length
|
|
1041
|
+
? candidates[_functions.randIdx(candidates.length)]
|
|
1031
1042
|
: res.tracks[_functions.randIdx(res.tracks.length)]
|
|
1032
1043
|
}
|
|
1033
1044
|
if (sourceName === 'soundcloud') {
|
|
1034
|
-
const scRes = await scAutoPlay(uri)
|
|
1045
|
+
const scRes = await scAutoPlay(uri, this.nodes?.rest?._autoplayAgent)
|
|
1035
1046
|
if (!scRes?.length) return null
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1047
|
+
|
|
1048
|
+
let anyValid = null
|
|
1049
|
+
for (const link of scRes) {
|
|
1050
|
+
const res = await this.aqua.resolve({
|
|
1051
|
+
query: link,
|
|
1052
|
+
source: 'scsearch',
|
|
1053
|
+
requester
|
|
1054
|
+
})
|
|
1055
|
+
if (_functions.isInvalidLoad(res) || !res.tracks?.length) continue
|
|
1056
|
+
if (!anyValid)
|
|
1057
|
+
anyValid = res.tracks[_functions.randIdx(res.tracks.length)]
|
|
1058
|
+
const candidates = res.tracks.filter(
|
|
1059
|
+
(t) => t.identifier && !seen.has(t.identifier)
|
|
1060
|
+
)
|
|
1061
|
+
if (candidates.length)
|
|
1062
|
+
return candidates[_functions.randIdx(candidates.length)]
|
|
1063
|
+
}
|
|
1064
|
+
return anyValid
|
|
1044
1065
|
}
|
|
1045
1066
|
if (sourceName === 'spotify') {
|
|
1046
1067
|
const res = await spAutoPlay(
|
|
@@ -1171,6 +1192,16 @@ class Player extends EventEmitter {
|
|
|
1171
1192
|
mixEnded(_p, t, payload) {
|
|
1172
1193
|
_functions.emitIfActive(this, AqualinkEvents.MixEnded, t, payload)
|
|
1173
1194
|
}
|
|
1195
|
+
PlayerReconnectingEvent(_p, _t, payload) {
|
|
1196
|
+
this._resuming = true
|
|
1197
|
+
_functions.emitIfActive(
|
|
1198
|
+
this,
|
|
1199
|
+
AqualinkEvents.PlayerReconnectingEvent,
|
|
1200
|
+
payload
|
|
1201
|
+
)
|
|
1202
|
+
this.connection?.resendVoiceUpdate?.(true)
|
|
1203
|
+
this.aqua.emit(AqualinkEvents.PlayerReconnect, this, { resuming: true })
|
|
1204
|
+
}
|
|
1174
1205
|
|
|
1175
1206
|
async _attemptVoiceResume(abortSignal) {
|
|
1176
1207
|
return this._lifecycleController.attemptVoiceResume(abortSignal)
|
|
@@ -241,6 +241,11 @@ class PlayerLifecycle {
|
|
|
241
241
|
}
|
|
242
242
|
|
|
243
243
|
const code = payload?.code
|
|
244
|
+
if (code === 4014) {
|
|
245
|
+
await player._delay(150)
|
|
246
|
+
if (player.destroyed) return
|
|
247
|
+
}
|
|
248
|
+
|
|
244
249
|
if (code === 4006 && player._resuming) {
|
|
245
250
|
if (player.aqua?.debugTrace) {
|
|
246
251
|
player.aqua._trace('player.socketClosed.ignored', {
|
|
@@ -253,7 +258,10 @@ class PlayerLifecycle {
|
|
|
253
258
|
}
|
|
254
259
|
|
|
255
260
|
let isRecoverable = [4015, 4009, 4006, 4014, 4022].includes(code)
|
|
256
|
-
if (
|
|
261
|
+
if (
|
|
262
|
+
code === 4014 &&
|
|
263
|
+
(player.connection?.isWaitingForDisconnect || !player.voiceChannel)
|
|
264
|
+
)
|
|
257
265
|
isRecoverable = false
|
|
258
266
|
|
|
259
267
|
if (code === 4015 && !player.nodes?.info?.isNodelink) {
|
|
@@ -452,6 +460,7 @@ class PlayerLifecycle {
|
|
|
452
460
|
if (
|
|
453
461
|
latestActivePlayer &&
|
|
454
462
|
latestActivePlayer !== player &&
|
|
463
|
+
latestActivePlayer !== np &&
|
|
455
464
|
!latestActivePlayer.destroyed
|
|
456
465
|
) {
|
|
457
466
|
try {
|
|
@@ -26,7 +26,19 @@ function reportSuppressedError(target, scope, error, data = null) {
|
|
|
26
26
|
return err
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
function emitOperationalError(target, source, error) {
|
|
30
|
+
const aqua = getAqua(target)
|
|
31
|
+
const err = normalizeError(error)
|
|
32
|
+
if (aqua?.listenerCount?.(AqualinkEvents.Error) > 0) {
|
|
33
|
+
aqua.emit(AqualinkEvents.Error, source, err)
|
|
34
|
+
} else if (aqua?.debugTrace) {
|
|
35
|
+
aqua._trace('aqua.error.unhandled', { error: err.message })
|
|
36
|
+
}
|
|
37
|
+
return err
|
|
38
|
+
}
|
|
39
|
+
|
|
29
40
|
module.exports = {
|
|
41
|
+
emitOperationalError,
|
|
30
42
|
normalizeError,
|
|
31
43
|
reportSuppressedError
|
|
32
44
|
}
|