livekit-client 2.18.7 → 2.18.8
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/dist/livekit-client.e2ee.worker.js +1 -1
- package/dist/livekit-client.e2ee.worker.js.map +1 -1
- package/dist/livekit-client.e2ee.worker.mjs +2 -2
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +391 -255
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.umd.js +1 -1
- package/dist/livekit-client.umd.js.map +1 -1
- package/dist/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/logger.d.ts +11 -1
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +13 -3
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/PCTransportManager.d.ts +3 -1
- package/dist/src/room/PCTransportManager.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/data-track/LocalDataTrack.d.ts +31 -0
- package/dist/src/room/data-track/LocalDataTrack.d.ts.map +1 -1
- package/dist/src/room/data-track/RemoteDataTrack.d.ts.map +1 -1
- package/dist/src/room/data-track/handle.d.ts +1 -0
- package/dist/src/room/data-track/handle.d.ts.map +1 -1
- package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts +4 -3
- package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts.map +1 -1
- package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +18 -3
- package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts.map +1 -1
- package/dist/src/room/data-track/outgoing/types.d.ts +6 -0
- package/dist/src/room/data-track/outgoing/types.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/Participant.d.ts.map +1 -1
- package/dist/src/utils/subscribeToEvents.d.ts.map +1 -1
- package/dist/ts4.2/logger.d.ts +11 -1
- package/dist/ts4.2/room/PCTransport.d.ts +13 -3
- package/dist/ts4.2/room/PCTransportManager.d.ts +3 -1
- package/dist/ts4.2/room/data-track/LocalDataTrack.d.ts +31 -0
- package/dist/ts4.2/room/data-track/handle.d.ts +1 -0
- package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +4 -3
- package/dist/ts4.2/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +18 -3
- package/dist/ts4.2/room/data-track/outgoing/types.d.ts +6 -0
- package/package.json +1 -1
- package/src/api/SignalClient.ts +19 -31
- package/src/logger.test.ts +61 -0
- package/src/logger.ts +38 -4
- package/src/room/PCTransport.ts +26 -3
- package/src/room/PCTransportManager.test.ts +281 -0
- package/src/room/PCTransportManager.ts +45 -31
- package/src/room/RTCEngine.ts +34 -52
- package/src/room/Room.ts +37 -59
- package/src/room/data-track/LocalDataTrack.ts +51 -0
- package/src/room/data-track/RemoteDataTrack.ts +4 -1
- package/src/room/data-track/handle.ts +4 -0
- package/src/room/data-track/incoming/IncomingDataTrackManager.test.ts +72 -2
- package/src/room/data-track/incoming/IncomingDataTrackManager.ts +5 -3
- package/src/room/data-track/outgoing/OutgoingDataTrackManager.test.ts +235 -1
- package/src/room/data-track/outgoing/OutgoingDataTrackManager.ts +45 -3
- package/src/room/data-track/outgoing/types.ts +5 -0
- package/src/room/participant/LocalParticipant.ts +59 -144
- package/src/room/participant/Participant.ts +4 -1
- package/src/utils/subscribeToEvents.ts +11 -8
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { PCEvents } from './PCTransport';
|
|
4
|
+
import { PCTransportManager } from './PCTransportManager';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Yield to the microtask queue so any then/catch handlers chained to a promise
|
|
8
|
+
* have a chance to run. Sufficient for "is this promise still pending right
|
|
9
|
+
* now?" assertions; nothing in these tests depends on real elapsed time.
|
|
10
|
+
*/
|
|
11
|
+
const flushMicrotasks = () => Promise.resolve();
|
|
12
|
+
|
|
13
|
+
class StubPC {
|
|
14
|
+
iceConnectionState: RTCIceConnectionState = 'new';
|
|
15
|
+
|
|
16
|
+
signalingState: RTCSignalingState = 'stable';
|
|
17
|
+
|
|
18
|
+
connectionState: RTCPeerConnectionState = 'new';
|
|
19
|
+
|
|
20
|
+
onicecandidate: ((ev: RTCPeerConnectionIceEvent) => void) | null = null;
|
|
21
|
+
|
|
22
|
+
onicecandidateerror: ((ev: Event) => void) | null = null;
|
|
23
|
+
|
|
24
|
+
oniceconnectionstatechange: (() => void) | null = null;
|
|
25
|
+
|
|
26
|
+
onsignalingstatechange: (() => void) | null = null;
|
|
27
|
+
|
|
28
|
+
onconnectionstatechange: (() => void) | null = null;
|
|
29
|
+
|
|
30
|
+
ondatachannel: ((ev: RTCDataChannelEvent) => void) | null = null;
|
|
31
|
+
|
|
32
|
+
ontrack: ((ev: RTCTrackEvent) => void) | null = null;
|
|
33
|
+
|
|
34
|
+
getTransceivers() {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getSenders() {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
close() {}
|
|
43
|
+
|
|
44
|
+
setConfiguration() {}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class FakePublisher extends EventEmitter {
|
|
48
|
+
latestOfferId = 0;
|
|
49
|
+
|
|
50
|
+
latestAcknowledgedOfferId = 0;
|
|
51
|
+
|
|
52
|
+
negotiate = vi.fn(async (_onError?: (e: Error) => void) => {});
|
|
53
|
+
|
|
54
|
+
/** Simulate a publisher offer cycle: bump latestOfferId. */
|
|
55
|
+
startOffer() {
|
|
56
|
+
this.latestOfferId += 1;
|
|
57
|
+
return this.latestOfferId;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Simulate a successful answer for the given offerId. */
|
|
61
|
+
answer(offerId: number) {
|
|
62
|
+
this.latestAcknowledgedOfferId = offerId;
|
|
63
|
+
this.emit(PCEvents.OfferAnswered, offerId);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describe('PCTransportManager.negotiate', () => {
|
|
68
|
+
let originalRTCPeerConnection: unknown;
|
|
69
|
+
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
originalRTCPeerConnection = (globalThis as unknown as { RTCPeerConnection?: unknown })
|
|
72
|
+
.RTCPeerConnection;
|
|
73
|
+
(globalThis as unknown as { RTCPeerConnection: unknown }).RTCPeerConnection = StubPC;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
afterEach(() => {
|
|
77
|
+
(globalThis as unknown as { RTCPeerConnection: unknown }).RTCPeerConnection =
|
|
78
|
+
originalRTCPeerConnection;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
function makeManager() {
|
|
82
|
+
const manager = new PCTransportManager('publisher-only', {});
|
|
83
|
+
const fake = new FakePublisher();
|
|
84
|
+
(manager as unknown as { publisher: FakePublisher }).publisher = fake;
|
|
85
|
+
manager.peerConnectionTimeout = 200;
|
|
86
|
+
return { manager, pub: fake };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
it('resolves when an offer past the checkpoint is answered', async () => {
|
|
90
|
+
const { manager, pub } = makeManager();
|
|
91
|
+
const p = manager.negotiate(new AbortController());
|
|
92
|
+
|
|
93
|
+
const id = pub.startOffer();
|
|
94
|
+
pub.answer(id);
|
|
95
|
+
|
|
96
|
+
await expect(p).resolves.toBeUndefined();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('does not resolve on answers for offers at or before the checkpoint', async () => {
|
|
100
|
+
const { manager, pub } = makeManager();
|
|
101
|
+
// Some prior cycle is in flight with id=5 at the moment we capture our
|
|
102
|
+
// checkpoint. Its answer must NOT satisfy our request — our changes
|
|
103
|
+
// weren't in offer 5.
|
|
104
|
+
pub.latestOfferId = 5;
|
|
105
|
+
const ac = new AbortController();
|
|
106
|
+
const p = manager.negotiate(ac);
|
|
107
|
+
|
|
108
|
+
let settled = false;
|
|
109
|
+
p.then(
|
|
110
|
+
() => {
|
|
111
|
+
settled = true;
|
|
112
|
+
},
|
|
113
|
+
() => {
|
|
114
|
+
settled = true;
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
pub.answer(5);
|
|
119
|
+
await flushMicrotasks();
|
|
120
|
+
expect(settled).toBe(false);
|
|
121
|
+
|
|
122
|
+
ac.abort();
|
|
123
|
+
await expect(p).rejects.toThrow(/aborted/);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('resolves through the renegotiate-recursion path', async () => {
|
|
127
|
+
// Reproduces the field shape: we capture checkpoint=N while an offer N is
|
|
128
|
+
// in flight. The answer for N arrives (renegotiate=true on the publisher,
|
|
129
|
+
// so it doesn't satisfy us), then a follow-up offer N+1 is created and
|
|
130
|
+
// answered. We resolve on the second answer.
|
|
131
|
+
const { manager, pub } = makeManager();
|
|
132
|
+
pub.latestOfferId = 1;
|
|
133
|
+
const p = manager.negotiate(new AbortController());
|
|
134
|
+
|
|
135
|
+
pub.answer(1); // does not satisfy checkpoint=1
|
|
136
|
+
|
|
137
|
+
const id = pub.startOffer(); // 2
|
|
138
|
+
pub.answer(id);
|
|
139
|
+
|
|
140
|
+
await expect(p).resolves.toBeUndefined();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('resolves immediately when an answer past the checkpoint already arrived', async () => {
|
|
144
|
+
const { manager, pub } = makeManager();
|
|
145
|
+
pub.latestOfferId = 3;
|
|
146
|
+
pub.latestAcknowledgedOfferId = 4;
|
|
147
|
+
await expect(manager.negotiate(new AbortController())).resolves.toBeUndefined();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('resolves concurrent callers independently at their own checkpoints', async () => {
|
|
151
|
+
const { manager, pub } = makeManager();
|
|
152
|
+
|
|
153
|
+
// A captures checkpoint=0
|
|
154
|
+
const a = manager.negotiate(new AbortController());
|
|
155
|
+
|
|
156
|
+
// First cycle starts; B captures checkpoint=1 (offer now in flight)
|
|
157
|
+
const id1 = pub.startOffer();
|
|
158
|
+
const b = manager.negotiate(new AbortController());
|
|
159
|
+
|
|
160
|
+
let bResolved = false;
|
|
161
|
+
b.then(() => {
|
|
162
|
+
bResolved = true;
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// The first answer satisfies A (1 > 0) but not B (1 > 1 is false).
|
|
166
|
+
pub.answer(id1);
|
|
167
|
+
await a;
|
|
168
|
+
expect(bResolved).toBe(false);
|
|
169
|
+
|
|
170
|
+
// The next cycle's answer satisfies B.
|
|
171
|
+
const id2 = pub.startOffer();
|
|
172
|
+
pub.answer(id2);
|
|
173
|
+
await b;
|
|
174
|
+
expect(bResolved).toBe(true);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('rejects when the deadline elapses', async () => {
|
|
178
|
+
const { manager } = makeManager();
|
|
179
|
+
await expect(manager.negotiate(new AbortController())).rejects.toThrow(/timed out/);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('rejects when the abort signal fires', async () => {
|
|
183
|
+
const { manager } = makeManager();
|
|
184
|
+
const ac = new AbortController();
|
|
185
|
+
const p = manager.negotiate(ac);
|
|
186
|
+
ac.abort();
|
|
187
|
+
await expect(p).rejects.toThrow(/aborted/);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('rejects when publisher.negotiate invokes its error callback', async () => {
|
|
191
|
+
const { manager, pub } = makeManager();
|
|
192
|
+
pub.negotiate.mockImplementationOnce(async (onError?: (e: Error) => void) => {
|
|
193
|
+
onError?.(new Error('publisher boom'));
|
|
194
|
+
});
|
|
195
|
+
await expect(manager.negotiate(new AbortController())).rejects.toThrow(/publisher boom/);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('listener cleanup', () => {
|
|
199
|
+
it('after success', async () => {
|
|
200
|
+
const { manager, pub } = makeManager();
|
|
201
|
+
const p = manager.negotiate(new AbortController());
|
|
202
|
+
const id = pub.startOffer();
|
|
203
|
+
pub.answer(id);
|
|
204
|
+
await p;
|
|
205
|
+
expect(pub.listenerCount(PCEvents.OfferAnswered)).toBe(0);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('after non-matching answer (still pending), then abort', async () => {
|
|
209
|
+
const { manager, pub } = makeManager();
|
|
210
|
+
pub.latestOfferId = 5;
|
|
211
|
+
const ac = new AbortController();
|
|
212
|
+
const p = manager.negotiate(ac);
|
|
213
|
+
pub.answer(5); // does not satisfy checkpoint=5
|
|
214
|
+
expect(pub.listenerCount(PCEvents.OfferAnswered)).toBe(1);
|
|
215
|
+
ac.abort();
|
|
216
|
+
await expect(p).rejects.toThrow(/aborted/);
|
|
217
|
+
expect(pub.listenerCount(PCEvents.OfferAnswered)).toBe(0);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('after deadline', async () => {
|
|
221
|
+
const { manager, pub } = makeManager();
|
|
222
|
+
await expect(manager.negotiate(new AbortController())).rejects.toThrow(/timed out/);
|
|
223
|
+
expect(pub.listenerCount(PCEvents.OfferAnswered)).toBe(0);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('after abort', async () => {
|
|
227
|
+
const { manager, pub } = makeManager();
|
|
228
|
+
const ac = new AbortController();
|
|
229
|
+
const p = manager.negotiate(ac);
|
|
230
|
+
ac.abort();
|
|
231
|
+
await expect(p).rejects.toThrow(/aborted/);
|
|
232
|
+
expect(pub.listenerCount(PCEvents.OfferAnswered)).toBe(0);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('after publisher.negotiate errors', async () => {
|
|
236
|
+
const { manager, pub } = makeManager();
|
|
237
|
+
pub.negotiate.mockImplementationOnce(async (onError?: (e: Error) => void) => {
|
|
238
|
+
onError?.(new Error('publisher boom'));
|
|
239
|
+
});
|
|
240
|
+
await expect(manager.negotiate(new AbortController())).rejects.toThrow(/publisher boom/);
|
|
241
|
+
expect(pub.listenerCount(PCEvents.OfferAnswered)).toBe(0);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('does not leak across many sequential negotiate() calls', async () => {
|
|
245
|
+
const { manager, pub } = makeManager();
|
|
246
|
+
for (let i = 0; i < 12; i += 1) {
|
|
247
|
+
const p = manager.negotiate(new AbortController());
|
|
248
|
+
const id = pub.startOffer();
|
|
249
|
+
pub.answer(id);
|
|
250
|
+
await p;
|
|
251
|
+
}
|
|
252
|
+
expect(pub.listenerCount(PCEvents.OfferAnswered)).toBe(0);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Regression test for publishing call getting stuck
|
|
257
|
+
// With the old design, NegotiationStarted firing faster than
|
|
258
|
+
// peerConnectionTimeout kept resetting the timer indefinitely while
|
|
259
|
+
// NegotiationComplete was suppressed by an unconverging `renegotiate` cycle,
|
|
260
|
+
// wedging the publishTrack Promise. The offerId-checkpoint design resolves
|
|
261
|
+
// on the first answer past the checkpoint, regardless of how many cycles
|
|
262
|
+
// start in between.
|
|
263
|
+
it('does not hang when many spurious cycles start without converging on the checkpoint', async () => {
|
|
264
|
+
const { manager, pub } = makeManager();
|
|
265
|
+
pub.latestOfferId = 1; // an unrelated cycle is in flight
|
|
266
|
+
const p = manager.negotiate(new AbortController());
|
|
267
|
+
|
|
268
|
+
// NegotiationStarted noise (not listened to anymore) interleaved with an
|
|
269
|
+
// answer for the in-flight offer that doesn't satisfy our checkpoint.
|
|
270
|
+
pub.emit(PCEvents.NegotiationStarted);
|
|
271
|
+
pub.emit(PCEvents.NegotiationStarted);
|
|
272
|
+
pub.answer(1); // doesn't satisfy checkpoint=1
|
|
273
|
+
pub.emit(PCEvents.NegotiationStarted);
|
|
274
|
+
|
|
275
|
+
// Eventually a fresh offer is created and answered.
|
|
276
|
+
const id = pub.startOffer(); // 2
|
|
277
|
+
pub.answer(id);
|
|
278
|
+
|
|
279
|
+
await expect(p).resolves.toBeUndefined();
|
|
280
|
+
});
|
|
281
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Mutex } from '@livekit/mutex';
|
|
2
2
|
import { SignalTarget } from '@livekit/protocol';
|
|
3
|
+
import type { Throws } from '@livekit/throws-transformer/throws';
|
|
3
4
|
import log, { LoggerNames, getLogger } from '../logger';
|
|
4
5
|
import TypedPromise from '../utils/TypedPromise';
|
|
5
6
|
import PCTransport, { PCEvents } from './PCTransport';
|
|
@@ -227,47 +228,60 @@ export class PCTransportManager {
|
|
|
227
228
|
}
|
|
228
229
|
}
|
|
229
230
|
|
|
230
|
-
async negotiate(
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
231
|
+
async negotiate(
|
|
232
|
+
abortController: AbortController,
|
|
233
|
+
): Promise<Throws<void, NegotiationError | Error>> {
|
|
234
|
+
return new TypedPromise<void, NegotiationError | Error>((resolve, reject) => {
|
|
235
|
+
// Capture the publisher's latest offer id at request time. We are done
|
|
236
|
+
// when an offer with a higher id has had its answer successfully
|
|
237
|
+
// applied — that offer is the one that includes any transceiver/SDP
|
|
238
|
+
// changes that motivated this negotiate call. Concurrent callers each
|
|
239
|
+
// get their own checkpoint and resolve independently.
|
|
240
|
+
const checkpoint = this.publisher.latestOfferId;
|
|
241
|
+
|
|
242
|
+
// Race: an answer past our checkpoint already arrived before we had a
|
|
243
|
+
// chance to subscribe.
|
|
244
|
+
if (this.publisher.latestAcknowledgedOfferId > checkpoint) {
|
|
245
|
+
this.log.debug(
|
|
246
|
+
`negotiation already handled in more recent acknowledged offer`,
|
|
247
|
+
this.logContext,
|
|
248
|
+
);
|
|
249
|
+
resolve();
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
235
252
|
|
|
253
|
+
let cleanedUp = false;
|
|
236
254
|
const cleanup = () => {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
255
|
+
if (cleanedUp) return;
|
|
256
|
+
cleanedUp = true;
|
|
257
|
+
clearTimeout(deadlineTimer);
|
|
258
|
+
this.publisher.off(PCEvents.OfferAnswered, onAnswered);
|
|
259
|
+
abortController.signal.removeEventListener('abort', onAbort);
|
|
240
260
|
};
|
|
241
261
|
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
262
|
+
const onAnswered = (offerId: number) => {
|
|
263
|
+
if (offerId > checkpoint) {
|
|
264
|
+
cleanup();
|
|
265
|
+
resolve();
|
|
266
|
+
}
|
|
245
267
|
};
|
|
246
268
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
// NegotiationComplete hasn't fired yet because new requirements keep
|
|
251
|
-
// arriving between offer/answer round-trips.
|
|
252
|
-
const onNegotiationStarted = () => {
|
|
253
|
-
if (abortController.signal.aborted) {
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
clearTimeout(negotiationTimeout);
|
|
257
|
-
negotiationTimeout = setTimeout(() => {
|
|
258
|
-
cleanup();
|
|
259
|
-
reject(new NegotiationError('negotiation timed out'));
|
|
260
|
-
}, this.peerConnectionTimeout);
|
|
269
|
+
const onAbort = () => {
|
|
270
|
+
cleanup();
|
|
271
|
+
reject(new NegotiationError('negotiation aborted'));
|
|
261
272
|
};
|
|
262
273
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
274
|
+
// Single hard deadline as a backstop. Not reset on cycle progress —
|
|
275
|
+
// progress is tracked via OfferAnswered, not NegotiationStarted.
|
|
276
|
+
const deadlineTimer = setTimeout(() => {
|
|
266
277
|
cleanup();
|
|
267
|
-
|
|
268
|
-
});
|
|
278
|
+
reject(new NegotiationError('negotiation timed out'));
|
|
279
|
+
}, this.peerConnectionTimeout);
|
|
280
|
+
|
|
281
|
+
abortController.signal.addEventListener('abort', onAbort);
|
|
282
|
+
this.publisher.on(PCEvents.OfferAnswered, onAnswered);
|
|
269
283
|
|
|
270
|
-
|
|
284
|
+
this.publisher.negotiate((e) => {
|
|
271
285
|
cleanup();
|
|
272
286
|
if (e instanceof Error) {
|
|
273
287
|
reject(e);
|