@rc-ex/ws 1.2.2 → 1.3.1
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/{src → lib/esm}/exceptions/ClosedException.js +1 -1
- package/lib/esm/exceptions/ClosedException.js.map +1 -0
- package/{src → lib/esm}/exceptions/ConnectionException.d.ts +1 -1
- package/{src → lib/esm}/exceptions/ConnectionException.js +4 -2
- package/lib/esm/exceptions/ConnectionException.js.map +1 -0
- package/{src → lib/esm}/exceptions/TimeoutException.js +1 -1
- package/lib/esm/exceptions/TimeoutException.js.map +1 -0
- package/{src → lib/esm}/index.d.ts +5 -5
- package/{src → lib/esm}/index.js +52 -47
- package/lib/esm/index.js.map +1 -0
- package/{src → lib/esm}/rest.d.ts +2 -2
- package/{src → lib/esm}/rest.js +5 -6
- package/lib/esm/rest.js.map +1 -0
- package/{src → lib/esm}/subscription.d.ts +3 -3
- package/{src → lib/esm}/subscription.js +8 -3
- package/lib/esm/subscription.js.map +1 -0
- package/{src → lib/esm}/types.d.ts +1 -1
- package/lib/esm/types.js.map +1 -0
- package/{src → lib/esm}/utils.d.ts +1 -1
- package/{src → lib/esm}/utils.js +4 -4
- package/lib/esm/utils.js.map +1 -0
- package/lib/exceptions/ClosedException.js +21 -4
- package/lib/exceptions/ClosedException.js.map +1 -1
- package/lib/exceptions/ConnectionException.d.ts +1 -1
- package/lib/exceptions/ConnectionException.js +27 -8
- package/lib/exceptions/ConnectionException.js.map +1 -1
- package/lib/exceptions/TimeoutException.js +21 -4
- package/lib/exceptions/TimeoutException.js.map +1 -1
- package/lib/index.d.ts +5 -5
- package/lib/index.js +478 -292
- package/lib/index.js.map +1 -1
- package/lib/rest.d.ts +2 -2
- package/lib/rest.js +96 -47
- package/lib/rest.js.map +1 -1
- package/lib/subscription.d.ts +3 -3
- package/lib/subscription.js +150 -63
- package/lib/subscription.js.map +1 -1
- package/lib/types.d.ts +1 -1
- package/lib/types.js.map +1 -1
- package/lib/utils.d.ts +1 -1
- package/lib/utils.js +79 -35
- package/lib/utils.js.map +1 -1
- package/package.json +10 -6
- package/src/exceptions/ConnectionException.ts +2 -2
- package/src/index.ts +15 -12
- package/src/rest.ts +4 -4
- package/src/subscription.ts +9 -5
- package/src/types.ts +1 -1
- package/src/utils.ts +3 -3
- package/lib/exceptions/ClosedException.ts +0 -7
- package/lib/exceptions/ConnectionException.ts +0 -17
- package/lib/exceptions/TimeoutException.ts +0 -7
- package/lib/index.ts +0 -418
- package/lib/rest.ts +0 -71
- package/lib/subscription.ts +0 -131
- package/lib/types.ts +0 -85
- package/lib/utils.ts +0 -82
- package/src/exceptions/ClosedException.js.map +0 -1
- package/src/exceptions/ConnectionException.js.map +0 -1
- package/src/exceptions/TimeoutException.js.map +0 -1
- package/src/index.js.map +0 -1
- package/src/rest.js.map +0 -1
- package/src/subscription.js.map +0 -1
- package/src/types.js.map +0 -1
- package/src/utils.js.map +0 -1
- /package/{src → lib/esm}/exceptions/ClosedException.d.ts +0 -0
- /package/{src → lib/esm}/exceptions/TimeoutException.d.ts +0 -0
- /package/{src → lib/esm}/types.js +0 -0
package/lib/index.ts
DELETED
|
@@ -1,418 +0,0 @@
|
|
|
1
|
-
import type RingCentral from "@rc-ex/core";
|
|
2
|
-
import type {
|
|
3
|
-
RestMethod,
|
|
4
|
-
RestRequestConfig,
|
|
5
|
-
RestResponse,
|
|
6
|
-
} from "@rc-ex/core/src/types";
|
|
7
|
-
import SdkExtension from "@rc-ex/core/src/SdkExtension";
|
|
8
|
-
import type { MessageEvent } from "isomorphic-ws";
|
|
9
|
-
import WS from "isomorphic-ws";
|
|
10
|
-
import hyperid from "hyperid";
|
|
11
|
-
import { EventEmitter } from "events";
|
|
12
|
-
import waitFor from "wait-for-async";
|
|
13
|
-
import RestException from "@rc-ex/core/src/RestException";
|
|
14
|
-
import type SubscriptionInfo from "@rc-ex/core/src/definitions/SubscriptionInfo";
|
|
15
|
-
|
|
16
|
-
import { request } from "./rest";
|
|
17
|
-
import type {
|
|
18
|
-
ConnectionDetails,
|
|
19
|
-
WebSocketExtensionInterface,
|
|
20
|
-
WebSocketOptions,
|
|
21
|
-
Wsc,
|
|
22
|
-
WsgEvent,
|
|
23
|
-
WsToken,
|
|
24
|
-
} from "./types";
|
|
25
|
-
import Subscription from "./subscription";
|
|
26
|
-
import ConnectionException from "./exceptions/ConnectionException";
|
|
27
|
-
import Utils from "./utils";
|
|
28
|
-
|
|
29
|
-
const CONNECTING = 0;
|
|
30
|
-
const OPEN = 1;
|
|
31
|
-
|
|
32
|
-
const uuid = hyperid();
|
|
33
|
-
|
|
34
|
-
export enum Events {
|
|
35
|
-
autoRecoverSuccess = "autoRecoverSuccess",
|
|
36
|
-
autoRecoverFailed = "autoRecoverFailed",
|
|
37
|
-
autoRecoverError = "autoRecoverError",
|
|
38
|
-
newWebSocketObject = "newWebSocketObject",
|
|
39
|
-
newWsc = "newWsc",
|
|
40
|
-
connectionReady = "connectionReady",
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
class WebSocketExtension extends SdkExtension {
|
|
44
|
-
public eventEmitter = new EventEmitter();
|
|
45
|
-
|
|
46
|
-
public options: WebSocketOptions;
|
|
47
|
-
|
|
48
|
-
public rc!: RingCentral;
|
|
49
|
-
|
|
50
|
-
public wsToken?: WsToken;
|
|
51
|
-
|
|
52
|
-
public wsTokenExpiresAt = 0;
|
|
53
|
-
|
|
54
|
-
public ws!: WS;
|
|
55
|
-
|
|
56
|
-
public connectionDetails!: ConnectionDetails;
|
|
57
|
-
|
|
58
|
-
public wsc?: Wsc;
|
|
59
|
-
|
|
60
|
-
public subscription?: Subscription;
|
|
61
|
-
|
|
62
|
-
// for auto recover
|
|
63
|
-
public intervalHandle?: NodeJS.Timeout;
|
|
64
|
-
|
|
65
|
-
public recoverTimestamp?: number;
|
|
66
|
-
|
|
67
|
-
public pingServerHandle?: NodeJS.Timeout;
|
|
68
|
-
|
|
69
|
-
public _recoverPromise?: Promise<void>;
|
|
70
|
-
|
|
71
|
-
public _connectPromise?: Promise<void>;
|
|
72
|
-
|
|
73
|
-
public request = request; // request method was moved to another file to keep this file short
|
|
74
|
-
|
|
75
|
-
public constructor(options: WebSocketOptions = {}) {
|
|
76
|
-
super();
|
|
77
|
-
this.options = options;
|
|
78
|
-
this.options.restOverWebSocket ??= false;
|
|
79
|
-
this.options.debugMode ??= false;
|
|
80
|
-
this.options.autoRecover ??= {
|
|
81
|
-
enabled: true,
|
|
82
|
-
};
|
|
83
|
-
this.options.autoRecover.checkInterval ??= (retriesAttempted) => {
|
|
84
|
-
const interval = 2000 + 2000 * retriesAttempted;
|
|
85
|
-
return Math.min(8000, interval);
|
|
86
|
-
};
|
|
87
|
-
this.options.autoRecover.pingServerInterval ??= 60000;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
public disable() {
|
|
91
|
-
super.disable();
|
|
92
|
-
if (this.subscription) {
|
|
93
|
-
this.subscription.enabled = false;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
public async install(rc: RingCentral) {
|
|
98
|
-
this.rc = rc;
|
|
99
|
-
if (this.options.restOverWebSocket) {
|
|
100
|
-
const request = rc.request.bind(rc);
|
|
101
|
-
rc.request = async <T>(
|
|
102
|
-
method: RestMethod,
|
|
103
|
-
endpoint: string,
|
|
104
|
-
content?: {},
|
|
105
|
-
queryParams?: {},
|
|
106
|
-
config?: RestRequestConfig,
|
|
107
|
-
): Promise<RestResponse<T>> => {
|
|
108
|
-
if (!this.enabled || !this.options.restOverWebSocket) {
|
|
109
|
-
return request(method, endpoint, content, queryParams, config);
|
|
110
|
-
}
|
|
111
|
-
if (
|
|
112
|
-
// the following cannot be done with WebSocket
|
|
113
|
-
config?.headers?.getContentType?.toString()?.includes(
|
|
114
|
-
"multipart/form-data",
|
|
115
|
-
) ||
|
|
116
|
-
config?.responseType === "arraybuffer" ||
|
|
117
|
-
endpoint.startsWith("/restapi/oauth/") // token, revoke, wstoken
|
|
118
|
-
) {
|
|
119
|
-
return request(method, endpoint, content, queryParams, config);
|
|
120
|
-
}
|
|
121
|
-
return this.request<T>(method, endpoint, content, queryParams, config);
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// should recover if this.options.wscToken
|
|
126
|
-
let connectMethod = this.connect.bind(this);
|
|
127
|
-
if (this.options.wscToken) {
|
|
128
|
-
this.wsc = {
|
|
129
|
-
token: this.options.wscToken,
|
|
130
|
-
sequence: 0,
|
|
131
|
-
};
|
|
132
|
-
connectMethod = this.recover.bind(this);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (!this.options.autoRecover!.enabled) {
|
|
136
|
-
await connectMethod();
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// code after is for auto recover
|
|
141
|
-
try {
|
|
142
|
-
await connectMethod();
|
|
143
|
-
} catch (e) {
|
|
144
|
-
if (e instanceof RestException) {
|
|
145
|
-
throw e; // such as InsufficientPermissions
|
|
146
|
-
}
|
|
147
|
-
if (this.options.debugMode) {
|
|
148
|
-
console.debug("Initial connect failed:", e);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
let retriesAttempted = 0;
|
|
152
|
-
let checking = false;
|
|
153
|
-
const check = async () => {
|
|
154
|
-
if (!this.enabled) {
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
if (this.options.autoRecover?.enabled !== true) {
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
if (checking) {
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
checking = true;
|
|
164
|
-
if (this.ws?.readyState !== OPEN && this.ws?.readyState !== CONNECTING) {
|
|
165
|
-
clearInterval(this.intervalHandle!);
|
|
166
|
-
try {
|
|
167
|
-
await this.recover();
|
|
168
|
-
retriesAttempted = 0;
|
|
169
|
-
if (this.options.debugMode) {
|
|
170
|
-
console.debug(
|
|
171
|
-
`Auto recover done, recoveryState: ${this.connectionDetails.recoveryState}`,
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
this.eventEmitter.emit(
|
|
175
|
-
this.connectionDetails.recoveryState === "Successful"
|
|
176
|
-
? Events.autoRecoverSuccess
|
|
177
|
-
: Events.autoRecoverFailed,
|
|
178
|
-
this.ws,
|
|
179
|
-
);
|
|
180
|
-
} catch (e) {
|
|
181
|
-
if (e instanceof RestException) {
|
|
182
|
-
throw e; // such as InsufficientPermissions
|
|
183
|
-
}
|
|
184
|
-
retriesAttempted += 1;
|
|
185
|
-
if (this.options.debugMode) {
|
|
186
|
-
console.debug("Auto recover error:", e);
|
|
187
|
-
}
|
|
188
|
-
this.eventEmitter.emit(Events.autoRecoverError, e);
|
|
189
|
-
}
|
|
190
|
-
this.intervalHandle = setInterval(
|
|
191
|
-
check,
|
|
192
|
-
this.options.autoRecover!.checkInterval!(retriesAttempted),
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
checking = false;
|
|
196
|
-
};
|
|
197
|
-
this.intervalHandle = setInterval(
|
|
198
|
-
check,
|
|
199
|
-
this.options.autoRecover!.checkInterval!(retriesAttempted),
|
|
200
|
-
);
|
|
201
|
-
|
|
202
|
-
// browser only code start
|
|
203
|
-
if (typeof window !== "undefined" && window.addEventListener) {
|
|
204
|
-
window.addEventListener("offline", () => {
|
|
205
|
-
if (this.pingServerHandle) {
|
|
206
|
-
clearTimeout(this.pingServerHandle);
|
|
207
|
-
}
|
|
208
|
-
this.ws?.close();
|
|
209
|
-
});
|
|
210
|
-
window.addEventListener("online", () => {
|
|
211
|
-
check();
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
// browser only code end
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
public async recover() {
|
|
218
|
-
if (this._recoverPromise) {
|
|
219
|
-
return this._recoverPromise;
|
|
220
|
-
}
|
|
221
|
-
this._recoverPromise = this._recover();
|
|
222
|
-
try {
|
|
223
|
-
await this._recoverPromise;
|
|
224
|
-
} finally {
|
|
225
|
-
this._recoverPromise = undefined;
|
|
226
|
-
}
|
|
227
|
-
return undefined;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
public async _recover() {
|
|
231
|
-
if (this.ws?.readyState === OPEN || this.ws?.readyState === CONNECTING) {
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
if (!this.wsc || !this.wsc.token) {
|
|
235
|
-
await this.connect(false); // connect to WSG but do not recover
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
if (this.recoverTimestamp === undefined) {
|
|
239
|
-
this.recoverTimestamp = Date.now();
|
|
240
|
-
}
|
|
241
|
-
if (
|
|
242
|
-
this.connectionDetails !== undefined &&
|
|
243
|
-
Date.now() - this.recoverTimestamp >
|
|
244
|
-
this.connectionDetails.recoveryTimeout * 1000
|
|
245
|
-
) {
|
|
246
|
-
if (this.options.debugMode) {
|
|
247
|
-
console.debug("connect to WSG but do not recover");
|
|
248
|
-
}
|
|
249
|
-
await this.connect(false); // connect to WSG but do not recover
|
|
250
|
-
} else {
|
|
251
|
-
if (this.options.debugMode) {
|
|
252
|
-
console.debug("connect to WSG and recover");
|
|
253
|
-
}
|
|
254
|
-
await this.connect(true); // connect to WSG and recover
|
|
255
|
-
}
|
|
256
|
-
this.recoverTimestamp = undefined;
|
|
257
|
-
this.enable();
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
public async pingServer() {
|
|
261
|
-
if (this.options.autoRecover?.enabled !== true) {
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
if (this.ws?.readyState !== OPEN) {
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
try {
|
|
268
|
-
await this.ws.send(
|
|
269
|
-
JSON.stringify([
|
|
270
|
-
{
|
|
271
|
-
type: "Heartbeat",
|
|
272
|
-
messageId: uuid(),
|
|
273
|
-
},
|
|
274
|
-
]),
|
|
275
|
-
);
|
|
276
|
-
} catch (e) {
|
|
277
|
-
this.ws.close(); // Explicitly mark WS as closed
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
public async connect(recoverSession?: boolean) {
|
|
282
|
-
if (this._connectPromise) {
|
|
283
|
-
return this._connectPromise;
|
|
284
|
-
}
|
|
285
|
-
this._connectPromise = this._connect(recoverSession);
|
|
286
|
-
try {
|
|
287
|
-
await this._connectPromise;
|
|
288
|
-
} finally {
|
|
289
|
-
this._connectPromise = undefined;
|
|
290
|
-
}
|
|
291
|
-
return undefined;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
public async _connect(recoverSession = false) {
|
|
295
|
-
if (!this.wsToken || Date.now() > this.wsTokenExpiresAt) {
|
|
296
|
-
const r = await this.rc.post("/restapi/oauth/wstoken");
|
|
297
|
-
this.wsToken = r.data as WsToken;
|
|
298
|
-
// `expires_in` default value is 600 seconds. That's why we `* 0.8`
|
|
299
|
-
this.wsTokenExpiresAt = Date.now() + this.wsToken.expires_in * 0.8 * 1000;
|
|
300
|
-
}
|
|
301
|
-
let wsUri = `${this.wsToken!.uri}?access_token=${
|
|
302
|
-
this.wsToken!.ws_access_token
|
|
303
|
-
}`;
|
|
304
|
-
if (recoverSession && this.wsc) {
|
|
305
|
-
wsUri += `&wsc=${this.wsc.token}`;
|
|
306
|
-
}
|
|
307
|
-
this.ws = new WS(wsUri);
|
|
308
|
-
this.eventEmitter.emit(Events.newWebSocketObject, this.ws);
|
|
309
|
-
|
|
310
|
-
// override send method to wait for connecting
|
|
311
|
-
const send = this.ws.send.bind(this.ws);
|
|
312
|
-
this.ws.send = async (s: string) => {
|
|
313
|
-
if (this.ws.readyState === CONNECTING) {
|
|
314
|
-
await waitFor({
|
|
315
|
-
interval: 100,
|
|
316
|
-
condition: () => this.ws.readyState !== CONNECTING,
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
await send(s);
|
|
320
|
-
};
|
|
321
|
-
|
|
322
|
-
if (this.options.autoRecover?.enabled) {
|
|
323
|
-
this.ws.addEventListener("message", () => {
|
|
324
|
-
if (this.pingServerHandle) {
|
|
325
|
-
clearTimeout(this.pingServerHandle);
|
|
326
|
-
}
|
|
327
|
-
this.pingServerHandle = setTimeout(
|
|
328
|
-
() => this.pingServer(),
|
|
329
|
-
this.options.autoRecover!.pingServerInterval,
|
|
330
|
-
);
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// debug mode to print all WebSocket traffic
|
|
335
|
-
if (this.options.debugMode) {
|
|
336
|
-
Utils.debugWebSocket(this.ws);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// listen for new wsc data
|
|
340
|
-
this.ws.addEventListener("message", (mEvent: MessageEvent) => {
|
|
341
|
-
const event = mEvent as WsgEvent;
|
|
342
|
-
const [meta, body] = Utils.splitWsgData(event.data);
|
|
343
|
-
if (
|
|
344
|
-
meta.wsc &&
|
|
345
|
-
(!this.wsc ||
|
|
346
|
-
(meta.type === "ConnectionDetails" && body.recoveryState) ||
|
|
347
|
-
this.wsc.sequence < meta.wsc.sequence)
|
|
348
|
-
) {
|
|
349
|
-
this.wsc = meta.wsc;
|
|
350
|
-
this.eventEmitter.emit(Events.newWsc, this.wsc);
|
|
351
|
-
}
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
// get initial ConnectionDetails data
|
|
355
|
-
const [meta, body, event] = await Utils.waitForWebSocketMessage(
|
|
356
|
-
this.ws,
|
|
357
|
-
(meta) => meta.type === "ConnectionDetails" || meta.type === "Error",
|
|
358
|
-
);
|
|
359
|
-
if (meta.type === "Error") {
|
|
360
|
-
throw new ConnectionException(event);
|
|
361
|
-
}
|
|
362
|
-
this.connectionDetails = body;
|
|
363
|
-
|
|
364
|
-
// fired when ws connection is ready for creating subscription
|
|
365
|
-
this.eventEmitter.emit(Events.connectionReady, this.ws);
|
|
366
|
-
|
|
367
|
-
// recover the subscription, if it exists and enabled
|
|
368
|
-
if (this.subscription && this.subscription.enabled) {
|
|
369
|
-
// because we have a new ws object
|
|
370
|
-
this.subscription.setupWsEventListener();
|
|
371
|
-
if (
|
|
372
|
-
!recoverSession || this.connectionDetails.recoveryState === "Failed"
|
|
373
|
-
) {
|
|
374
|
-
// create new subscription if don't recover existing one
|
|
375
|
-
await this.subscription.subscribe();
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// keepInterval means we do not clear the interval
|
|
381
|
-
public async revoke(keepInterval = false) {
|
|
382
|
-
await this.subscription?.revoke();
|
|
383
|
-
this.subscription = undefined;
|
|
384
|
-
if (!keepInterval && this.intervalHandle) {
|
|
385
|
-
clearInterval(this.intervalHandle);
|
|
386
|
-
}
|
|
387
|
-
if (this.pingServerHandle) {
|
|
388
|
-
clearTimeout(this.pingServerHandle);
|
|
389
|
-
}
|
|
390
|
-
this.ws?.close();
|
|
391
|
-
this.wsc = undefined;
|
|
392
|
-
this.wsToken = undefined;
|
|
393
|
-
this.wsTokenExpiresAt = 0;
|
|
394
|
-
this.disable();
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
public async subscribe(
|
|
398
|
-
eventFilters: string[],
|
|
399
|
-
callback: (event: {}) => void,
|
|
400
|
-
cache: SubscriptionInfo | undefined | null = undefined,
|
|
401
|
-
) {
|
|
402
|
-
const subscription = new Subscription(
|
|
403
|
-
this as WebSocketExtensionInterface,
|
|
404
|
-
eventFilters,
|
|
405
|
-
callback,
|
|
406
|
-
);
|
|
407
|
-
if (cache === undefined || cache === null) {
|
|
408
|
-
await subscription.subscribe();
|
|
409
|
-
} else {
|
|
410
|
-
subscription.subscriptionInfo = cache;
|
|
411
|
-
await subscription.refresh();
|
|
412
|
-
}
|
|
413
|
-
this.subscription = subscription;
|
|
414
|
-
return subscription;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
export default WebSocketExtension;
|
package/lib/rest.ts
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
RestMethod,
|
|
3
|
-
RestRequestConfig,
|
|
4
|
-
RestResponse,
|
|
5
|
-
} from "@rc-ex/core/src/types";
|
|
6
|
-
import RestException from "@rc-ex/core/src/RestException";
|
|
7
|
-
import hyperid from "hyperid";
|
|
8
|
-
import { getReasonPhrase } from "http-status-codes";
|
|
9
|
-
|
|
10
|
-
import Utils from "./utils";
|
|
11
|
-
import type { WebSocketExtensionInterface } from "./types";
|
|
12
|
-
|
|
13
|
-
const version = "0.16";
|
|
14
|
-
|
|
15
|
-
const uuid = hyperid();
|
|
16
|
-
|
|
17
|
-
export async function request<T>(
|
|
18
|
-
this: WebSocketExtensionInterface,
|
|
19
|
-
method: RestMethod,
|
|
20
|
-
endpoint: string,
|
|
21
|
-
content?: {},
|
|
22
|
-
queryParams?: {},
|
|
23
|
-
config?: RestRequestConfig,
|
|
24
|
-
): Promise<RestResponse<T>> {
|
|
25
|
-
const newConfig: RestRequestConfig = {
|
|
26
|
-
method,
|
|
27
|
-
baseURL: this.wsToken?.uri,
|
|
28
|
-
url: endpoint,
|
|
29
|
-
data: content,
|
|
30
|
-
params: queryParams,
|
|
31
|
-
...config,
|
|
32
|
-
};
|
|
33
|
-
newConfig.headers = {
|
|
34
|
-
...newConfig.headers,
|
|
35
|
-
"X-User-Agent": `${this.rc.rest!.appName}/${
|
|
36
|
-
this.rc.rest!.appVersion
|
|
37
|
-
} ringcentral-extensible/ws/${version}`,
|
|
38
|
-
} as any;
|
|
39
|
-
const messageId = uuid();
|
|
40
|
-
const requestBody = [
|
|
41
|
-
{
|
|
42
|
-
type: "ClientRequest",
|
|
43
|
-
messageId,
|
|
44
|
-
method: newConfig.method,
|
|
45
|
-
path: newConfig.url,
|
|
46
|
-
headers: newConfig.headers,
|
|
47
|
-
query: newConfig.params,
|
|
48
|
-
},
|
|
49
|
-
];
|
|
50
|
-
if (newConfig.data) {
|
|
51
|
-
requestBody.push(newConfig.data);
|
|
52
|
-
}
|
|
53
|
-
await this.ws.send(JSON.stringify(requestBody));
|
|
54
|
-
const [meta, body] = await Utils.waitForWebSocketMessage(
|
|
55
|
-
this.ws,
|
|
56
|
-
(_meta) => _meta.messageId === messageId,
|
|
57
|
-
);
|
|
58
|
-
const response: RestResponse = {
|
|
59
|
-
data: body as T,
|
|
60
|
-
status: meta.status,
|
|
61
|
-
statusText: getReasonPhrase(meta.status),
|
|
62
|
-
headers: meta.headers,
|
|
63
|
-
config: newConfig as any,
|
|
64
|
-
};
|
|
65
|
-
if (
|
|
66
|
-
meta.type === "ClientRequest" && meta.status >= 200 && meta.status < 300
|
|
67
|
-
) {
|
|
68
|
-
return response;
|
|
69
|
-
}
|
|
70
|
-
throw new RestException(response);
|
|
71
|
-
}
|
package/lib/subscription.ts
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import type CreateSubscriptionRequest from "@rc-ex/core/src/definitions/CreateSubscriptionRequest";
|
|
2
|
-
import type SubscriptionInfo from "@rc-ex/core/src/definitions/SubscriptionInfo";
|
|
3
|
-
import type { RestResponse } from "@rc-ex/core/src/types";
|
|
4
|
-
import type { MessageEvent } from "ws";
|
|
5
|
-
|
|
6
|
-
import type { WebSocketExtensionInterface, WsgEvent, WsgMeta } from "./types";
|
|
7
|
-
import Utils from "./utils";
|
|
8
|
-
|
|
9
|
-
class Subscription {
|
|
10
|
-
public subscriptionInfo?: SubscriptionInfo;
|
|
11
|
-
|
|
12
|
-
public wse: WebSocketExtensionInterface;
|
|
13
|
-
|
|
14
|
-
public eventFilters: string[];
|
|
15
|
-
|
|
16
|
-
public eventListener: (event: MessageEvent) => void;
|
|
17
|
-
|
|
18
|
-
public timeout?: NodeJS.Timeout;
|
|
19
|
-
|
|
20
|
-
public enabled = true;
|
|
21
|
-
|
|
22
|
-
public constructor(
|
|
23
|
-
wse: WebSocketExtensionInterface,
|
|
24
|
-
eventFilters: string[],
|
|
25
|
-
callback: (event: {}) => void,
|
|
26
|
-
) {
|
|
27
|
-
this.wse = wse;
|
|
28
|
-
this.eventFilters = eventFilters;
|
|
29
|
-
this.eventListener = (mEvent: MessageEvent) => {
|
|
30
|
-
const event = mEvent as WsgEvent;
|
|
31
|
-
const [meta, body]: [WsgMeta, { subscriptionId: string }] = Utils
|
|
32
|
-
.splitWsgData(event.data);
|
|
33
|
-
if (
|
|
34
|
-
this.enabled && meta.type === "ServerNotification" &&
|
|
35
|
-
body.subscriptionId === this.subscriptionInfo!.id
|
|
36
|
-
) {
|
|
37
|
-
callback(body);
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
this.setupWsEventListener();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
public setupWsEventListener() {
|
|
44
|
-
this.wse.ws.addEventListener("message", this.eventListener);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
public get requestBody(): CreateSubscriptionRequest {
|
|
48
|
-
return {
|
|
49
|
-
deliveryMode: { transportType: "WebSocket" as any }, // because WebSocket is not in spec
|
|
50
|
-
eventFilters: this.eventFilters,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
public async subscribe() {
|
|
55
|
-
this.subscriptionInfo = (
|
|
56
|
-
await this.wse.request<SubscriptionInfo>(
|
|
57
|
-
"POST",
|
|
58
|
-
"/restapi/v1.0/subscription",
|
|
59
|
-
this.requestBody,
|
|
60
|
-
)
|
|
61
|
-
).data;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
public async refresh() {
|
|
65
|
-
if (!this.subscriptionInfo) {
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
try {
|
|
69
|
-
this.subscriptionInfo = (
|
|
70
|
-
await this.wse.request<SubscriptionInfo>(
|
|
71
|
-
"PUT",
|
|
72
|
-
`/restapi/v1.0/subscription/${this.subscriptionInfo!.id}`,
|
|
73
|
-
this.requestBody,
|
|
74
|
-
)
|
|
75
|
-
).data;
|
|
76
|
-
} catch (e) {
|
|
77
|
-
const re = e as { response: RestResponse };
|
|
78
|
-
if (re.response && re.response.status === 404) {
|
|
79
|
-
// subscription expired
|
|
80
|
-
await this.subscribe();
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
public async revoke() {
|
|
86
|
-
if (!this.subscriptionInfo) {
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
try {
|
|
90
|
-
await this.wse.request<SubscriptionInfo>(
|
|
91
|
-
"DELETE",
|
|
92
|
-
`/restapi/v1.0/subscription/${this.subscriptionInfo!.id}`,
|
|
93
|
-
);
|
|
94
|
-
} catch (e) {
|
|
95
|
-
const re = e as { response: RestResponse };
|
|
96
|
-
if (re.response && re.response.status === 404) {
|
|
97
|
-
// ignore
|
|
98
|
-
if (this.wse.options.debugMode) {
|
|
99
|
-
console.debug(
|
|
100
|
-
`Subscription ${
|
|
101
|
-
this.subscriptionInfo!.id
|
|
102
|
-
} doesn't exist on server side`,
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
} else if (re.response && re.response.status === 401) {
|
|
106
|
-
// ignore
|
|
107
|
-
if (this.wse.options.debugMode) {
|
|
108
|
-
console.debug("Token invalid when trying to revoke subscription");
|
|
109
|
-
}
|
|
110
|
-
} else {
|
|
111
|
-
throw e;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
this.remove();
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
public remove() {
|
|
118
|
-
if (this.timeout) {
|
|
119
|
-
global.clearTimeout(this.timeout);
|
|
120
|
-
this.timeout = undefined;
|
|
121
|
-
}
|
|
122
|
-
this.enabled = false;
|
|
123
|
-
this.subscriptionInfo = undefined;
|
|
124
|
-
if (this.wse.ws) {
|
|
125
|
-
this.wse.ws.removeEventListener("message", this.eventListener);
|
|
126
|
-
}
|
|
127
|
-
this.wse.subscription = undefined;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export default Subscription;
|
package/lib/types.ts
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import type RingCentral from "@rc-ex/core";
|
|
2
|
-
import type {
|
|
3
|
-
RestMethod,
|
|
4
|
-
RestRequestConfig,
|
|
5
|
-
RestResponse,
|
|
6
|
-
} from "@rc-ex/core/src/types";
|
|
7
|
-
import type WS from "isomorphic-ws";
|
|
8
|
-
|
|
9
|
-
export interface WsToken {
|
|
10
|
-
uri: string;
|
|
11
|
-
ws_access_token: string;
|
|
12
|
-
expires_in: number;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export type CheckInterval = (retriesAttempted: number) => number;
|
|
16
|
-
export interface WebSocketOptions {
|
|
17
|
-
restOverWebSocket?: boolean;
|
|
18
|
-
debugMode?: boolean;
|
|
19
|
-
autoRecover?: {
|
|
20
|
-
enabled: boolean;
|
|
21
|
-
checkInterval?: CheckInterval;
|
|
22
|
-
pingServerInterval?: number;
|
|
23
|
-
};
|
|
24
|
-
wscToken?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface WsgEvent {
|
|
28
|
-
data: string;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface Wsc {
|
|
32
|
-
token: string;
|
|
33
|
-
sequence: number;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface WsgMeta {
|
|
37
|
-
type:
|
|
38
|
-
| "ClientRequest"
|
|
39
|
-
| "ServerNotification"
|
|
40
|
-
| "Error"
|
|
41
|
-
| "ConnectionDetails"
|
|
42
|
-
| "Heartbeat";
|
|
43
|
-
messageId: string;
|
|
44
|
-
status: number;
|
|
45
|
-
headers: {
|
|
46
|
-
[key: string]: string;
|
|
47
|
-
};
|
|
48
|
-
wsc?: Wsc;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export interface WsgError {
|
|
52
|
-
errorCode: string;
|
|
53
|
-
message: string;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export interface ConnectionDetails {
|
|
57
|
-
creationTime: string;
|
|
58
|
-
maxConnectionsPerSession: number;
|
|
59
|
-
recoveryBufferSize: number;
|
|
60
|
-
recoveryTimeout: number;
|
|
61
|
-
idleTimeout: number;
|
|
62
|
-
absoluteTimeout: number;
|
|
63
|
-
maxActiveRequests: number;
|
|
64
|
-
recoveryState?: "Successful" | "Failed";
|
|
65
|
-
recoveryErrorCode?: string;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export interface WebSocketExtensionInterface {
|
|
69
|
-
options: WebSocketOptions;
|
|
70
|
-
subscription?: SubscriptionInterface;
|
|
71
|
-
ws: WS;
|
|
72
|
-
wsToken?: WsToken;
|
|
73
|
-
rc: RingCentral;
|
|
74
|
-
request: <T>(
|
|
75
|
-
method: RestMethod,
|
|
76
|
-
endpoint: string,
|
|
77
|
-
content?: {},
|
|
78
|
-
queryParams?: {},
|
|
79
|
-
config?: RestRequestConfig,
|
|
80
|
-
) => Promise<RestResponse<T>>;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export interface SubscriptionInterface {
|
|
84
|
-
eventFilters: string[];
|
|
85
|
-
}
|