dingtalk-stream 2.1.4 → 2.1.6-beta.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/README.md CHANGED
@@ -1,3 +1,14 @@
1
+ <p align="left">
2
+ <a target="_blank" href="https://github.com/open-dingtalk/dingtalk-stream-sdk-nodejs/actions/workflows/publish.yml">
3
+ <img src="https://img.shields.io/github/actions/workflow/status/open-dingtalk/dingtalk-stream-sdk-nodejs/publish.yml" />
4
+ </a>
5
+
6
+ <a target="_blank" href="https://www.npmjs.com/package/dingtalk-stream">
7
+ <img alt="NPM Version" src="https://img.shields.io/npm/v/dingtalk-stream">
8
+ </a>
9
+
10
+ </p>
11
+
1
12
  钉钉支持 Stream 模式接入事件推送、机器人收消息以及卡片回调,该 SDK 实现了 Stream 模式。相比 Webhook 模式,Stream 模式可以更简单的接入各类事件和回调。
2
13
 
3
14
  ## 开发教程
package/dist/client.cjs CHANGED
@@ -27,9 +27,13 @@ class DWClient extends EventEmitter {
27
27
  registered = false;
28
28
  reconnecting = false;
29
29
  userDisconnect = false;
30
- reconnectInterval = 1e3;
30
+ reconnectBaseInterval = 1e3;
31
+ reconnectMaxInterval = 6e4;
32
+ reconnectAttempts = 0;
31
33
  heartbeat_interval = 8e3;
32
34
  heartbeatIntervallId;
35
+ reconnectTimerId;
36
+ isConnecting = false;
33
37
  sslopts = { rejectUnauthorized: true };
34
38
  config;
35
39
  socket;
@@ -128,13 +132,52 @@ class DWClient extends EventEmitter {
128
132
  throw new Error("build: get endpoint failed");
129
133
  }
130
134
  }
