@vlasky/zongji 0.5.9 → 0.6.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.
Files changed (45) hide show
  1. package/.claude/settings.local.json +16 -0
  2. package/.mcp.json +17 -0
  3. package/.tap/processinfo/84de694d-068b-4ac7-b4d8-f835c2122b2d.json +892 -0
  4. package/.tap/processinfo/a1e9627e-66eb-412c-aaa5-079a2eb9f385.json +891 -0
  5. package/.tap/processinfo/abc34c6f-f4aa-4405-8f9a-947048be1df2.json +892 -0
  6. package/.tap/processinfo/affe6c5b-a34d-41b9-bb73-4b14d35138de.json +891 -0
  7. package/.tap/processinfo/bf82449d-cdeb-478b-9506-b939a9506bf2.json +883 -0
  8. package/.tap/processinfo/dd98c508-7067-4342-bdf5-63664b630701.json +892 -0
  9. package/.tap/test-results/test_await_end.js.tap +19 -0
  10. package/.tap/test-results/test_await_end2.js.tap +27 -0
  11. package/.tap/test-results/test_debug_init.js.tap +23 -0
  12. package/.tap/test-results/test_debug_init2.js.tap +35 -0
  13. package/.tap/test-results/test_invalid_host.js.tap +34 -0
  14. package/.tap/test-results/test_minimal.js.tap +26 -0
  15. package/.tap/test-results/test_queryseq.js.tap +14 -0
  16. package/.tap/test-results/test_timing.js.tap +17 -0
  17. package/.tap/test-results/test_unref.js.tap +15 -0
  18. package/CHANGELOG.md +55 -0
  19. package/CLAUDE.md +99 -0
  20. package/GTID.md +54 -0
  21. package/README.md +44 -21
  22. package/TODO.md +43 -0
  23. package/docker-compose.yml +14 -13
  24. package/docker-test.sh +7 -7
  25. package/eslint.config.js +35 -0
  26. package/example.js +1 -1
  27. package/index.d.ts +314 -0
  28. package/index.js +444 -272
  29. package/jsconfig.json +15 -0
  30. package/lib/binlog_event.js +296 -217
  31. package/lib/code_map.js +7 -4
  32. package/lib/common.js +35 -22
  33. package/lib/datetime_decode.js +147 -34
  34. package/lib/json_decode.js +12 -6
  35. package/lib/packet/binlog.js +3 -3
  36. package/lib/packet/combinlog.js +22 -20
  37. package/lib/packet/index.js +50 -50
  38. package/lib/reader.js +215 -109
  39. package/lib/rows_event.js +98 -102
  40. package/lib/sequence/binlog.js +142 -39
  41. package/notes.md +77 -0
  42. package/package.json +15 -10
  43. package/scripts/start-mysql.sh +28 -0
  44. package/workerthreads.md +173 -0
  45. package/.eslintrc +0 -19
package/index.js CHANGED
@@ -1,355 +1,527 @@
1
- const mysql = require('@vlasky/mysql');
2
- const util = require('util');
3
- const EventEmitter = require('events').EventEmitter;
4
- const initBinlogClass = require('./lib/sequence/binlog');
1
+ import mysql from 'mysql2';
2
+ import { EventEmitter } from 'events';
3
+ import initBinlogClass from './lib/sequence/binlog.js';
5
4
 
6
5
  const ConnectionConfigMap = {
7
6
  'Connection': obj => obj.config,
8
7
  'Pool': obj => obj.config.connectionConfig,
9
8
  };
10
9
 
