@vlasky/zongji 0.8.0 → 0.9.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.
- package/CHANGELOG.md +7 -0
- package/README.md +12 -0
- package/index.d.ts +76 -3
- package/index.js +5 -0
- package/lib/binlog_event.js +17 -7
- package/lib/common.js +8 -0
- package/lib/gtid_set.js +19 -12
- package/lib/mariadb_gtid.js +58 -17
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to @vlasky/zongji since forking from nevill/zongji.
|
|
4
4
|
|
|
5
|
+
## [0.9.0] - 2026-07-06
|
|
6
|
+
|
|
7
|
+
- Export the `MariadbGtidPosition` class (`import { MariadbGtidPosition } from '@vlasky/zongji'`), the MariaDB counterpart of `GtidSet` and the model behind `zongji.gtidSet` on MariaDB servers: parse a persisted position (a `zongji.gtidSet` checkpoint or `@@gtid_binlog_pos`), build one from a `mariadbgtidlist` event with `fromGtidList()`, extend it with `add()`, and test transaction coverage with `covers(event.gtid, { inclusive })` instead of hand-rolling sequence-number comparators (which silently round above `Number.MAX_SAFE_INTEGER` when done with Number). The inclusive form (default) answers "was this transaction's commit at or before the position?" for snapshot barriers; redelivery watermarks must pass `{ inclusive: false }`, because all events of a transaction share one GTID and an inclusive check would drop every replayed event of the watermark transaction after the first. A server id differing from the position's entry for that domain reports not covered even for a lower sequence number (post-failover sequence numbers can regress under the new server id; for at-least-once consumers an idempotent redelivery is recoverable where a skip is not), as does a domain the position does not list.
|
|
8
|
+
- `GtidSet` and `MariadbGtidPosition` now use BigInt internally, making them exact over MySQL's full GNO range (1 to 2^63-1) and MariaDB's full u64 sequence range. Previously `GtidSet.parse`/`add` threw on GNOs above 2^53, so a snapshot barrier or checkpoint parse failed outright against a server carrying such GTIDs. Public APIs are unchanged: strings in, strings and booleans out.
|
|
9
|
+
- Fix Previous_gtids decoding silently rounding interval bounds above 2^53: the u64 bounds were combined with Number arithmetic, corrupting `event.gtidSet` - the exact text that seeds `zongji.gtidSet` when a dump starts at a binlog file boundary. Bounds are now decoded as BigInt, and the `event.sids` intervals follow the Number-or-exact-string convention used for other 64-bit values (typed `number | string`; previously typed `number` while silently wrong beyond 2^53).
|
|
10
|
+
- Document on the `seqNo` fields of `mariadbgtid` and `mariadbgtidlist` events that values above `Number.MAX_SAFE_INTEGER` arrive as exact strings and must never be compared numerically - use `event.gtid` with `MariadbGtidPosition`/`GtidSet` instead.
|
|
11
|
+
|
|
5
12
|
## [0.8.0] - 2026-07-06
|
|
6
13
|
|
|
7
14
|
- Export the `GtidSet` class (`import ZongJi, { GtidSet } from '@vlasky/zongji'`): parse a persisted set (e.g. a `zongji.gtidSet` checkpoint or `@@gtid_executed`), `add()` transactions, and test single-transaction membership with `contains(event.gtid)` - handy for snapshot drain barriers ("was this event already covered when I took my snapshot?") without maintaining a separate GTID parser. Fully tagged-GTID aware.
|
package/README.md
CHANGED
|
@@ -224,6 +224,18 @@ if (!snapshot.contains(event.gtid)) {
|
|
|
224
224
|
}
|
|
225
225
|
```
|
|
226
226
|
|
|
227
|
+
Its MariaDB counterpart `MariadbGtidPosition` is exported too, with a `covers()` method so consumers do not have to compare sequence numbers themselves (`seqNo` values above `Number.MAX_SAFE_INTEGER` arrive as exact strings, and comparing them as Numbers silently rounds; internally `GtidSet` is exact over MySQL's full GNO range of 1 to 2^63-1 and `MariadbGtidPosition` over MariaDB's full u64 sequence range):
|
|
228
|
+
|
|
229
|
+
```javascript
|
|
230
|
+
import { MariadbGtidPosition } from '@vlasky/zongji';
|
|
231
|
+
|
|
232
|
+
const snapshot = MariadbGtidPosition.parse(savedGtidPos); // or @@gtid_binlog_pos
|
|
233
|
+
snapshot.covers(event.gtid); // at or before the position?
|
|
234
|
+
snapshot.covers(event.gtid, { inclusive: false }); // strictly before it?
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
The default (inclusive) form answers "was this transaction's commit at or before the position?", which is what snapshot-barrier checks want: a recorded position covers the whole of its final transaction. Redelivery watermarks must pass `{ inclusive: false }`: all events of a transaction share one GTID, so an inclusive check would treat the watermark transaction's own replayed events as already seen and drop every one after the first. A server id different from the position's entry for that domain reports **not covered**, even for a lower sequence number: after a failover, sequence numbers can regress under the new server id, so ordering against another server's watermark is not meaningful, and for at-least-once consumers an idempotent redelivery is recoverable where a skip is not. A domain the position does not list is likewise not covered.
|
|
238
|
+
|
|
227
239
|
`zongji.gtidSet` is available when `start()` was given a `gtidSet`, with `startAtEnd` (seeded from the server's `gtid_executed` on MySQL, `gtid_current_pos` on MariaDB), or when reading from the start of a binlog file (seeded from its Previous_gtids or MariaDB GTID list event). It is `undefined` when resuming from an arbitrary mid-file file+position checkpoint, where no exact value can be known. Tagged GTIDs (MySQL 8.3+) are fully supported: tagged transactions appear in the set as `uuid:…:tag:intervals` sections, and a checkpoint containing tags resumes correctly (its wire encoding is only understood by MySQL 8.3+ servers).
|
|
228
240
|
|
|
229
241
|
On MySQL, GTID resume requires `gtid_mode=ON` and a set is a per-source-UUID collection of intervals. On MariaDB, GTIDs are always on and a position is a single watermark per replication domain (`domain-server-sequence`): resuming replays each listed domain after its watermark, and domains not listed replay from the beginning. If the server has purged binlogs your checkpoint still needs, `start()` emits an error (code 1236) naming the problem; MariaDB additionally reports a checkpoint ahead of its binlog as error 1945/1955. A fresh snapshot is then required.
|
package/index.d.ts
CHANGED
|
@@ -151,8 +151,16 @@ export interface AnonymousGtidEvent extends BinlogEvent {
|
|
|
151
151
|
|
|
152
152
|
// Previous GTIDs event
|
|
153
153
|
export interface GtidInterval {
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
/**
|
|
155
|
+
* Interval start GNO, inclusive (exact string above
|
|
156
|
+
* Number.MAX_SAFE_INTEGER)
|
|
157
|
+
*/
|
|
158
|
+
start: number | string;
|
|
159
|
+
/**
|
|
160
|
+
* Interval end GNO, exclusive as on the wire (exact string above
|
|
161
|
+
* Number.MAX_SAFE_INTEGER)
|
|
162
|
+
*/
|
|
163
|
+
end: number | string;
|
|
156
164
|
}
|
|
157
165
|
|
|
158
166
|
export interface GtidSidEntry {
|
|
@@ -360,7 +368,12 @@ export interface HeartbeatEvent extends BinlogEvent {
|
|
|
360
368
|
export interface MariadbGtidEvent extends BinlogEvent {
|
|
361
369
|
getTypeName(): 'MariadbGtid';
|
|
362
370
|
getEventName(): 'mariadbgtid';
|
|
363
|
-
/**
|
|
371
|
+
/**
|
|
372
|
+
* Sequence number within the domain. Arrives as an exact string above
|
|
373
|
+
* Number.MAX_SAFE_INTEGER, so never compare it numerically: use
|
|
374
|
+
* event.gtid with MariadbGtidPosition.covers() (or GtidSet on MySQL)
|
|
375
|
+
* instead.
|
|
376
|
+
*/
|
|
364
377
|
seqNo: number | string;
|
|
365
378
|
/** Replication domain id */
|
|
366
379
|
domainId: number;
|
|
@@ -401,6 +414,11 @@ export interface MariadbGtidEvent extends BinlogEvent {
|
|
|
401
414
|
export interface MariadbGtidListEntry {
|
|
402
415
|
domainId: number;
|
|
403
416
|
serverId: number;
|
|
417
|
+
/**
|
|
418
|
+
* Sequence number within the domain. Arrives as an exact string above
|
|
419
|
+
* Number.MAX_SAFE_INTEGER, so never compare it numerically: use the
|
|
420
|
+
* gtid string with MariadbGtidPosition.covers() instead.
|
|
421
|
+
*/
|
|
404
422
|
seqNo: number | string;
|
|
405
423
|
/** Full GTID string (domain-server-sequence) */
|
|
406
424
|
gtid: string;
|
|
@@ -671,3 +689,58 @@ export class GtidSet {
|
|
|
671
689
|
/** Canonical text form (sorted; tagged intervals after untagged) */
|
|
672
690
|
toString(): string;
|
|
673
691
|
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* A MariaDB GTID position (the model behind zongji.gtidSet on MariaDB):
|
|
695
|
+
* the last transaction processed per replication domain. String form
|
|
696
|
+
* matches @@gtid_binlog_pos / @@gtid_current_pos, e.g. '0-1-1234,1-3-45'.
|
|
697
|
+
* Exported so consumers can parse persisted positions and test coverage
|
|
698
|
+
* via covers() instead of hand-rolling seqNo comparators (which silently
|
|
699
|
+
* round above Number.MAX_SAFE_INTEGER when done with Number; internally
|
|
700
|
+
* this class is exact over the full u64 sequence range).
|
|
701
|
+
*/
|
|
702
|
+
export class MariadbGtidPosition {
|
|
703
|
+
/**
|
|
704
|
+
* Parses a GTID position string ('' gives an empty position). Rejects
|
|
705
|
+
* two entries for one domain, as the server would. Throws on malformed
|
|
706
|
+
* input.
|
|
707
|
+
*/
|
|
708
|
+
static parse(text: string): MariadbGtidPosition;
|
|
709
|
+
/**
|
|
710
|
+
* Builds the position implied by a MariadbGtidList event's gtids
|
|
711
|
+
* array: the last entry per domain wins
|
|
712
|
+
*/
|
|
713
|
+
static fromGtidList(entries: Array<{
|
|
714
|
+
domainId: number;
|
|
715
|
+
serverId: number;
|
|
716
|
+
seqNo: number | string;
|
|
717
|
+
}>): MariadbGtidPosition;
|
|
718
|
+
/**
|
|
719
|
+
* Records a transaction as the last processed in its domain
|
|
720
|
+
* (an overwrite, not a max: "last processed" survives failovers where
|
|
721
|
+
* sequence numbers regress under a new server id)
|
|
722
|
+
*/
|
|
723
|
+
add(gtid: {
|
|
724
|
+
domainId: number;
|
|
725
|
+
serverId: number;
|
|
726
|
+
seqNo: number | string;
|
|
727
|
+
}): void;
|
|
728
|
+
/**
|
|
729
|
+
* Whether a transaction ('domain-server-sequence', e.g. an event.gtid)
|
|
730
|
+
* is covered by this position. inclusive (default true) counts the
|
|
731
|
+
* position's own last transaction as covered (snapshot-barrier
|
|
732
|
+
* semantics); redelivery watermarks must pass { inclusive: false },
|
|
733
|
+
* because all events of a transaction share one GTID and an inclusive
|
|
734
|
+
* check would drop every replayed event of the watermark transaction
|
|
735
|
+
* after the first. A server-id mismatch within a domain and an unknown
|
|
736
|
+
* domain both report not covered; null/undefined is never covered; a
|
|
737
|
+
* malformed string throws.
|
|
738
|
+
*/
|
|
739
|
+
covers(
|
|
740
|
+
gtid: string | null | undefined,
|
|
741
|
+
options?: { inclusive?: boolean },
|
|
742
|
+
): boolean;
|
|
743
|
+
isEmpty(): boolean;
|
|
744
|
+
/** Text form, sorted by domain ('D-S-N[,D-S-N...]') */
|
|
745
|
+
toString(): string;
|
|
746
|
+
}
|
package/index.js
CHANGED
|
@@ -989,3 +989,8 @@ export default ZongJi;
|
|
|
989
989
|
// already covered by my snapshot?") without their own parser. Handles
|
|
990
990
|
// tagged GTIDs (MySQL 8.3+).
|
|
991
991
|
export { GtidSet };
|
|
992
|
+
// Its MariaDB counterpart: the position model behind zongji.gtidSet on
|
|
993
|
+
// MariaDB servers, exported so consumers can parse persisted positions
|
|
994
|
+
// and test coverage via covers() instead of hand-rolling seqNo
|
|
995
|
+
// comparators (which silently round beyond 2^53 when done with Number).
|
|
996
|
+
export { MariadbGtidPosition };
|
package/lib/binlog_event.js
CHANGED
|
@@ -230,8 +230,7 @@ class Gtid extends BinlogEvent {
|
|
|
230
230
|
parser.parseUnsignedNumber(1);
|
|
231
231
|
const gno =
|
|
232
232
|
Common.parseSerializationVarintSigned(parser, messageEnd, what);
|
|
233
|
-
this.gno = gno
|
|
234
|
-
gno <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(gno) : String(gno);
|
|
233
|
+
this.gno = Common.bigintToNumberOrString(gno);
|
|
235
234
|
this.tag = '';
|
|
236
235
|
if (nextFieldIs(3)) {
|
|
237
236
|
parser.parseUnsignedNumber(1);
|
|
@@ -307,13 +306,24 @@ class PreviousGtids extends BinlogEvent {
|
|
|
307
306
|
'PreviousGtids');
|
|
308
307
|
const intervals = [];
|
|
309
308
|
for (let j = 0; j < intervalCount; j++) {
|
|
310
|
-
|
|
311
|
-
|
|
309
|
+
// u64 bounds as BigInt: parseUnsignedNumber(8) would silently
|
|
310
|
+
// round beyond 2^53, corrupting the gtidSet text this event seeds
|
|
311
|
+
// the executed-set tracker with
|
|
312
|
+
const start = parser.parseBuffer(8).readBigUInt64LE(0);
|
|
313
|
+
const end = parser.parseBuffer(8).readBigUInt64LE(0);
|
|
312
314
|
intervals.push({ start, end });
|
|
313
315
|
}
|
|
314
316
|
sids.push(tagged ? { sid, tag, intervals } : { sid, intervals });
|
|
315
317
|
}
|
|
316
|
-
|
|
318
|
+
// The public sids follow the Number-or-exact-string convention for
|
|
319
|
+
// 64-bit values (events must stay JSON-serialisable, so no BigInt)
|
|
320
|
+
this.sids = sids.map(entry => ({
|
|
321
|
+
...entry,
|
|
322
|
+
intervals: entry.intervals.map(({ start, end }) => ({
|
|
323
|
+
start: Common.bigintToNumberOrString(start),
|
|
324
|
+
end: Common.bigintToNumberOrString(end),
|
|
325
|
+
})),
|
|
326
|
+
}));
|
|
317
327
|
// Entries sharing a uuid print as one block with the untagged
|
|
318
328
|
// intervals first: 'uuid:1-5:tag_a:1:tag_b:1', matching
|
|
319
329
|
// @@gtid_executed. The server writes entries in that order already;
|
|
@@ -329,9 +339,9 @@ class PreviousGtids extends BinlogEvent {
|
|
|
329
339
|
group.sort((a, b) => (a.tag || '') < (b.tag || '') ? -1 : 1);
|
|
330
340
|
return sid + group.map(entry => {
|
|
331
341
|
const ranges = entry.intervals.map(interval =>
|
|
332
|
-
interval.start === interval.end -
|
|
342
|
+
interval.start === interval.end - 1n ?
|
|
333
343
|
`${interval.start}` :
|
|
334
|
-
`${interval.start}-${interval.end -
|
|
344
|
+
`${interval.start}-${interval.end - 1n}`);
|
|
335
345
|
return (entry.tag ? `:${entry.tag}` : '') + `:${ranges.join(':')}`;
|
|
336
346
|
}).join('');
|
|
337
347
|
}).join(',');
|
package/lib/common.js
CHANGED
|
@@ -58,6 +58,14 @@ export function unsignedInt64ToNumberOrString(high, low) {
|
|
|
58
58
|
return value <= MAX_SAFE_BIGINT ? Number(value) : value.toString();
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// An exact BigInt to the Number-or-exact-string convention used for
|
|
62
|
+
// 64-bit values: a Number within the safe integer range, otherwise an
|
|
63
|
+
// exact decimal string.
|
|
64
|
+
export function bigintToNumberOrString(value) {
|
|
65
|
+
return (value >= MIN_SAFE_BIGINT && value <= MAX_SAFE_BIGINT) ?
|
|
66
|
+
Number(value) : value.toString();
|
|
67
|
+
}
|
|
68
|
+
|
|
61
69
|
// Combine two unsigned 32-bit halves into an exact signed (two's
|
|
62
70
|
// complement) 64-bit value. Returns a Number when within the safe integer
|
|
63
71
|
// range, otherwise an exact string.
|
package/lib/gtid_set.js
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
// MySQL 8.3+ GTIDs may carry a tag; a (uuid, tag) pair is its own
|
|
6
6
|
// source, printed merged into the uuid's block after the untagged
|
|
7
7
|
// intervals: 'uuid:1-5:tag_a:1:tag_b:1' (tags lowercase, sorted).
|
|
8
|
-
// Intervals are stored internally as [start, end] inclusive, kept
|
|
9
|
-
// and coalesced
|
|
10
|
-
//
|
|
8
|
+
// Intervals are stored internally as [start, end] inclusive BigInts, kept
|
|
9
|
+
// sorted and coalesced, so the full GNO range MySQL allows (1 to 2^63-1)
|
|
10
|
+
// is handled exactly; the public API stays strings in, strings and
|
|
11
|
+
// booleans out.
|
|
11
12
|
class GtidSet {
|
|
12
13
|
constructor() {
|
|
13
14
|
// uuid (lowercase, dashed) -> Map of tag ('' = untagged) ->
|
|
@@ -97,13 +98,17 @@ class GtidSet {
|
|
|
97
98
|
}
|
|
98
99
|
const existing = tags.get(tag) || [];
|
|
99
100
|
existing.push([start, end]);
|
|
100
|
-
|
|
101
|
+
// BigInt bounds: a subtraction comparator would hand sort() a BigInt,
|
|
102
|
+
// which ToNumber rejects
|
|
103
|
+
existing.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
|
|
101
104
|
const merged = [existing[0]];
|
|
102
105
|
for (let i = 1; i < existing.length; i++) {
|
|
103
106
|
const last = merged[merged.length - 1];
|
|
104
107
|
const next = existing[i];
|
|
105
|
-
if (next[0] <= last[1] +
|
|
106
|
-
|
|
108
|
+
if (next[0] <= last[1] + 1n) {
|
|
109
|
+
if (next[1] > last[1]) {
|
|
110
|
+
last[1] = next[1];
|
|
111
|
+
}
|
|
107
112
|
} else {
|
|
108
113
|
merged.push(next);
|
|
109
114
|
}
|
|
@@ -178,8 +183,8 @@ class GtidSet {
|
|
|
178
183
|
}
|
|
179
184
|
offset = buffer.writeBigUInt64LE(BigInt(intervals.length), offset);
|
|
180
185
|
for (const [start, end] of intervals) {
|
|
181
|
-
offset = buffer.writeBigUInt64LE(
|
|
182
|
-
offset = buffer.writeBigUInt64LE(
|
|
186
|
+
offset = buffer.writeBigUInt64LE(start, offset);
|
|
187
|
+
offset = buffer.writeBigUInt64LE(end + 1n, offset);
|
|
183
188
|
}
|
|
184
189
|
}
|
|
185
190
|
return buffer;
|
|
@@ -222,13 +227,15 @@ function normaliseUuid(text) {
|
|
|
222
227
|
}
|
|
223
228
|
|
|
224
229
|
function parsePositiveInt(text, context) {
|
|
225
|
-
// Decimal digits only, per the MySQL GTID grammar:
|
|
226
|
-
// also accept '
|
|
230
|
+
// Decimal digits only, per the MySQL GTID grammar: BigInt() alone would
|
|
231
|
+
// also accept '0x10' or '0b1' and silently reinterpret them
|
|
227
232
|
if (!/^\d+$/.test(text)) {
|
|
228
233
|
throw new Error(`Invalid GTID number in '${context}': '${text}'`);
|
|
229
234
|
}
|
|
230
|
-
const value =
|
|
231
|
-
|
|
235
|
+
const value = BigInt(text);
|
|
236
|
+
// GNOs are signed 64-bit on the server and start at 1
|
|
237
|
+
// (GNO_END = 2^63 - 1)
|
|
238
|
+
if (value < 1n || value > 0x7fffffffffffffffn) {
|
|
232
239
|
throw new Error(`Invalid GTID number in '${context}': '${text}'`);
|
|
233
240
|
}
|
|
234
241
|
return value;
|
package/lib/mariadb_gtid.js
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
// are streamed from the beginning.
|
|
10
10
|
class MariadbGtidPosition {
|
|
11
11
|
constructor() {
|
|
12
|
-
// domainId (number) -> { domainId, serverId, seqNo }
|
|
12
|
+
// domainId (number) -> { domainId, serverId, seqNo }; seqNo is a
|
|
13
|
+
// BigInt so the full u64 sequence range stays exact
|
|
13
14
|
this._domains = new Map();
|
|
14
15
|
}
|
|
15
16
|
|
|
@@ -21,13 +22,7 @@ class MariadbGtidPosition {
|
|
|
21
22
|
return position;
|
|
22
23
|
}
|
|
23
24
|
for (const entry of String(text).split(',')) {
|
|
24
|
-
const
|
|
25
|
-
if (parts.length !== 3) {
|
|
26
|
-
throw new Error(`Invalid MariaDB GTID: '${entry.trim()}'`);
|
|
27
|
-
}
|
|
28
|
-
const domainId = parseUInt32(parts[0], entry);
|
|
29
|
-
const serverId = parseUInt32(parts[1], entry);
|
|
30
|
-
const seqNo = parseSeqNo(parts[2], entry);
|
|
25
|
+
const { domainId, serverId, seqNo } = parseSingleGtid(entry);
|
|
31
26
|
if (position._domains.has(domainId)) {
|
|
32
27
|
throw new Error(
|
|
33
28
|
`Duplicate replication domain ${domainId} in GTID position ` +
|
|
@@ -59,10 +54,46 @@ class MariadbGtidPosition {
|
|
|
59
54
|
this._domains.set(gtid.domainId, {
|
|
60
55
|
domainId: gtid.domainId,
|
|
61
56
|
serverId: gtid.serverId,
|
|
62
|
-
|
|
57
|
+
// event.seqNo follows the Number-or-exact-string convention for
|
|
58
|
+
// 64-bit values; BigInt accepts both and stays exact beyond 2^53
|
|
59
|
+
seqNo: BigInt(gtid.seqNo),
|
|
63
60
|
});
|
|
64
61
|
}
|
|
65
62
|
|
|
63
|
+
// Whether a transaction ('domain-server-sequence', e.g. an event.gtid)
|
|
64
|
+
// is covered by this position. A position names the last transaction
|
|
65
|
+
// processed per domain, and within a domain the binlog order is total,
|
|
66
|
+
// so any earlier sequence number from the same server is covered.
|
|
67
|
+
//
|
|
68
|
+
// inclusive (default true) decides whether the position's own last
|
|
69
|
+
// transaction counts as covered. A recorded position means that
|
|
70
|
+
// transaction completed, so snapshot-barrier checks ("did my snapshot
|
|
71
|
+
// already include this commit?") want the default. Redelivery
|
|
72
|
+
// watermarks must pass { inclusive: false }: all events of a
|
|
73
|
+
// transaction share one GTID, so an inclusive check would treat the
|
|
74
|
+
// watermark transaction's own replayed events as already seen and drop
|
|
75
|
+
// every one after the first.
|
|
76
|
+
//
|
|
77
|
+
// A server-id mismatch within a domain reports NOT covered even for a
|
|
78
|
+
// lower sequence number: after a failover, sequence numbers can regress
|
|
79
|
+
// under the new server id, so ordering against another server's
|
|
80
|
+
// watermark is not meaningful; for at-least-once consumers an
|
|
81
|
+
// idempotent redelivery is recoverable where a skip is not. An unknown
|
|
82
|
+
// domain is likewise not covered. null/undefined (an event.gtid seen
|
|
83
|
+
// before the stream's first GTID event) is never covered; a malformed
|
|
84
|
+
// string throws, as it would corrupt rather than answer.
|
|
85
|
+
covers(gtid, { inclusive = true } = {}) {
|
|
86
|
+
if (gtid === undefined || gtid === null) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
const { domainId, serverId, seqNo } = parseSingleGtid(gtid);
|
|
90
|
+
const last = this._domains.get(domainId);
|
|
91
|
+
if (!last || last.serverId !== serverId) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
return inclusive ? seqNo <= last.seqNo : seqNo < last.seqNo;
|
|
95
|
+
}
|
|
96
|
+
|
|
66
97
|
isEmpty() {
|
|
67
98
|
return this._domains.size === 0;
|
|
68
99
|
}
|
|
@@ -76,6 +107,19 @@ class MariadbGtidPosition {
|
|
|
76
107
|
}
|
|
77
108
|
}
|
|
78
109
|
|
|
110
|
+
function parseSingleGtid(gtid) {
|
|
111
|
+
const entry = String(gtid).trim();
|
|
112
|
+
const parts = entry.split('-');
|
|
113
|
+
if (parts.length !== 3) {
|
|
114
|
+
throw new Error(`Invalid MariaDB GTID: '${entry}'`);
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
domainId: parseUInt32(parts[0], entry),
|
|
118
|
+
serverId: parseUInt32(parts[1], entry),
|
|
119
|
+
seqNo: parseSeqNo(parts[2], entry),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
79
123
|
function parseUInt32(text, context) {
|
|
80
124
|
// Decimal digits only: Number() alone would also accept '1e3' or '0x10'
|
|
81
125
|
if (!/^\d+$/.test(text)) {
|
|
@@ -89,19 +133,16 @@ function parseUInt32(text, context) {
|
|
|
89
133
|
}
|
|
90
134
|
|
|
91
135
|
function parseSeqNo(text, context) {
|
|
136
|
+
// Decimal digits only: BigInt() alone would also accept '0x10' or '0b1'
|
|
92
137
|
if (!/^\d+$/.test(text)) {
|
|
93
138
|
throw new Error(`Invalid MariaDB GTID in '${context}': '${text}'`);
|
|
94
139
|
}
|
|
95
|
-
// seq_no is u64;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (Number.isSafeInteger(value)) {
|
|
99
|
-
return value;
|
|
100
|
-
}
|
|
101
|
-
if (BigInt(text) > 0xffffffffffffffffn) {
|
|
140
|
+
// seq_no is u64; BigInt keeps it exact beyond 2^53
|
|
141
|
+
const value = BigInt(text);
|
|
142
|
+
if (value > 0xffffffffffffffffn) {
|
|
102
143
|
throw new Error(`Invalid MariaDB GTID in '${context}': '${text}'`);
|
|
103
144
|
}
|
|
104
|
-
return
|
|
145
|
+
return value;
|
|
105
146
|
}
|
|
106
147
|
|
|
107
148
|
export { MariadbGtidPosition };
|