135
+ cleanup() {
136
+ if (this.heartbeatIntervallId !== void 0) {
137
+ clearInterval(this.heartbeatIntervallId);
138
+ this.heartbeatIntervallId = void 0;
139
+ }
140
+ if (this.socket) {
141
+ this.socket.removeAllListeners();
142
+ if (this.socket.readyState === WebSocket.OPEN || this.socket.readyState === WebSocket.CONNECTING) {
143
+ this.socket.terminate();
144
+ }
145
+ this.socket = void 0;
146
+ }
147
+ }
148
+ scheduleReconnect() {
149
+ if (!this.config.autoReconnect || this.userDisconnect || this.isConnecting) {
150
+ return;
151
+ }
152
+ const delay = Math.min(
153
+ this.reconnectBaseInterval * Math.pow(2, this.reconnectAttempts) + Math.random() * 1e3,
154
+ this.reconnectMaxInterval
155
+ );
156
+ this.reconnecting = true;
157
+ this.printDebug("Reconnecting in " + (delay / 1e3).toFixed(1) + " seconds... (attempt " + (this.reconnectAttempts + 1) + ")");
158
+ if (this.reconnectTimerId) {
159
+ clearTimeout(this.reconnectTimerId);
160
+ }
161
+ this.reconnectTimerId = setTimeout(() => {
162
+ this.reconnectTimerId = void 0;
163
+ this.connect();
164
+ }, delay);
165
+ }
131
166
  _connect() {
132
167
  return new Promise((resolve, reject) => {
133
- this.userDisconnect = false;
134
168
  this.printDebug("Connecting to dingtalk websocket @ " + this.dw_url);
135
- this.socket = new WebSocket(this.dw_url, this.sslopts);
169
+ try {
170
+ this.socket = new WebSocket(this.dw_url, this.sslopts);
171
+ } catch (err) {
172
+ this.printDebug("WebSocket constructor error");
173
+ console.warn("ERROR", err);
174
+ reject(err);
175
+ return;
176
+ }
177
+ let settled = false;
136
178
  this.socket.on("open", () => {
137
179
  this.connected = true;
180
+ this.reconnectAttempts = 0;
138
181
  console.info("[" + (/* @__PURE__ */ new Date()).toISOString() + "] connect success");
139
182
  if (this.config.keepAlive) {
140
183
  this.isAlive = true;
@@ -150,6 +193,8 @@ class DWClient extends EventEmitter {
150
193
  (_b = this.socket) == null ? void 0 : _b.ping("", true);
151
194
  }, this.heartbeat_interval);
152
195
  }
196
+ settled = true;
197
+ resolve();
153
198
  });
154
199
  this.socket.on("pong", () => {
155
200
  this.heartbeat();
@@ -157,37 +202,67 @@ class DWClient extends EventEmitter {
157
202
  this.socket.on("message", (data) => {
158
203
  this.onDownStream(data);
159
204
  });
160
- this.socket.on("close", (err) => {
205
+ this.socket.on("close", () => {
161
206
  this.printDebug("Socket closed");
162
207
  this.connected = false;
163
208
  this.registered = false;
164
- if (this.config.autoReconnect && !this.userDisconnect) {
165
- this.reconnecting = true;
166
- this.printDebug(
167
- "Reconnecting in " + this.reconnectInterval / 1e3 + " seconds..."
168
- );
169
- setTimeout(this.connect.bind(this), this.reconnectInterval);
209
+ if (this.heartbeatIntervallId !== void 0) {
210
+ clearInterval(this.heartbeatIntervallId);
211
+ this.heartbeatIntervallId = void 0;
212
+ }
213
+ if (settled) {
214
+ this.scheduleReconnect();
170
215
  }
171
216
  });
172
217
  this.socket.on("error", (err) => {
218
+ var _a;
173
219
  this.printDebug("SOCKET ERROR");
174
220
  console.warn("ERROR", err);
221
+ (_a = this.socket) == null ? void 0 : _a.terminate();
222
+ if (!settled) {
223
+ settled = true;
224
+ reject(err);
225
+ }
175
226
  });
176
- resolve();
177
227
  });
178
228
  }
179
229
  async connect() {
180
- await this.getEndpoint();
181
- await this._connect();
230
+ if (this.isConnecting) {
231
+ this.printDebug("connect() already in progress, skipping");
232
+ return;
233
+ }
234
+ this.userDisconnect = false;
235
+ this.isConnecting = true;
236
+ try {
237
+ this.cleanup();
238
+ await this.getEndpoint();
239
+ if (this.userDisconnect)
240
+ return;
241
+ await this._connect();
242
+ } catch (err) {
243
+ this.printDebug("Connect failed: " + (err instanceof Error ? err.message : String(err)));
244
+ if (!this.userDisconnect) {
245
+ this.reconnectAttempts++;
246
+ this.isConnecting = false;
247
+ this.scheduleReconnect();
248
+ }
249
+ return;
250
+ } finally {
251
+ this.isConnecting = false;
252
+ }
182
253
  }
183
254
  disconnect() {
184
- var _a;
185
255
  console.info("Disconnecting.");
186
256
  this.userDisconnect = true;
187
- if (this.config.keepAlive && this.heartbeatIntervallId !== void 0) {
188
- clearInterval(this.heartbeatIntervallId);
257
+ if (this.reconnectTimerId) {
258
+ clearTimeout(this.reconnectTimerId);
259
+ this.reconnectTimerId = void 0;
189
260
  }
190
- (_a = this.socket) == null ? void 0 : _a.close();
261
+ this.reconnecting = false;
262
+ this.reconnectAttempts = 0;
263
+ this.cleanup();
264
+ this.connected = false;
265
+ this.registered = false;
191
266
  }
192
267
  heartbeat() {
193
268
  this.isAlive = true;
package/dist/client.d.ts CHANGED
@@ -50,9 +50,13 @@ declare class DWClient extends EventEmitter {
50
50
  registered: boolean;
51
51
  reconnecting: boolean;
52
52
  private userDisconnect;
53
- private reconnectInterval;
53
+ private reconnectBaseInterval;
54
+ private reconnectMaxInterval;
55
+ private reconnectAttempts;
54
56
  private heartbeat_interval;
55
57
  private heartbeatIntervallId?;
58
+ private reconnectTimerId?;
59
+ private isConnecting;
56
60
  private sslopts;
57
61
  readonly config: DWClientConfig;
58
62
  private socket?;
@@ -85,6 +89,8 @@ declare class DWClient extends EventEmitter {
85
89
  registerCallbackListener(eventId: string, callback: (v: DWClientDownStream) => void): this;
86
90
  getAccessToken(): Promise<any>;
87
91
  getEndpoint(): Promise<this>;
92
+ private cleanup;
93
+ private scheduleReconnect;
88
94
  _connect(): Promise<void>;
89
95
  connect(): Promise<void>;
90
96
  disconnect(): void;
package/dist/client.mjs CHANGED
@@ -25,9 +25,13 @@ class DWClient extends EventEmitter {
25
25
  registered = false;
26
26
  reconnecting = false;
27
27
  userDisconnect = false;
28
- reconnectInterval = 1e3;
28
+ reconnectBaseInterval = 1e3;
29
+ reconnectMaxInterval = 6e4;
30
+ reconnectAttempts = 0;
29
31
  heartbeat_interval = 8e3;
30
32
  heartbeatIntervallId;
33
+ reconnectTimerId;
34
+ isConnecting = false;
31
35
  sslopts = { rejectUnauthorized: true };
32
36
  config;
33
37
  socket;
@@ -126,13 +130,52 @@ class DWClient extends EventEmitter {
126
130
  throw new Error("build: get endpoint failed");
127
131
  }
128
132
  }
133
+ cleanup() {
134
+ if (this.heartbeatIntervallId !== void 0) {
135
+ clearInterval(this.heartbeatIntervallId);
136
+ this.heartbeatIntervallId = void 0;
137
+ }
138
+ if (this.socket) {
139
+ this.socket.removeAllListeners();
140
+ if (this.socket.readyState === WebSocket.OPEN || this.socket.readyState === WebSocket.CONNECTING) {
141
+ this.socket.terminate();
142
+ }
143
+ this.socket = void 0;
144
+ }
145
+ }
146
+ scheduleReconnect() {
147
+ if (!this.config.autoReconnect || this.userDisconnect || this.isConnecting) {
148
+ return;
149
+ }
150
+ const delay = Math.min(
151
+ this.reconnectBaseInterval * Math.pow(2, this.reconnectAttempts) + Math.random() * 1e3,
152
+ this.reconnectMaxInterval
153
+ );
154
+ this.reconnecting = true;
155
+ this.printDebug("Reconnecting in " + (delay / 1e3).toFixed(1) + " seconds... (attempt " + (this.reconnectAttempts + 1) + ")");
156
+ if (this.reconnectTimerId) {
157
+ clearTimeout(this.reconnectTimerId);
158
+ }
159
+ this.reconnectTimerId = setTimeout(() => {
160
+ this.reconnectTimerId = void 0;
161
+ this.connect();
162
+ }, delay);
163
+ }
129
164
  _connect() {
130
165
  return new Promise((resolve, reject) => {
131
- this.userDisconnect = false;
132
166
  this.printDebug("Connecting to dingtalk websocket @ " + this.dw_url);
133
- this.socket = new WebSocket(this.dw_url, this.sslopts);
167
+ try {
168
+ this.socket = new WebSocket(this.dw_url, this.sslopts);
169
+ } catch (err) {
170
+ this.printDebug("WebSocket constructor error");
171
+ console.warn("ERROR", err);
172
+ reject(err);
173
+ return;
174
+ }
175
+ let settled = false;
134
176
  this.socket.on("open", () => {
135
177
  this.connected = true;
178
+ this.reconnectAttempts = 0;
136
179
  console.info("[" + (/* @__PURE__ */ new Date()).toISOString() + "] connect success");
137
180
  if (this.config.keepAlive) {
138
181
  this.isAlive = true;
@@ -148,6 +191,8 @@ class DWClient extends EventEmitter {
148
191
  (_b = this.socket) == null ? void 0 : _b.ping("", true);
149
192
  }, this.heartbeat_interval);
150
193
  }
194
+ settled = true;
195
+ resolve();
151
196
  });
152
197
  this.socket.on("pong", () => {
153
198
  this.heartbeat();
@@ -155,37 +200,67 @@ class DWClient extends EventEmitter {
155
200
  this.socket.on("message", (data) => {
156
201
  this.onDownStream(data);
157
202
  });
158
- this.socket.on("close", (err) => {
203
+ this.socket.on("close", () => {
159
204
  this.printDebug("Socket closed");
160
205
  this.connected = false;
161
206
  this.registered = false;
162
- if (this.config.autoReconnect && !this.userDisconnect) {
163
- this.reconnecting = true;
164
- this.printDebug(
165
- "Reconnecting in " + this.reconnectInterval / 1e3 + " seconds..."
166
- );
167
- setTimeout(this.connect.bind(this), this.reconnectInterval);
207
+ if (this.heartbeatIntervallId !== void 0) {
208
+ clearInterval(this.heartbeatIntervallId);
209
+ this.heartbeatIntervallId = void 0;
210
+ }
211
+ if (settled) {
212
+ this.scheduleReconnect();
168
213
  }
169
214
  });
170
215
  this.socket.on("error", (err) => {
216
+ var _a;
171
217
  this.printDebug("SOCKET ERROR");
172
218
  console.warn("ERROR", err);
219
+ (_a = this.socket) == null ? void 0 : _a.terminate();
220
+ if (!settled) {
221
+ settled = true;
222
+ reject(err);
223
+ }
173
224
  });
174
- resolve();
175
225
  });
176
226
  }
177
227
  async connect() {
178
- await this.getEndpoint();
179
- await this._connect();
228
+ if (this.isConnecting) {
229
+ this.printDebug("connect() already in progress, skipping");
230
+ return;
231
+ }
232
+ this.userDisconnect = false;
233
+ this.isConnecting = true;
234
+ try {
235
+ this.cleanup();
236
+ await this.getEndpoint();
237
+ if (this.userDisconnect)
238
+ return;
239
+ await this._connect();
240
+ } catch (err) {
241
+ this.printDebug("Connect failed: " + (err instanceof Error ? err.message : String(err)));
242
+ if (!this.userDisconnect) {
243
+ this.reconnectAttempts++;
244
+ this.isConnecting = false;
245
+ this.scheduleReconnect();
246
+ }
247
+ return;
248
+ } finally {
249
+ this.isConnecting = false;
250
+ }
180
251
  }
181
252
  disconnect() {
182
- var _a;
183
253
  console.info("Disconnecting.");
184
254
  this.userDisconnect = true;
185
- if (this.config.keepAlive && this.heartbeatIntervallId !== void 0) {
186
- clearInterval(this.heartbeatIntervallId);
255
+ if (this.reconnectTimerId) {
256
+ clearTimeout(this.reconnectTimerId);
257
+ this.reconnectTimerId = void 0;
187
258
  }
188
- (_a = this.socket) == null ? void 0 : _a.close();
259
+ this.reconnecting = false;
260
+ this.reconnectAttempts = 0;
261
+ this.cleanup();
262
+ this.connected = false;
263
+ this.registered = false;
189
264
  }
190
265
  heartbeat() {
191
266
  this.isAlive = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dingtalk-stream",
3
- "version": "2.1.4",
3
+ "version": "v2.1.6-beta.1",
4
4
  "description": "Nodejs SDK for DingTalk Stream Mode API, Compared with the webhook mode, it is easier to access the DingTalk",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -37,7 +37,10 @@
37
37
  "build": "rimraf dist && rollup -c",
38
38
  "dev": "rollup -c --watch --watch.include 'src/**' -m inline",
39
39
  "prepublishOnly": "pnpm build",
40
- "typecheck": "tsc --noEmit"
40
+ "typecheck": "tsc --noEmit",
41
+ "test": "pnpm build && node test/reconnect-mock.mjs",
42
+ "test:prod": "pnpm build && node test/reconnect-prod.mjs",
43
+ "test:demo": "node test/reconnect-storm-demo.mjs"
41
44
  },
42
45
  "repository": {
43
46
  "type": "git",
@@ -72,6 +75,7 @@
72
75
  "@types/debug": "^4.1.8",
73
76
  "@types/node": ">=16",
74
77
  "@types/ws": "^8.5.5",
78
+ "nock": "^14.0.11",
75
79
  "rimraf": "^5.0.1",
76
80
  "rollup": "^3.28.0",
77
81
  "rollup-plugin-dts": "^6.0.0",