@vlasky/zongji 0.6.1 → 0.7.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/CHANGELOG.md CHANGED
@@ -2,6 +2,38 @@
2
2
 
3
3
  All notable changes to @vlasky/zongji since forking from nevill/zongji.
4
4
 
5
+ ## [0.7.1] - 2026-07-04
6
+
7
+ - Fix `start()` calls made while a previous `start()` was still initialising silently discarding their filters. Since 0.7.0 filters are snapshotted and re-calling `start()` is the documented way to update them, but updates made between `start()` and the `ready` event were lost; consumers registering tables during boot (e.g. @vlasky/mysql-live-select) missed events for tables added in that window. Filters passed during initialisation now apply, exactly as when already running; stream options (filename/position/serverId) still come from the first call.
8
+ - Fix a resume-position gap that could silently drop row events. `options.position` was advanced past TableMap events on the cached-metadata path, so a consumer persisting `filename`/`position` for reconnect could resume between a TableMap and its row events; the resumed instance had no metadata for the table id and dropped those rows with no error. TableMap events no longer advance the resume position, closing the gap for single-table statements (the common case). A narrower window remains for multi-table statements (multi-table UPDATE, foreign-key cascades), where the server writes all TableMap events before any row events: emitting the first table's rows still advances the position past the later TableMaps. Rows already processed before a crash may be re-delivered after resume (at-least-once), which is recoverable where dropping is not.
9
+ - Fix rotate events corrupting the `filename`/`position` resume pair. A rotate's header position refers to the old binlog file (0 for the artificial rotate at the start of every dump), yet it was written into `options.position` alongside the new file's name; a consumer resuming from that pair after a real rotation could get "position > file size" or a mid-event read, and the artificial rotate silently reset the start position to 0. The rotate's payload position (the start of the new file) is now used, and the filename update is unconditional. Present in every zongji release since the original upstream project.
10
+ - Fix a corrupt GTID event's parse error being swallowed when `gtid` is excluded by `includeEvents`; the whole following transaction was then silently mislabelled as anonymous. The error now reaches the `error` event regardless of filtering.
11
+ - Schema drift between a binlog event being written and the metadata fetch (e.g. a column dropped in between) now emits a descriptive error naming the table and column counts, instead of throwing a bare TypeError from inside event parsing; the affected table's rows are skipped until its next TableMap event refreshes the metadata.
12
+
13
+ ## [0.7.0] - 2026-07-04
14
+
15
+ ### Breaking changes
16
+
17
+ - DECIMAL columns now emit exact string values (e.g. `'-123.4500'`) instead of lossy floats, matching mysql2 query results. Migration: set `decimalNumbers: true` on the connection options passed to ZongJi to restore Numbers.
18
+ - JSON columns now emit parsed JavaScript values instead of JSON strings, matching mysql2 query results. Migration: set `jsonStrings: true` on the connection options passed to ZongJi to restore strings. In string mode, output now uses MySQL's own formatting (spaces after `:` and `,`) and 64-bit integers appear as exact raw numerals rather than lossy doubles.
19
+ - Event and schema filters are snapshotted when `start()` is called; mutating the arrays or objects you passed in no longer changes filtering afterwards. Migration: call `start()` again with the new filters (the documented way to update them).
20
+
21
+ ### Other changes
22
+
23
+ - Fix SQL injection in the table metadata query: schema and table names from TableMap events are now bound via a cached prepared statement (`execute()`) instead of being spliced into SQL text
24
+ - Replace the big-integer dependency with native BigInt (one fewer dependency); also fixes silent corruption of 64-bit integers inside JSON columns beyond 2^53, which now follow the same exact Number-or-string rule as BIGINT columns
25
+ - Emit an error (once per instance per type) when the server sends undecodable TRANSACTION_PAYLOAD_EVENT (`binlog_transaction_compression=ON`) or PARTIAL_UPDATE_ROWS_EVENT (`binlog_row_value_options=PARTIAL_JSON`) events, instead of silently dropping the row changes; remaining MySQL 8 event codes (TRANSACTION_CONTEXT, VIEW_CHANGE, XA_PREPARE, HEARTBEAT_V2) are now named in the code map
26
+ - Lifecycle hardening: emit an explicit error instead of hanging silently when the control connection dies during a metadata fetch; a duplicate `start()` while one is still initialising is ignored, while stop-then-restart during initialisation now works (exactly one binlog dump command is ever enqueued); errors from connections deliberately destroyed by `stop()` are no longer forwarded as teardown noise; errors buffered before an `error` listener attaches are thrown if no listener ever appears, restoring Node's default unhandled `'error'` behaviour
27
+ - DECIMAL parsing no longer mutates the shared network packet buffer when flipping the sign bit
28
+ - Fix `stop()` destroying the control connection of a subsequent `start()`: the asynchronous KILL cleanup now only touches the connections that particular `stop()` owned, so immediate stop-then-restart no longer wedges the new stream on its first metadata fetch
29
+ - The `nonBlock` option declared in the TypeScript definitions is now actually passed through by `start()`; previously it was dropped and the dump command always ran in blocking mode
30
+ - Remove dead code left over from the mysql.js protocol layer (ComBinlog, EofPacket/ErrorPacket, BufferReader)
31
+ - Compile event and schema filters into Sets and Maps for O(1) per-event filtering; only own keys of schema filter objects are considered
32
+ - Add a package.json `exports` map with `types` and `default` conditions
33
+ - All emitted events carry an `event.gtid` property (`'uuid:sequence'`) identifying their transaction when the server runs with `gtid_mode=ON`, tracked at the packet layer so it works even when `gtid` events are excluded by `includeEvents`; `undefined` for anonymous transactions
34
+ - Update mysql2 to ^3.22.5; the internal APIs zongji relies on (addCommand, handlePacket, packet sequence validation) were verified unchanged, and a new regression test covers the binlog stream over a compressed connection
35
+ - Continuous integration now tests Node.js 22, 24 and 26 against MySQL 5.7, 8.0 and 8.4. Node.js 18 and 20 are end-of-life: they remain allowed by `engines` (nothing in the code requires anything newer) and 0.7.0 passed the full test suite on both at release time, but they are no longer tested and future releases may break on them
36
+
5
37
  ## [0.6.1] - 2026-02-13
