@powersync/service-module-mysql 0.0.0-dev-20241015210820 → 0.0.0-dev-20241021185145

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.
@@ -34,6 +34,12 @@ interface WriteChangePayload {
34
34
 
35
35
  export type Data = Record<string, any>;
36
36
 
37
+ export class BinlogConfigurationError extends Error {
38
+ constructor(message: string) {
39
+ super(message);
40
+ }
41
+ }
42
+
37
43
  /**
38
44
  * MySQL does not have same relation structure. Just returning unique key as string.
39
45
  * @param source
@@ -301,6 +307,7 @@ AND table_type = 'BASE TABLE';`,
301
307
  // all connections automatically closed, including this one.
302
308
  await this.initReplication();
303
309
  await this.streamChanges();
310
+ logger.info('BinlogStream has been shut down');
304
311
  } catch (e) {
305
312
  await this.storage.reportError(e);
306
313
  throw e;
@@ -309,9 +316,13 @@ AND table_type = 'BASE TABLE';`,
309
316
 
310
317
  async initReplication() {
311
318
  const connection = await this.connections.getConnection();
312
- await common.checkSourceConfiguration(connection);
319
+ const errors = await common.checkSourceConfiguration(connection);
313
320
  connection.release();
314
321
 
322
+ if (errors.length > 0) {
323
+ throw new BinlogConfigurationError(`Binlog Configuration Errors: ${errors.join(', ')}`);
324
+ }
325
+
315
326
  const initialReplicationCompleted = await this.checkInitialReplicated();
316
327
  if (!initialReplicationCompleted) {
317
328
  await this.startInitialReplication();
@@ -342,135 +353,148 @@ AND table_type = 'BASE TABLE';`,
342
353
  const binLogPositionState = fromGTID.position;
343
354
  connection.release();
344
355
 
345
- await this.storage.startBatch(
346
- { zeroLSN: ReplicatedGTID.ZERO.comparable, defaultSchema: this.defaultSchema },
347
- async (batch) => {
348
- const zongji = this.connections.createBinlogListener();
349
-
350
- let currentGTID: common.ReplicatedGTID | null = null;
351
-
352
- const queue = async.queue(async (evt: BinLogEvent) => {
353
- // State machine
354
- switch (true) {
355
- case zongji_utils.eventIsGTIDLog(evt):
356
- currentGTID = common.ReplicatedGTID.fromBinLogEvent({
357
- raw_gtid: {
358
- server_id: evt.serverId,
359
- transaction_range: evt.transactionRange
360
- },
361
- position: {
362
- filename: binLogPositionState.filename,
363
- offset: evt.nextPosition
364
- }
365
- });
366
- break;
367
- case zongji_utils.eventIsRotation(evt):
368
- // Update the position
369
- binLogPositionState.filename = evt.binlogName;
370
- binLogPositionState.offset = evt.position;
371
- break;
372
- case zongji_utils.eventIsWriteMutation(evt):
373
- // TODO, can multiple tables be present?
374
- const writeTableInfo = evt.tableMap[evt.tableId];
375
- await this.writeChanges(batch, {
376
- type: storage.SaveOperationTag.INSERT,
377
- data: evt.rows,
378
- database: writeTableInfo.parentSchema,
379
- table: writeTableInfo.tableName,
380
- sourceTable: this.getTable(
381
- getMysqlRelId({
382
- schema: writeTableInfo.parentSchema,
383
- name: writeTableInfo.tableName
384
- })
385
- )
386
- });
387
- break;
388
- case zongji_utils.eventIsUpdateMutation(evt):
389
- const updateTableInfo = evt.tableMap[evt.tableId];
390
- await this.writeChanges(batch, {
391
- type: storage.SaveOperationTag.UPDATE,
392
- data: evt.rows.map((row) => row.after),
393
- previous_data: evt.rows.map((row) => row.before),
394
- database: updateTableInfo.parentSchema,
395
- table: updateTableInfo.tableName,
396
- sourceTable: this.getTable(
397
- getMysqlRelId({
398
- schema: updateTableInfo.parentSchema,
399
- name: updateTableInfo.tableName
400
- })
401
- )
402
- });
403
- break;
404
- case zongji_utils.eventIsDeleteMutation(evt):
405
- // TODO, can multiple tables be present?
406
- const deleteTableInfo = evt.tableMap[evt.tableId];
407
- await this.writeChanges(batch, {
408
- type: storage.SaveOperationTag.DELETE,
409
- data: evt.rows,
410
- database: deleteTableInfo.parentSchema,
411
- table: deleteTableInfo.tableName,
412
- // TODO cleanup
413
- sourceTable: this.getTable(
414
- getMysqlRelId({
415
- schema: deleteTableInfo.parentSchema,
416
- name: deleteTableInfo.tableName
417
- })
418
- )
419
- });
420
- break;
421
- case zongji_utils.eventIsXid(evt):
422
- Metrics.getInstance().transactions_replicated_total.add(1);
423
- // Need to commit with a replicated GTID with updated next position
424
- await batch.commit(
425
- new common.ReplicatedGTID({
426
- raw_gtid: currentGTID!.raw,
356
+ if (!this.stopped) {
357
+ await this.storage.startBatch(
358
+ { zeroLSN: ReplicatedGTID.ZERO.comparable, defaultSchema: this.defaultSchema },
359
+ async (batch) => {
360
+ const zongji = this.connections.createBinlogListener();
361
+
362
+ let currentGTID: common.ReplicatedGTID | null = null;
363
+
364
+ const queue = async.queue(async (evt: BinLogEvent) => {
365
+ // State machine
366
+ switch (true) {
367
+ case zongji_utils.eventIsGTIDLog(evt):
368
+ currentGTID = common.ReplicatedGTID.fromBinLogEvent({
369
+ raw_gtid: {
370
+ server_id: evt.serverId,
371
+ transaction_range: evt.transactionRange
372
+ },
427
373
  position: {
428
374
  filename: binLogPositionState.filename,
429
375
  offset: evt.nextPosition
430
376
  }
431
- }).comparable
432
- );
433
- currentGTID = null;
434
- // chunks_replicated_total.add(1);
435
- break;
436
- }
437
- }, 1);
438
-
439
- zongji.on('binlog', (evt: BinLogEvent) => {
440
- logger.info(`Pushing Binlog event ${evt.getEventName()}`);
441
- queue.push(evt);
442
- });
443
-
444
- logger.info(`Starting replication from ${binLogPositionState.filename}:${binLogPositionState.offset}`);
445
- zongji.start({
446
- includeEvents: ['tablemap', 'writerows', 'updaterows', 'deleterows', 'xid', 'rotate', 'gtidlog'],
447
- excludeEvents: [],
448
- filename: binLogPositionState.filename,
449
- position: binLogPositionState.offset
450
- });
377
+ });
378
+ break;
379
+ case zongji_utils.eventIsRotation(evt):
380
+ // Update the position
381
+ binLogPositionState.filename = evt.binlogName;
382
+ binLogPositionState.offset = evt.position;
383
+ break;
384
+ case zongji_utils.eventIsWriteMutation(evt):
385
+ // TODO, can multiple tables be present?
386
+ const writeTableInfo = evt.tableMap[evt.tableId];
387
+ await this.writeChanges(batch, {
388
+ type: storage.SaveOperationTag.INSERT,
389
+ data: evt.rows,
390
+ database: writeTableInfo.parentSchema,
391
+ table: writeTableInfo.tableName,
392
+ sourceTable: this.getTable(
393
+ getMysqlRelId({
394
+ schema: writeTableInfo.parentSchema,
395
+ name: writeTableInfo.tableName
396
+ })
397
+ )
398
+ });
399
+ break;
400
+ case zongji_utils.eventIsUpdateMutation(evt):
401
+ const updateTableInfo = evt.tableMap[evt.tableId];
402
+ await this.writeChanges(batch, {
403
+ type: storage.SaveOperationTag.UPDATE,
404
+ data: evt.rows.map((row) => row.after),
405
+ previous_data: evt.rows.map((row) => row.before),
406
+ database: updateTableInfo.parentSchema,
407
+ table: updateTableInfo.tableName,
408
+ sourceTable: this.getTable(
409
+ getMysqlRelId({
410
+ schema: updateTableInfo.parentSchema,
411
+ name: updateTableInfo.tableName
412
+ })
413
+ )
414
+ });
415
+ break;
416
+ case zongji_utils.eventIsDeleteMutation(evt):
417
+ // TODO, can multiple tables be present?
418
+ const deleteTableInfo = evt.tableMap[evt.tableId];
419
+ await this.writeChanges(batch, {
420
+ type: storage.SaveOperationTag.DELETE,
421
+ data: evt.rows,
422
+ database: deleteTableInfo.parentSchema,
423
+ table: deleteTableInfo.tableName,
424
+ // TODO cleanup
425
+ sourceTable: this.getTable(
426
+ getMysqlRelId({
427
+ schema: deleteTableInfo.parentSchema,
428
+ name: deleteTableInfo.tableName
429
+ })
430
+ )
431
+ });
432
+ break;
433
+ case zongji_utils.eventIsXid(evt):
434
+ Metrics.getInstance().transactions_replicated_total.add(1);
435
+ // Need to commit with a replicated GTID with updated next position
436
+ await batch.commit(
437
+ new common.ReplicatedGTID({
438
+ raw_gtid: currentGTID!.raw,
439
+ position: {
440
+ filename: binLogPositionState.filename,
441
+ offset: evt.nextPosition
442
+ }
443
+ }).comparable
444
+ );
445
+ currentGTID = null;
446
+ // chunks_replicated_total.add(1);
447
+ break;
448
+ }
449
+ }, 1);
450
+
451
+ zongji.on('binlog', (evt: BinLogEvent) => {
452
+ if (!this.stopped) {
453
+ logger.info(`Pushing Binlog event ${evt.getEventName()}`);
454
+ queue.push(evt);
455
+ } else {
456
+ logger.info(`Replication is busy stopping, ignoring event ${evt.getEventName()}`);
457
+ }
458
+ });
451
459
 
452
- // Forever young
453
- await new Promise<void>((resolve, reject) => {
454
- queue.error((error) => {
455
- zongji.stop();
456
- queue.kill();
457
- reject(error);
460
+ if (this.stopped) {
461
+ // Powersync is shutting down, don't start replicating
462
+ return;
463
+ }
464
+ // Only listen for changes to tables in the sync rules
465
+ const includedTables = [...this.tableCache.values()].map((table) => table.table);
466
+ logger.info(`Starting replication from ${binLogPositionState.filename}:${binLogPositionState.offset}`);
467
+ zongji.start({
468
+ includeEvents: ['tablemap', 'writerows', 'updaterows', 'deleterows', 'xid', 'rotate', 'gtidlog'],
469
+ excludeEvents: [],
470
+ includeSchema: { [this.defaultSchema]: includedTables },
471
+ filename: binLogPositionState.filename,
472
+ position: binLogPositionState.offset
458
473
  });
459
- this.abortSignal.addEventListener(
460
- 'abort',
461
- async () => {
474
+
475
+ // Forever young
476
+ await new Promise<void>((resolve, reject) => {
477
+ queue.error((error) => {
478
+ logger.error('Queue error.', error);
462
479
  zongji.stop();
463
480
  queue.kill();
464
- if (!queue.length) {
465
- await queue.drain();
466
- }
467
- resolve();
468
- },
469
- { once: true }
470
- );
471
- });
472
- }
473
- );
481
+ reject(error);
482
+ });
483
+
484
+ this.abortSignal.addEventListener(
485
+ 'abort',
486
+ () => {
487
+ logger.info('Abort signal received, stopping replication...');
488
+ zongji.stop();
489
+ queue.kill();
490
+ resolve();
491
+ },
492
+ { once: true }
493
+ );
494
+ });
495
+ }
496
+ );
497
+ }
474
498
  }
475
499
 
476
500
  private async writeChanges(
@@ -3,11 +3,26 @@ declare module '@powersync/mysql-zongji' {
3
3
  host: string;
4
4
  user: string;
5
5
  password: string;
6
+ dateStrings?: boolean;
6
7
  };
7
8
 
9
+ interface DatabaseFilter {
10
+ [databaseName: string]: string[] | true;
11
+ }
12
+
8
13
  export type StartOptions = {
9
14
  includeEvents?: string[];
10
15
  excludeEvents?: string[];
16
+ /**
17
+ * Describe which databases and tables to include (Only for row events). Use database names as the key and pass an array of table names or true (for the entire database).
18
+ * Example: { 'my_database': ['allow_table', 'another_table'], 'another_db': true }
19
+ */
20
+ includeSchema?: DatabaseFilter;
21
+ /**
22
+ * Object describing which databases and tables to exclude (Same format as includeSchema)
23
+ * Example: { 'other_db': ['disallowed_table'], 'ex_db': true }
24
+ */
25
+ excludeSchema?: DatabaseFilter;
11
26
  /**
12
27
  * BinLog position filename to start reading events from
13
28
  */