11
- const TableInfoQueryTemplate = `SELECT
12
- COLUMN_NAME, COLLATION_NAME, CHARACTER_SET_NAME,
13
- COLUMN_COMMENT, COLUMN_TYPE
14
- FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA='%s' AND TABLE_NAME='%s'
10
+ const TableInfoQueryTemplate = `SELECT
11
+ COLUMN_NAME, COLLATION_NAME, CHARACTER_SET_NAME,
12
+ COLUMN_COMMENT, COLUMN_TYPE
13
+ FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA='%s' AND TABLE_NAME='%s'
15
14
  ORDER BY ORDINAL_POSITION`;
16
15
 
17
- function ZongJi(dsn) {
18
- EventEmitter.call(this);
16
+ // Simple format function to replace %s placeholders
17
+ function formatSql(template, ...args) {
18
+ let i = 0;
19
+ return template.replace(/%s/g, () => args[i++]);
20
+ }
19
21
 
20
- this._options({});
21
- this._filters({});
22
- this.ctrlCallbacks = [];
23
- this.tableMap = {};
24
- this.ready = false;
25
- this.useChecksum = false;
22
+ class ZongJi extends EventEmitter {
23
+ constructor(dsn) {
24
+ super();
26
25
 
27
- this._establishConnection(dsn);
28
- }
26
+ this._pendingErrors = [];
27
+ this.on('newListener', (event) => {
28
+ if (event !== 'error' || this._pendingErrors.length === 0) {
29
+ return;
30
+ }
31
+ const pending = this._pendingErrors.slice();
32
+ this._pendingErrors.length = 0;
33
+ process.nextTick(() => {
34
+ pending.forEach(err => this.emit('error', err));
35
+ });
36
+ });
29
37
 
30
- util.inherits(ZongJi, EventEmitter);
31
-
32
- // dsn - can be one instance of Connection or Pool / object / url string
33
- ZongJi.prototype._establishConnection = function(dsn) {
34
- const createConnection = (options) => {
35
- let connection = mysql.createConnection(options);
36
- connection.on('error', this.emit.bind(this, 'error'));
37
- connection.on('unhandledError', this.emit.bind(this, 'error'));
38
- // don't need to call connection.connect() here
39
- // we use implicitly established connection
40
- // see https://github.com/mysqljs/mysql#establishing-connections
41
- return connection;
42
- };
43
-
44
- const configFunc = ConnectionConfigMap[dsn.constructor.name];
45
- let binlogDsn;
46
-
47
- if (typeof dsn === 'object' && configFunc) {
48
- // dsn is a pool or connection object
49
- let conn = dsn; // reuse as ctrlConnection
50
- this.ctrlConnection = conn;
38
+ this._options({});
39
+ this._filters({});
40
+ this.ctrlCallbacks = [];
41
+ this.tableMap = {};
42
+ this.ready = false;
43
+ this.stopped = false;
44
+ this.useChecksum = false;
45
+
46
+ this._dsn = dsn;
47
+ this.ctrlConnection = null;
48
+ this.connection = null;
51
49
  this.ctrlConnectionOwner = false;
52
- binlogDsn = Object.assign({}, configFunc(conn));
53
50
  }
54
51
 
55
- if (!binlogDsn) {
56
- // assuming that the object passed is the connection settings
57
- this.ctrlConnectionOwner = true;
58
- this.ctrlConnection = createConnection(dsn);
59
- binlogDsn = dsn;
52
+ // dsn - can be one instance of Connection or Pool / object / url string
53
+ _establishConnection(dsn) {
54
+ const createConnection = (options) => {
55
+ const emitError = (err) => {
56
+ if (this.listenerCount('error') === 0) {
57
+ this._pendingErrors.push(err);
58
+ return;
59
+ }
60
+ this.emit('error', err);
61
+ };
62
+ let connection = mysql.createConnection(options);
63
+ connection.on('error', emitError);
64
+ // don't need to call connection.connect() here
65
+ // we use implicitly established connection
66
+ // see https://github.com/mysqljs/mysql#establishing-connections
67
+ return connection;
68
+ };
69
+
70
+ const configFunc = ConnectionConfigMap[dsn.constructor.name];
71
+ let binlogDsn;
72
+ const sanitizeConnectionOptions = (options) => {
73
+ if (!options) return options;
74
+ const cleaned = Object.assign({}, options);
75
+ delete cleaned.maxPacketSize;
76
+ delete cleaned.clientFlags;
77
+ return cleaned;
78
+ };
79
+
80
+ if (typeof dsn === 'object' && configFunc) {
81
+ // dsn is a pool or connection object
82
+ let conn = dsn; // reuse as ctrlConnection
83
+ this.ctrlConnection = conn;
84
+ this.ctrlConnectionOwner = false;
85
+ binlogDsn = sanitizeConnectionOptions(configFunc(conn));
86
+ }
87
+
88
+ if (!binlogDsn) {
89
+ // assuming that the object passed is the connection settings
90
+ this.ctrlConnectionOwner = true;
91
+ this.ctrlConnection = createConnection(dsn);
92
+ binlogDsn = dsn;
93
+ }
94
+
95
+ this.connection = createConnection(binlogDsn);
60
96
  }
61
97
 
62
- this.connection = createConnection(binlogDsn);
63
- };
98
+ _isChecksumEnabled(next) {
99
+ const SelectChecksumParamSql = 'select @@GLOBAL.binlog_checksum as checksum';
100
+ const SetChecksumSql = 'set @master_binlog_checksum=@@global.binlog_checksum';
64
101
 
65
- ZongJi.prototype._isChecksumEnabled = function(next) {
66
- const SelectChecksumParamSql = 'select @@GLOBAL.binlog_checksum as checksum';
67
- const SetChecksumSql = 'set @master_binlog_checksum=@@global.binlog_checksum';
102
+ let done = false;
103
+ const finish = (err, enabled) => {
104
+ if (done) return;
105
+ done = true;
106
+ next(err, enabled);
107
+ };
68
108
 
69
- const query = (conn, sql) => {
70
- return new Promise(
71
- (resolve, reject) => {
72
- conn.query(sql, (err, result) => {
73
- if (err) {
109
+ const isConnectionReady = (conn) => {
110
+ return conn &&
111
+ conn.state !== 'disconnected' &&
112
+ !conn._fatalError &&
113
+ !conn._protocolError &&
114
+ !conn._closing;
115
+ };
116
+
117
+ const query = (conn, sql) => {
118
+ if (!isConnectionReady(conn)) {
119
+ return Promise.resolve(null);
120
+ }
121
+ return new Promise(
122
+ (resolve, reject) => {
123
+ try {
124
+ conn.query(sql, (err, result) => {
125
+ if (err) {
126
+ reject(err);
127
+ }
128
+ else {
129
+ resolve(result);
130
+ }
131
+ });
132
+ } catch (err) {
74
133
  reject(err);
75
134
  }
76
- else {
77
- resolve(result);
78
- }
79
- });
80
- }
81
- );
82
- };
135
+ }
136
+ );
137
+ };
83
138
 
84
- let checksumEnabled = true;
139
+ let checksumEnabled = true;
85
140
 
86
- query(this.ctrlConnection, SelectChecksumParamSql)
87
- .then(rows => {
88
- if (rows[0].checksum === 'NONE') {
89
- checksumEnabled = false;
90
- return query(this.connection, 'SELECT 1');
91
- }
141
+ query(this.ctrlConnection, SelectChecksumParamSql)
142
+ .then(rows => {
143
+ if (!rows) {
144
+ checksumEnabled = false;
145
+ return null;
146
+ }
147
+ if (rows[0].checksum === 'NONE') {
148
+ checksumEnabled = false;
149
+ return query(this.connection, 'SELECT 1');
150
+ }
92
151
 
93
- if (checksumEnabled) {
94
- return query(this.connection, SetChecksumSql);
95
- }
96
- })
97
- .catch(err => {
98
- if (err.toString().match(/ER_UNKNOWN_SYSTEM_VARIABLE/)) {
99
- checksumEnabled = false;
100
- // a simple query to open this.connection
101
- return query(this.connection, 'SELECT 1');
152
+ if (checksumEnabled) {
153
+ return query(this.connection, SetChecksumSql);
154
+ }
155
+ })
156
+ .catch(err => {
157
+ if (err.toString().match(/ER_UNKNOWN_SYSTEM_VARIABLE/)) {
158
+ checksumEnabled = false;
159
+ // a simple query to open this.connection
160
+ return query(this.connection, 'SELECT 1');
161
+ }
162
+ else {
163
+ return finish(err);
164
+ }
165
+ })
166
+ .then(() => {
167
+ finish(null, checksumEnabled);
168
+ })
169
+ .catch(err => {
170
+ finish(err);
171
+ });
172
+ }
173
+
174
+ _findBinlogEnd(next) {
175
+ this.ctrlConnection.query('SHOW BINARY LOGS', (err, rows) => {
176
+ if (err) {
177
+ // Errors should be emitted
178
+ next(err);
102
179
  }
103
180
  else {
104
- next(err);
181
+ next(null, rows.length > 0 ? rows[rows.length - 1] : null);
105
182
  }
106
- })
107
- .then(() => {
108
- next(null, checksumEnabled);
109
183
  });
110
- };
111
-
112
- ZongJi.prototype._findBinlogEnd = function(next) {
113
- this.ctrlConnection.query('SHOW BINARY LOGS', (err, rows) => {
114
- if (err) {
115
- // Errors should be emitted
116
- next(err);
117
- }
118
- else {
119
- next(null, rows.length > 0 ? rows[rows.length - 1] : null);
120
- }
121
- });
122
- };
184
+ }
123
185
 
124
- ZongJi.prototype._fetchTableInfo = function(tableMapEvent, next) {
125
- const sql = util.format(TableInfoQueryTemplate,
126
- tableMapEvent.schemaName, tableMapEvent.tableName);
186
+ _fetchTableInfo(tableMapEvent, next) {
187
+ const sql = formatSql(TableInfoQueryTemplate,
188
+ tableMapEvent.schemaName, tableMapEvent.tableName);
127
189
 
128
- this.ctrlConnection.query(sql, (err, rows) => {
129
- if (err) {
130
- // Errors should be emitted
131
- this.emit('error', err);
132
- // This is a fatal error, no additional binlog events will be
133
- // processed since next() will never be called
190
+ if (!this.ctrlConnection ||
191
+ this.ctrlConnection.state === 'disconnected' ||
192
+ this.ctrlConnection._fatalError ||
193
+ this.ctrlConnection._protocolError ||
194
+ this.ctrlConnection._closing) {
134
195
  return;
135
196
  }
136
197
 
137
- if (rows.length === 0) {
138
- this.emit('error', new Error(
139
- 'Insufficient permissions to access: ' +
140
- tableMapEvent.schemaName + '.' + tableMapEvent.tableName));
141
- // This is a fatal error, no additional binlog events will be
142
- // processed since next() will never be called
143
- return;
144
- }
198
+ try {
199
+ this.ctrlConnection.query(sql, (err, rows) => {
200
+ if (err) {
201
+ // Errors should be emitted
202
+ this.emit('error', err);
203
+ // This is a fatal error, no additional binlog events will be
204
+ // processed since next() will never be called
205
+ return;
206
+ }
145
207
 
146
- this.tableMap[tableMapEvent.tableId] = {
147
- columnSchemas: rows,
148
- parentSchema: tableMapEvent.schemaName,
149
- tableName: tableMapEvent.tableName
150
- };
208
+ if (rows.length === 0) {
209
+ this.emit('error', new Error(
210
+ 'Insufficient permissions to access: ' +
211
+ tableMapEvent.schemaName + '.' + tableMapEvent.tableName));
212
+ // This is a fatal error, no additional binlog events will be
213
+ // processed since next() will never be called
214
+ return;
215
+ }
151
216
 
152
- next();
153
- });
154
- };
217
+ this.tableMap[tableMapEvent.tableId] = {
218
+ columnSchemas: rows,
219
+ parentSchema: tableMapEvent.schemaName,
220
+ tableName: tableMapEvent.tableName
221
+ };
222
+
223
+ next();
224
+ });
225
+ } catch (err) {
226
+ this.emit('error', err);
227
+ }
228
+ }
155
229
 
156
- // #_options will reset all the options.
157
- ZongJi.prototype._options = function({
158
- serverId,
159
- filename,
160
- position,
161
- startAtEnd,
162
- }) {
163
- this.options = {
230
+ // #_options will reset all the options.
231
+ /** @param {object} [options] */
232
+ _options({
164
233
  serverId,
165
234
  filename,
166
235
  position,
167
236
  startAtEnd,
168
- };
169
- };
237
+ } = {}) {
238
+ this.options = {
239
+ serverId,
240
+ filename,
241
+ position,
242
+ startAtEnd,
243
+ };
244
+ }
170
245
 
171
- // #_filters will reset all the filters.
172
- ZongJi.prototype._filters = function({
173
- includeEvents,
174
- excludeEvents,
175
- includeSchema,
176
- excludeSchema,
177
- }) {
178
- this.filters = {
246
+ // #_filters will reset all the filters.
247
+ /** @param {object} [options] */
248
+ _filters({
179
249
  includeEvents,
180
250
  excludeEvents,
181
251
  includeSchema,
182
252
  excludeSchema,
183
- };
184
- };
185
-
186
- ZongJi.prototype.get = function(name) {
187
- let result;
188
- if (typeof name === 'string') {
189
- result = this.options[name];
253
+ } = {}) {
254
+ this.filters = {
255
+ includeEvents,
256
+ excludeEvents,
257
+ includeSchema,
258
+ excludeSchema,
259
+ };
190
260
  }
191
- else if (Array.isArray(name)) {
192
- result = name.reduce(
193
- (acc, cur) => {
194
- acc[cur] = this.options[cur];
195
- return acc;
196
- },
197
- {}
198
- );
261
+
262
+ get(name) {
263
+ let result;
264
+ if (typeof name === 'string') {
265
+ result = this.options[name];
266
+ }
267
+ else if (Array.isArray(name)) {
268
+ result = name.reduce(
269
+ (acc, cur) => {
270
+ acc[cur] = this.options[cur];
271
+ return acc;
272
+ },
273
+ {}
274
+ );
275
+ }
276
+
277
+ return result;
199
278
  }
200
279
 
201
- return result;
202
- };
280
+ // @options contains a list options
281
+ // - `serverId` unique identifier
282
+ // - `filename`, `position` the position of binlog to begin with
283
+ // - `startAtEnd` if true, will update filename / position automatically
284
+ // - `includeEvents`, `excludeEvents`, `includeSchema`, `excludeSchema` filter different binlog events bubbling
285
+ start(options = {}) {
286
+ // If already running, just update filters (for pause/resume) and return
287
+ if (this.ready && !this.stopped) {
288
+ this._filters(options);
289
+ return;
290
+ }
203
291
 
204
- // @options contains a list options
205
- // - `serverId` unique identifier
206
- // - `filename`, `position` the position of binlog to beigin with
207
- // - `startAtEnd` if true, will update filename / postion automatically
208
- // - `includeEvents`, `excludeEvents`, `includeSchema`, `exludeSchema` filter different binlog events bubbling
209
- ZongJi.prototype.start = function(options = {}) {
292
+ this.stopped = false;
210
293
 
211
- this._options(options);
212
- this._filters(options);
294
+ if (!this.connection || !this.ctrlConnection) {
295
+ this._establishConnection(this._dsn);
296
+ }
213
297
 
214
- const testChecksum = (resolve, reject) => {
215
- this._isChecksumEnabled((err, checksumEnabled) => {
216
- if (err) {
217
- reject(err);
218
- }
219
- else {
220
- this.useChecksum = checksumEnabled;
221
- resolve();
222
- }
223
- });
224
- };
298
+ this._options(options);
299
+ this._filters(options);
300
+
301
+ const testChecksum = (resolve, reject) => {
302
+ this._isChecksumEnabled((err, checksumEnabled) => {
303
+ if (err) {
304
+ reject(err);
305
+ }
306
+ else {
307
+ this.useChecksum = checksumEnabled;
308
+ resolve();
309
+ }
310
+ });
311
+ };
225
312
 
226
313
 
227
- const findBinlogEnd = (resolve, reject) => {
228
- this._findBinlogEnd((err, result) => {
229
- if (err) {
230
- return reject(err);
314
+ const findBinlogEnd = (resolve, reject) => {
315
+ this._findBinlogEnd((err, result) => {
316
+ if (err) {
317
+ return reject(err);
318
+ }
319
+
320
+ if (result) {
321
+ this._options(
322
+ Object.assign({}, options, {
323
+ filename: result.Log_name,
324
+ position: result.File_size,
325
+ })
326
+ );
327
+ }
328
+
329
+ resolve();
330
+ });
331
+ };
332
+
333
+ const binlogHandler = (error, event) => {
334
+ if (error) {
335
+ return this.emit('error', error);
231
336
  }
232
337
 
233
- if (result) {
234
- this._options(
235
- Object.assign({}, options, {
236
- filename: result.Log_name,
237
- position: result.File_size,
238
- })
239
- );
338
+ // Ignore events if connection has been stopped
339
+ if (this.stopped || !this.connection) return;
340
+
341
+ // Do not emit events that have been filtered out
342
+ if (event === undefined || event._filtered === true) return;
343
+
344
+ switch (event.getTypeName()) {
345
+ case 'TableMap': {
346
+ const tableMap = this.tableMap[event.tableId];
347
+ if (!tableMap || tableMap.tableName !== event.tableName || tableMap.columns.length !== event.columnCount) {
348
+ if (!this.connection) return;
349
+ this.connection.pause();
350
+ this._fetchTableInfo(event, () => {
351
+ // merge the column info with metadata
352
+ event.updateColumnInfo();
353
+ this.emit('binlog', event);
354
+ if (this.connection) this.connection.resume();
355
+ });
356
+ return;
357
+ }
358
+ break;
359
+ }
360
+ case 'Rotate':
361
+ if (this.options.filename !== event.binlogName) {
362
+ this.options.filename = event.binlogName;
363
+ }
364
+ break;
240
365
  }
366
+ this.options.position = event.nextPosition;
367
+ this.emit('binlog', event);
368
+ };
241
369
 
242
- resolve();
243
- });
244
- };
370
+ let promises = [new Promise(testChecksum)];
245
371
 
246
- const binlogHandler = (error, event) => {
247
- if (error) {
248
- return this.emit('error', error);
372
+ if (this.options.startAtEnd) {
373
+ promises.push(new Promise(findBinlogEnd));
249
374
  }
250
375
 
251
- // Do not emit events that have been filtered out
252
- if (event === undefined || event._filtered === true) return;
253
-
254
- switch (event.getTypeName()) {
255
- case 'TableMap': {
256
- const tableMap = this.tableMap[event.tableId];
257
- if (!tableMap || tableMap.tableName !== event.tableName || tableMap.columns.length !== event.columnCount) {
258
- this.connection.pause();
259
- this._fetchTableInfo(event, () => {
260
- // merge the column info with metadata
261
- event.updateColumnInfo();
262
- this.emit('binlog', event);
263
- this.connection.resume();
264
- });
376
+ Promise.all(promises)
377
+ .then(() => {
378
+ // Check if stop() was called while promises were pending
379
+ if (this.stopped) {
265
380
  return;
266
381
  }
267
- break;
268
- }
269
- case 'Rotate':
270
- if (this.options.filename !== event.binlogName) {
271
- this.options.filename = event.binlogName;
382
+
383
+ this.BinlogClass = initBinlogClass(this);
384
+ this.ready = true;
385
+ this.emit('ready');
386
+
387
+ // Final check right before addCommand - connection may have been destroyed
388
+ if (this.stopped) {
389
+ return;
272
390
  }
273
- break;
274
- }
275
- this.options.position = event.nextPosition;
276
- this.emit('binlog', event);
277
- };
278
391
 
279
- let promises = [new Promise(testChecksum)];
392
+ // When compression is enabled, patch handlePacket to sync sequence ID.
393
+ // MySQL resets inner packet sequence IDs within compressed chunks for
394
+ // binlog streams, causing mysql2's sequence validation to emit warnings.
395
+ // By syncing the expected sequence ID to match the incoming packet, we
396
+ // prevent these harmless warnings. This is safe because binlog events
397
+ // track position independently (binlog file + position / GTID).
398
+ // Only applied when compression is enabled to preserve normal error
399
+ // detection for uncompressed connections.
400
+ if (this.connection.config && this.connection.config.compress) {
401
+ // @ts-ignore - internal mysql2 API
402
+ const originalHandlePacket = this.connection.handlePacket.bind(this.connection);
403
+ // @ts-ignore - internal mysql2 API
404
+ this.connection.handlePacket = function(packet) {
405
+ if (packet && typeof packet.sequenceId !== 'undefined') {
406
+ this.sequenceId = packet.sequenceId;
407
+ }
408
+ return originalHandlePacket(packet);
409
+ };
410
+ }
280
411
 
281
- if (this.options.startAtEnd) {
282
- promises.push(new Promise(findBinlogEnd));
283
- }
412
+ // @ts-ignore - internal mysql2 API
413
+ this.connection.addCommand(new this.BinlogClass(binlogHandler));
414
+ })
415
+ .catch(err => {
416
+ this.emit('error', err);
417
+ });
284
418
 
285
- Promise.all(promises)
286
- .then(() => {
287
- this.BinlogClass = initBinlogClass(this);
288
- this.ready = true;
289
- this.emit('ready');
419
+ }
290
420
 
291
- this.connection._protocol._enqueue(
292
- new this.BinlogClass(binlogHandler)
293
- );
294
- })
295
- .catch(err => {
296
- this.emit('error', err);
297
- });
421
+ stop() {
422
+ this.stopped = true;
423
+ this.ready = false;
298
424
 
299
- };
425
+ if (!this.connection && !this.ctrlConnection) {
426
+ this.emit('stopped');
427
+ return;
428
+ }
300
429
 
301
- ZongJi.prototype.stop = function() {
302
- // Binary log connection does not end with destroy()
303
- this.connection.destroy();
304
- this.ctrlConnection.query(
305
- 'KILL ' + this.connection.threadId,
306
- () => {
307
- if (this.ctrlConnectionOwner) {
430
+ // Binary log connection does not end with destroy()
431
+ let connectionThreadId = null;
432
+ if (this.connection) {
433
+ connectionThreadId = this.connection.threadId;
434
+ this.connection.destroy();
435
+ // @ts-ignore - internal mysql2 API
436
+ if (this.connection.stream && typeof this.connection.stream.unref === 'function') {
437
+ // @ts-ignore - internal mysql2 API
438
+ this.connection.stream.unref();
439
+ }
440
+ this.connection = null;
441
+ }
442
+ let finished = false;
443
+ const finish = () => {
444
+ if (finished) return;
445
+ finished = true;
446
+ if (this.ctrlConnectionOwner && this.ctrlConnection) {
308
447
  this.ctrlConnection.destroy();
448
+ // @ts-ignore - internal mysql2 API
449
+ if (this.ctrlConnection.stream && typeof this.ctrlConnection.stream.unref === 'function') {
450
+ // @ts-ignore - internal mysql2 API
451
+ this.ctrlConnection.stream.unref();
452
+ }
453
+ this.ctrlConnection = null;
309
454
  }
310
455
  this.emit('stopped');
456
+ };
457
+
458
+ if (!this.ctrlConnection ||
459
+ this.ctrlConnection.state === 'disconnected' ||
460
+ this.ctrlConnection._fatalError ||
461
+ this.ctrlConnection._protocolError ||
462
+ this.ctrlConnection._closing) {
463
+ this.ctrlConnection = null;
464
+ return finish();
311
465
  }
312
- );
313
- };
314
466
 
315
- // It includes every events by default.
316
- ZongJi.prototype._skipEvent = function(name) {
317
- const includes = this.filters.includeEvents;
318
- const excludes = this.filters.excludeEvents;
467
+ if (!connectionThreadId) {
468
+ return finish();
469
+ }
470
+
471
+ const killTimeout = setTimeout(finish, 1000);
472
+ try {
473
+ this.ctrlConnection.query(
474
+ 'KILL ' + connectionThreadId,
475
+ () => {
476
+ clearTimeout(killTimeout);
477
+ finish();
478
+ }
479
+ );
480
+ } catch {
481
+ clearTimeout(killTimeout);
482
+ finish();
483
+ }
484
+ }
319
485
 
320
- let included = (includes === undefined) ||
321
- (Array.isArray(includes) && (includes.indexOf(name) > -1));
322
- let excluded = Array.isArray(excludes) && (excludes.indexOf(name) > -1);
486
+ // It includes every events by default.
487
+ _skipEvent(name) {
488
+ const includes = this.filters.includeEvents;
489
+ const excludes = this.filters.excludeEvents;
323
490
 
324
- return excluded || !included;
325
- };
491
+ let included = (includes === undefined) ||
492
+ (Array.isArray(includes) && (includes.indexOf(name) > -1));
493
+ let excluded = Array.isArray(excludes) && (excludes.indexOf(name) > -1);
494
+
495
+ return excluded || !included;
496
+ }
326
497
 
327
- // It doesn't skip any schema by default.
328
- ZongJi.prototype._skipSchema = function(database, table) {
329
- const includes = this.filters.includeSchema;
330
- const excludes = this.filters.excludeSchema || {};
498
+ // It doesn't skip any schema by default.
499
+ _skipSchema(database, table) {
500
+ const includes = this.filters.includeSchema;
501
+ const excludes = this.filters.excludeSchema || {};
331
502
 
332
- let included = (includes === undefined) ||
333
- (
334
- (database in includes) &&
503
+ let included = (includes === undefined) ||
335
504
  (
336
- includes[database] === true ||
505
+ (database in includes) &&
337
506
  (
338
- Array.isArray(includes[database]) &&
339
- includes[database].indexOf(table) > -1
507
+ includes[database] === true ||
508
+ (
509
+ Array.isArray(includes[database]) &&
510
+ includes[database].indexOf(table) > -1
511
+ )
340
512
  )
341
- )
342
- );
343
- let excluded = (database in excludes) &&
344
- (
345
- excludes[database] === true ||
513
+ );
514
+ let excluded = (database in excludes) &&
346
515
  (
347
- Array.isArray(excludes[database]) &&
348
- excludes[database].indexOf(table) > -1
349
- )
350
- );
516
+ excludes[database] === true ||
517
+ (
518
+ Array.isArray(excludes[database]) &&
519
+ excludes[database].indexOf(table) > -1
520
+ )
521
+ );
351
522
 
352
- return excluded || !included;
353
- };
523
+ return excluded || !included;
524
+ }
525
+ }
354
526
 
355
- module.exports = ZongJi;
527
+ export default ZongJi;