6
38
 
7
39
  - Updated .gitignore and .npmignore to exclude AI tool and build/test files
package/README.md CHANGED
@@ -9,9 +9,11 @@ MySQL binlog-based change data capture (CDC) for Node.js, [originally created by
9
9
 
10
10
  It leverages [`mysql2`](https://github.com/sidorares/node-mysql2) for connections and authentication, while using zongji's binlog parsing and event pipeline.
11
11
 
12
- # Latest Release
12
+ # Release Notes
13
13
 
14
- Version 0.6.0 is a major modernisation that rewrites the codebase to use the mysql2 module, ES6 syntax and ESM exports, adds TypeScript definitions, and adds official support for MySQL 8.4.
14
+ See the [CHANGELOG](CHANGELOG.md) for the full release history.
15
+
16
+ Version 0.6.0 was a major modernisation that rewrote the codebase to use the mysql2 module, ES6 syntax and ESM exports, added TypeScript definitions, and added official support for MySQL 8.4.
15
17
 
16
18
  Version 0.5.9 is the last release that supports Node.js versions below 18 and CommonJS.
17
19
 
@@ -54,9 +56,17 @@ import ZongJi from '@vlasky/zongji';
54
56
 
55
57
  For a complete implementation see [`example.js`](example.js)...
56
58
 
59
+ ## Module format
60
+
61
+ Since v0.6.0 this package is ESM-only. CommonJS projects can load it on Node.js >= 20.17 or >= 22.12, where `require()` of ES modules is supported, or on any supported version via dynamic `import()`:
62
+
63
+ ```javascript
64
+ const { default: ZongJi } = await import('@vlasky/zongji');
65
+ ```
66
+
57
67
  ## Installation
58
68
 
59
- * Requires Node.js v18+
69
+ * Requires Node.js v18+. Actively supported and tested on Node.js 22, 24 and 26; version 0.7.0 was also verified on Node.js 18 and 20 at release, but those lines are end-of-life and no longer tested.
60
70
 
61
71
  ```bash
62
72
  $ npm install @vlasky/zongji
@@ -93,7 +103,14 @@ The `ZongJi` constructor accepts one argument of either:
93
103
 
94
104
  If a `Connection` or `Pool` object is passed to the constructor, it will not be destroyed/ended by Zongji's `stop()` method.
95
105
 
96
- The `dateStrings` and `timezone` configuration options from the connection details are respected for date/time value handling.
106
+ Binlog row values follow the same [mysql2 connection options](https://sidorares.github.io/node-mysql2/docs/api-and-configurations) as query results, so CDC events and queries on the same connection agree:
107
+
108
+ Option | Effect on row values
109
+ -------|---------------------
110
+ `dateStrings` | `DATE`, `DATETIME` and `TIMESTAMP` columns are returned as strings instead of `Date` objects.
111
+ `timezone` | Applied when converting `DATETIME` and `TIMESTAMP` values to `Date` objects.
112
+ `decimalNumbers` | `DECIMAL` columns are returned as exact strings by default (e.g. `'-123.4500'`); set `decimalNumbers: true` to receive Numbers (may lose precision beyond 15 significant digits).
113
+ `jsonStrings` | `JSON` columns are returned as parsed JavaScript values by default; set `jsonStrings: true` to receive JSON strings.
97
114
 
98
115
  Each instance includes the following methods:
99
116
 
@@ -112,6 +129,8 @@ Event Name | Description
112
129
  `error` | Every error will be caught by this event.
113
130
  `stopped` | Emitted when ZongJi connection is stopped (ZongJi#stop is called).
114
131
 
132
+ Always attach an `error` listener. Errors that occur before a listener attaches (for example, a connection failure in the same tick as construction) are buffered and re-delivered to the first `error` listener. If no listener is ever attached, the buffered errors are thrown, following Node's default behaviour for unhandled `'error'` events.
133
+
115
134
  **Options available:**
116
135
 
117
136
  Option Name | Type | Description
@@ -127,6 +146,8 @@ Option Name | Type | Description
127
146
 
128
147
  * By default, all events and schema are emitted.
129
148
  * `excludeSchema` and `excludeEvents` take precedence over `includeSchema` and `includeEvents`, respectively.
149
+ * Calling `start()` while a previous `start()` is still initialising is ignored and the first call completes. The exception is after an intervening `stop()`: the new `start()` then restarts cleanly, and exactly one binlog stream is opened.
150
+ * Calling `start()` while ZongJi is already running does not reconnect; it only updates the event and schema filters from the given options.
130
151
 
131
152
  **Supported Binlog Events:**
132
153
 
@@ -145,6 +166,10 @@ Event name | Description
145
166
  `writerows` | Rows inserted, row data array available as `rows` property on event object
146
167
  `updaterows` | Rows changed, row data array available as `rows` property on event object
147
168
  `deleterows` | Rows deleted, row data array available as `rows` property on event object
169
+ `transactionpayload` | Compressed transaction from MySQL 8.0.20+ servers with `binlog_transaction_compression=ON`. ZongJi cannot decode the row events inside it, so it emits an `error` (once per instance) naming the server setting responsible.
170
+ `partialupdaterows` | Partial JSON update from MySQL 8.0+ servers with `binlog_row_value_options=PARTIAL_JSON`. ZongJi cannot decode the JSON diff format, so it emits an `error` (once per instance) naming the server setting responsible.
171
+
172
+ When the server runs with `gtid_mode=ON`, every emitted event carries a `gtid` property (`'uuid:sequence'`) identifying the transaction it belongs to, tracked internally even if `gtid` events are excluded by `includeEvents`. It is `undefined` for anonymous transactions and for events seen before the stream's first GTID event. Note that non-row events arriving between a transaction's commit and the next transaction's GTID event (e.g. a rotate) still carry the previous transaction's `gtid`. Use it for checkpointing and deduplication.
148
173
 
149
174
  **Event Methods**
150
175
 
@@ -158,7 +183,7 @@ Name | Description
158
183
  ## Important Notes
159
184
 
160
185
  * :star2: All MySQL column types are supported, with type casting similar to [mysql2](https://github.com/sidorares/node-mysql2).
161
- * :speak_no_evil: 64-bit integer is supported via package big-integer(see #108). If an integer is within the safe range of JS number (-2^53, 2^53), a Number object will returned, otherwise, will return as String.
186
+ * :speak_no_evil: 64-bit integers are decoded exactly using native BigInt (see #108). If an integer is within the safe range of JS numbers (-2^53, 2^53), a Number is returned, otherwise an exact String. This also applies to 64-bit integers inside JSON columns.
162
187
  * :point_right: `TRUNCATE` statement does not cause corresponding `DeleteRows` event. Use unqualified `DELETE FROM` for same effect.
163
188
  * When using fractional seconds with `DATETIME` and `TIMESTAMP` data types, only millisecond precision is available due to the limit of Javascript's `Date` object.
164
189
  * Binlog checksums (e.g. `CRC32`) are supported; zongji will detect and ignore the checksum bytes at the end of row events.
package/example.js CHANGED
@@ -12,6 +12,11 @@ zongji.on('binlog', function(evt) {
12
12
  evt.dump();
13
13
  });
14
14
 
15
+ // Always attach an error listener; without one, errors are thrown
16
+ zongji.on('error', function(err) {
17
+ console.error('zongji error:', err);
18
+ });
19
+
15
20
  zongji.start({
16
21
  includeEvents: ['tablemap', 'writerows', 'updaterows', 'deleterows']
17
22
  });
package/index.d.ts CHANGED
@@ -56,6 +56,13 @@ export interface BinlogEvent {
56
56
  nextPosition: number;
57
57
  /** Size of the event in bytes */
58
58
  size: number;
59
+ /**
60
+ * GTID of the transaction this event belongs to ('uuid:sequence'),
61
+ * when the server runs with gtid_mode=ON. Undefined for anonymous
62
+ * transactions and for events seen before the first GTID event of the
63
+ * stream. On Gtid/AnonymousGtid events this is their own parsed value.
64
+ */
65
+ gtid?: string;
59
66
  /** Get the lowercase event name */
60
67
  getEventName(): string;
61
68
  /** Get the event class name */
@@ -66,6 +73,8 @@ export interface BinlogEvent {
66
73
 
67
74
  // Rotate event - indicates binlog file change
68
75
  export interface RotateEvent extends BinlogEvent {
76
+ getTypeName(): 'Rotate';
77
+ getEventName(): 'rotate';
69
78
  /** Position in the new binlog file */
70
79
  position: number;
71
80
  /** Name of the new binlog file */
@@ -73,10 +82,15 @@ export interface RotateEvent extends BinlogEvent {
73
82
  }
74
83
 
75
84
  // Format Description event
76
- export interface FormatEvent extends BinlogEvent {}
85
+ export interface FormatEvent extends BinlogEvent {
86
+ getTypeName(): 'Format';
87
+ getEventName(): 'format';
88
+ }
77
89
 
78
90
  // GTID event
79
91
  export interface GtidEvent extends BinlogEvent {
92
+ getTypeName(): 'Gtid';
93
+ getEventName(): 'gtid';
80
94
  /** GTID flags */
81
95
  flags: number;
82
96
  /** Source UUID */
@@ -89,6 +103,8 @@ export interface GtidEvent extends BinlogEvent {
89
103
 
90
104
  // Anonymous GTID event
91
105
  export interface AnonymousGtidEvent extends BinlogEvent {
106
+ getTypeName(): 'AnonymousGtid';
107
+ getEventName(): 'anonymousgtid';
92
108
  /** GTID flags */
93
109
  flags: number;
94
110
  /** Source UUID */
@@ -111,6 +127,8 @@ export interface GtidSidEntry {
111
127
  }
112
128
 
113
129
  export interface PreviousGtidsEvent extends BinlogEvent {
130
+ getTypeName(): 'PreviousGtids';
131
+ getEventName(): 'previousgtids';
114
132
  /** Array of SID entries with their intervals */
115
133
  sids: GtidSidEntry[];
116
134
  /** GTID set as a formatted string */
@@ -119,12 +137,16 @@ export interface PreviousGtidsEvent extends BinlogEvent {
119
137
 
120
138
  // XID (commit) event
121
139
  export interface XidEvent extends BinlogEvent {
140
+ getTypeName(): 'Xid';
141
+ getEventName(): 'xid';
122
142
  /** Transaction ID for 2PC */
123
143
  xid: number | string;
124
144
  }
125
145
 
126
146
  // Query event
127
147
  export interface QueryEvent extends BinlogEvent {
148
+ getTypeName(): 'Query';
149
+ getEventName(): 'query';
128
150
  /** Slave proxy ID */
129
151
  slaveProxyId: number;
130
152
  /** Time in seconds the query took to execute */
@@ -145,6 +167,8 @@ export interface QueryEvent extends BinlogEvent {
145
167
 
146
168
  // IntVar event (for statement-based replication)
147
169
  export interface IntVarEvent extends BinlogEvent {
170
+ getTypeName(): 'IntVar';
171
+ getEventName(): 'intvar';
148
172
  /** Variable type: 1=LAST_INSERT_ID, 2=INSERT_ID */
149
173
  type: number;
150
174
  /** The integer value */
@@ -155,6 +179,8 @@ export interface IntVarEvent extends BinlogEvent {
155
179
 
156
180
  // TableMap event
157
181
  export interface TableMapEvent extends BinlogEvent {
182
+ getTypeName(): 'TableMap';
183
+ getEventName(): 'tablemap';
158
184
  /** Internal table ID */
159
185
  tableId: number;
160
186
  /** Table flags */
@@ -200,24 +226,47 @@ export interface RowsEvent extends BinlogEvent {
200
226
 
201
227
  // WriteRows event (INSERT)
202
228
  export interface WriteRowsEvent extends RowsEvent {
229
+ getTypeName(): 'WriteRows';
230
+ getEventName(): 'writerows';
203
231
  /** Array of inserted rows */
204
232
  rows: RowData[];
205
233
  }
206
234
 
207
235
  // DeleteRows event (DELETE)
208
236
  export interface DeleteRowsEvent extends RowsEvent {
237
+ getTypeName(): 'DeleteRows';
238
+ getEventName(): 'deleterows';
209
239
  /** Array of deleted rows */
210
240
  rows: RowData[];
211
241
  }
212
242
 
213
243
  // UpdateRows event (UPDATE)
214
244
  export interface UpdateRowsEvent extends RowsEvent {
245
+ getTypeName(): 'UpdateRows';
246
+ getEventName(): 'updaterows';
215
247
  /** Array of updated rows with before/after values */
216
248
  rows: UpdateRowData[];
217
249
  }
218
250
 
251
+ // Transaction Payload event (MySQL 8.0.20+, binlog_transaction_compression=ON)
252
+ // The compressed transaction body cannot be decoded and is skipped.
253
+ export interface TransactionPayloadEvent extends BinlogEvent {
254
+ getTypeName(): 'TransactionPayload';
255
+ getEventName(): 'transactionpayload';
256
+ }
257
+
258
+ // Partial Update Rows event (MySQL 8.0+, binlog_row_value_options=PARTIAL_JSON)
259
+ // The partial JSON diff body cannot be decoded and is skipped.
260
+ export interface PartialUpdateRowsEvent extends BinlogEvent {
261
+ getTypeName(): 'PartialUpdateRows';
262
+ getEventName(): 'partialupdaterows';
263
+ }
264
+
219
265
  // Unknown event type
220
- export interface UnknownEvent extends BinlogEvent {}
266
+ export interface UnknownEvent extends BinlogEvent {
267
+ getTypeName(): 'Unknown';
268
+ getEventName(): 'unknown';
269
+ }
221
270
 
222
271
  // Union type of all event types
223
272
  export type AnyBinlogEvent =
@@ -233,8 +282,40 @@ export type AnyBinlogEvent =
233
282
  | WriteRowsEvent
234
283
  | DeleteRowsEvent
235
284
  | UpdateRowsEvent
285
+ | TransactionPayloadEvent
286
+ | PartialUpdateRowsEvent
236
287
  | UnknownEvent;
237
288
 
289
+ /** Union of all possible getTypeName() return values (e.g. 'Rotate', 'WriteRows') */
290
+ export type BinlogEventTypeName = ReturnType<AnyBinlogEvent['getTypeName']>;
291
+
292
+ /** Union of all possible getEventName() return values (e.g. 'rotate', 'writerows') */
293
+ export type BinlogEventName = ReturnType<AnyBinlogEvent['getEventName']>;
294
+
295
+ /**
296
+ * Look up the concrete event type for a getTypeName() value.
297
+ * Enables narrowing AnyBinlogEvent via getTypeName(), e.g.:
298
+ *
299
+ * function isEventType<N extends BinlogEventTypeName>(
300
+ * event: AnyBinlogEvent, name: N
301
+ * ): event is BinlogEventByTypeName<N> {
302
+ * return event.getTypeName() === name;
303
+ * }
304
+ *
305
+ * (A plain `switch (event.getTypeName())` cannot narrow `event` because
306
+ * TypeScript does not narrow unions on method-call discriminants.)
307
+ */
308
+ export type BinlogEventByTypeName<N extends BinlogEventTypeName> = Extract<
309
+ AnyBinlogEvent,
310
+ { getTypeName(): N }
311
+ >;
312
+
313
+ /** Look up the concrete event type for a getEventName() value */
314
+ export type BinlogEventByName<N extends BinlogEventName> = Extract<
315
+ AnyBinlogEvent,
316
+ { getEventName(): N }
317
+ >;
318
+
238
319
  // Connection options - can be mysql2 options, Connection, or Pool
239
320
  export type ZongJiDsn = string | ConnectionOptions | PoolOptions | Connection | Pool;
240
321
 
@@ -254,6 +335,7 @@ declare class ZongJi extends EventEmitter {
254
335
  filename?: string;
255
336
  position?: number;
256
337
  startAtEnd?: boolean;
338
+ nonBlock?: boolean;
257
339
  };
258
340
  /** Current filter settings */
259
341
  filters: {