@vlasky/zongji 0.5.9 → 0.6.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 +60 -0
- package/README.md +48 -20
- package/docker-compose.yml +14 -13
- package/docker-test.sh +7 -7
- package/eslint.config.js +35 -0
- package/example.js +1 -1
- package/index.d.ts +314 -0
- package/index.js +444 -272
- package/jsconfig.json +15 -0
- package/lib/binlog_event.js +296 -217
- package/lib/code_map.js +7 -4
- package/lib/common.js +35 -22
- package/lib/datetime_decode.js +147 -34
- package/lib/json_decode.js +12 -6
- package/lib/packet/binlog.js +3 -3
- package/lib/packet/combinlog.js +22 -20
- package/lib/packet/index.js +50 -50
- package/lib/reader.js +215 -109
- package/lib/rows_event.js +98 -102
- package/lib/sequence/binlog.js +142 -39
- package/package.json +16 -11
- package/scripts/start-mysql.sh +28 -0
- package/.eslintrc +0 -19
package/lib/sequence/binlog.js
CHANGED
|
@@ -1,56 +1,159 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { initBinlogPacketClass } from '../packet/index.js';
|
|
3
|
+
import { Parser } from '../reader.js';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
const BINLOG_DUMP_COMMAND = 0x12;
|
|
6
|
+
|
|
7
|
+
class Command extends EventEmitter {
|
|
8
|
+
constructor() {
|
|
9
|
+
super();
|
|
10
|
+
this.next = null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
execute(packet, connection) {
|
|
14
|
+
if (!this.next) {
|
|
15
|
+
// @ts-ignore - start is defined in subclass
|
|
16
|
+
this.next = this.start;
|
|
17
|
+
connection._resetSequenceId();
|
|
18
|
+
}
|
|
19
|
+
if (packet && packet.isError && packet.isError()) {
|
|
20
|
+
const err = packet.asError(connection.clientEncoding);
|
|
21
|
+
// @ts-ignore - onResult is an optional callback property
|
|
22
|
+
if (this.onResult) {
|
|
23
|
+
// @ts-ignore
|
|
24
|
+
this.onResult(err);
|
|
25
|
+
this.emit('end');
|
|
26
|
+
} else {
|
|
27
|
+
this.emit('error', err);
|
|
28
|
+
this.emit('end');
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
this.next = this.next(packet, connection);
|
|
33
|
+
if (this.next) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
this.emit('end');
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class SimplePacket {
|
|
42
|
+
constructor(length) {
|
|
43
|
+
this.buffer = Buffer.allocUnsafe(length);
|
|
44
|
+
this.offset = 4;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
length() {
|
|
48
|
+
return this.buffer.length;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
writeInt8(value) {
|
|
52
|
+
this.buffer.writeUInt8(value, this.offset);
|
|
53
|
+
this.offset += 1;
|
|
54
|
+
}
|
|
7
55
|
|
|
8
|
-
|
|
9
|
-
|
|
56
|
+
writeInt16(value) {
|
|
57
|
+
this.buffer.writeUInt16LE(value, this.offset);
|
|
58
|
+
this.offset += 2;
|
|
10
59
|
}
|
|
11
60
|
|
|
12
|
-
|
|
61
|
+
writeInt24(value) {
|
|
62
|
+
this.buffer.writeUIntLE(value, this.offset, 3);
|
|
63
|
+
this.offset += 3;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
writeInt32(value) {
|
|
67
|
+
this.buffer.writeUInt32LE(value, this.offset);
|
|
68
|
+
this.offset += 4;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
writeNullTerminatedString(value, encoding) {
|
|
72
|
+
const buf = Buffer.from(value, encoding);
|
|
73
|
+
buf.copy(this.buffer, this.offset);
|
|
74
|
+
this.offset += buf.length;
|
|
75
|
+
this.writeInt8(0);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
writeHeader(sequenceId) {
|
|
79
|
+
const offset = this.offset;
|
|
80
|
+
this.offset = 0;
|
|
81
|
+
this.writeInt24(this.buffer.length - 4);
|
|
82
|
+
this.writeInt8(sequenceId);
|
|
83
|
+
this.offset = offset;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
13
86
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
let options = zongji.get([
|
|
17
|
-
'serverId', 'position', 'filename', 'nonBlock',
|
|
18
|
-
]);
|
|
19
|
-
this.emit('packet', new ComBinlog(options));
|
|
20
|
-
};
|
|
87
|
+
export default function initBinlogClass(zongji) {
|
|
88
|
+
const BinlogPacket = initBinlogPacketClass(zongji);
|
|
21
89
|
|
|
22
|
-
Binlog
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
case 0xff:
|
|
27
|
-
return ErrorPacket;
|
|
28
|
-
default:
|
|
29
|
-
return BinlogPacket;
|
|
90
|
+
class Binlog extends Command {
|
|
91
|
+
constructor(callback) {
|
|
92
|
+
super();
|
|
93
|
+
this._callback = callback;
|
|
30
94
|
}
|
|
31
|
-
};
|
|
32
95
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
96
|
+
start(packet, connection) {
|
|
97
|
+
const options = zongji.get([
|
|
98
|
+
'serverId', 'position', 'filename', 'nonBlock',
|
|
99
|
+
]);
|
|
100
|
+
const binlogPos = options.position || 4;
|
|
101
|
+
const serverId = options.serverId || 1;
|
|
102
|
+
const flags = options.nonBlock ? 1 : 0;
|
|
103
|
+
const filename = options.filename || '';
|
|
36
104
|
|
|
37
|
-
|
|
38
|
-
|
|
105
|
+
const length = 16 + Buffer.byteLength(filename, 'utf8');
|
|
106
|
+
const outPacket = new SimplePacket(length);
|
|
107
|
+
outPacket.writeInt8(BINLOG_DUMP_COMMAND);
|
|
108
|
+
outPacket.writeInt32(binlogPos);
|
|
109
|
+
outPacket.writeInt16(flags);
|
|
110
|
+
outPacket.writeInt32(serverId);
|
|
111
|
+
outPacket.writeNullTerminatedString(filename, 'utf8');
|
|
112
|
+
connection.writePacket(outPacket);
|
|
113
|
+
return Binlog.prototype.binlogData;
|
|
114
|
+
}
|
|
39
115
|
|
|
40
|
-
|
|
41
|
-
if (
|
|
42
|
-
|
|
116
|
+
binlogData(packet, connection) {
|
|
117
|
+
if (packet.isEOF()) {
|
|
118
|
+
this.emit('eof');
|
|
119
|
+
return null;
|
|
43
120
|
}
|
|
44
121
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
122
|
+
if (packet.isError()) {
|
|
123
|
+
const err = packet.asError(connection.clientEncoding);
|
|
124
|
+
if (this._callback) {
|
|
125
|
+
this._callback.call(this, err);
|
|
126
|
+
} else {
|
|
127
|
+
this.emit('error', err);
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
50
130
|
}
|
|
51
|
-
|
|
131
|
+
|
|
132
|
+
const parser = new Parser(packet);
|
|
133
|
+
const binlogPacket = new BinlogPacket();
|
|
134
|
+
binlogPacket.parse(parser);
|
|
135
|
+
|
|
136
|
+
if (this._callback) {
|
|
137
|
+
const eventName = binlogPacket.eventName
|
|
138
|
+
? binlogPacket.eventName.toLowerCase()
|
|
139
|
+
: 'unknown';
|
|
140
|
+
if (zongji._skipEvent(eventName)) {
|
|
141
|
+
return Binlog.prototype.binlogData;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let event;
|
|
145
|
+
let error;
|
|
146
|
+
try {
|
|
147
|
+
event = binlogPacket.getEvent();
|
|
148
|
+
} catch (err) {
|
|
149
|
+
error = err;
|
|
150
|
+
}
|
|
151
|
+
this._callback.call(this, error, event);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return Binlog.prototype.binlogData;
|
|
52
155
|
}
|
|
53
|
-
}
|
|
156
|
+
}
|
|
54
157
|
|
|
55
158
|
return Binlog;
|
|
56
159
|
};
|
package/package.json
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vlasky/zongji",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.6.1",
|
|
4
|
+
"description": "MySQL binlog-based change data capture (CDC) for Node.js",
|
|
5
|
+
"type": "module",
|
|
5
6
|
"main": "index.js",
|
|
7
|
+
"types": "index.d.ts",
|
|
6
8
|
"directories": {
|
|
7
9
|
"test": "test"
|
|
8
10
|
},
|
|
9
11
|
"scripts": {
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
+
"pretest": "bash scripts/start-mysql.sh",
|
|
13
|
+
"test": "tap --bail --disable-coverage --jobs=1 test/*.js",
|
|
14
|
+
"lint": "eslint .",
|
|
15
|
+
"checkjs": "tsc --project jsconfig.json"
|
|
12
16
|
},
|
|
13
17
|
"repository": {
|
|
14
18
|
"type": "git",
|
|
15
|
-
"url": "https://github.com/vlasky/zongji"
|
|
19
|
+
"url": "git+https://github.com/vlasky/zongji.git"
|
|
16
20
|
},
|
|
17
21
|
"keywords": [
|
|
18
22
|
"mysql",
|
|
@@ -30,15 +34,16 @@
|
|
|
30
34
|
],
|
|
31
35
|
"license": "MIT",
|
|
32
36
|
"engines": {
|
|
33
|
-
"node": ">=
|
|
37
|
+
"node": ">= 18.0"
|
|
34
38
|
},
|
|
35
39
|
"devDependencies": {
|
|
36
|
-
"eslint": "
|
|
37
|
-
"
|
|
40
|
+
"@eslint/js": "^9.39.2",
|
|
41
|
+
"eslint": "^9.39.2",
|
|
42
|
+
"tap": "^21.5.0"
|
|
38
43
|
},
|
|
39
44
|
"dependencies": {
|
|
40
|
-
"big-integer": "1.6.
|
|
41
|
-
"iconv-lite": "0.
|
|
42
|
-
"
|
|
45
|
+
"big-integer": "1.6.52",
|
|
46
|
+
"iconv-lite": "0.7.2",
|
|
47
|
+
"mysql2": "^3.17.1"
|
|
43
48
|
}
|
|
44
49
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Start MySQL Docker containers for testing
|
|
3
|
+
# Skips MySQL 5.7 on ARM64 (Apple Silicon) as no image is available
|
|
4
|
+
|
|
5
|
+
if [ "$(uname -m)" = "arm64" ]; then
|
|
6
|
+
SERVICES="mysql80 mysql84"
|
|
7
|
+
else
|
|
8
|
+
SERVICES="mysql57 mysql80 mysql84"
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
docker-compose up -d $SERVICES
|
|
12
|
+
|
|
13
|
+
# Wait for MySQL to be ready
|
|
14
|
+
for service in $SERVICES; do
|
|
15
|
+
container="zongji-${service}-1"
|
|
16
|
+
echo -n "Waiting for ${service}..."
|
|
17
|
+
for i in $(seq 1 30); do
|
|
18
|
+
if docker exec "$container" mysqladmin ping -u root -psecret --silent 2>/dev/null; then
|
|
19
|
+
echo " ready"
|
|
20
|
+
break
|
|
21
|
+
fi
|
|
22
|
+
if [ "$i" -eq 30 ]; then
|
|
23
|
+
echo " timed out"
|
|
24
|
+
exit 1
|
|
25
|
+
fi
|
|
26
|
+
sleep 2
|
|
27
|
+
done
|
|
28
|
+
done
|
package/.eslintrc
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"env": {
|
|
3
|
-
"node": true,
|
|
4
|
-
"es2020": true
|
|
5
|
-
},
|
|
6
|
-
"extends": "eslint:recommended",
|
|
7
|
-
"rules": {
|
|
8
|
-
"comma-dangle": ["warn", "only-multiline"],
|
|
9
|
-
"eol-last": ["error"],
|
|
10
|
-
"keyword-spacing": ["error", { "before": true } ],
|
|
11
|
-
"no-console": "off",
|
|
12
|
-
"no-trailing-spaces": ["error", { "skipBlankLines": true }],
|
|
13
|
-
"no-unused-vars": "warn",
|
|
14
|
-
"no-var": "warn",
|
|
15
|
-
"quotes": ["warn", "single", "avoid-escape"],
|
|
16
|
-
"semi": ["error", "always"],
|
|
17
|
-
"space-before-blocks": "error"
|
|
18
|
-
}
|
|
19
|
-
}
|