@powersync/service-module-postgres-storage 0.16.3 → 0.18.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 (52) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/dist/migrations/scripts/1782950400000-checkpoint-requested-at.d.ts +3 -0
  3. package/dist/migrations/scripts/1782950400000-checkpoint-requested-at.js +42 -0
  4. package/dist/migrations/scripts/1782950400000-checkpoint-requested-at.js.map +1 -0
  5. package/dist/migrations/scripts/1784900000000-source-metadata.d.ts +3 -0
  6. package/dist/migrations/scripts/1784900000000-source-metadata.js +22 -0
  7. package/dist/migrations/scripts/1784900000000-source-metadata.js.map +1 -0
  8. package/dist/storage/PostgresBucketStorageFactory.js +0 -2
  9. package/dist/storage/PostgresBucketStorageFactory.js.map +1 -1
  10. package/dist/storage/PostgresCompactor.d.ts +2 -0
  11. package/dist/storage/PostgresCompactor.js +20 -0
  12. package/dist/storage/PostgresCompactor.js.map +1 -1
  13. package/dist/storage/PostgresSyncRulesStorage.d.ts +16 -1
  14. package/dist/storage/PostgresSyncRulesStorage.js +200 -131
  15. package/dist/storage/PostgresSyncRulesStorage.js.map +1 -1
  16. package/dist/storage/batch/PostgresBucketBatch.d.ts +2 -20
  17. package/dist/storage/batch/PostgresBucketBatch.js +217 -240
  18. package/dist/storage/batch/PostgresBucketBatch.js.map +1 -1
  19. package/dist/storage/checkpoints/PostgresWriteCheckpointAPI.d.ts +1 -1
  20. package/dist/storage/checkpoints/PostgresWriteCheckpointAPI.js +156 -36
  21. package/dist/storage/checkpoints/PostgresWriteCheckpointAPI.js.map +1 -1
  22. package/dist/storage/current-data-store.d.ts +8 -2
  23. package/dist/storage/current-data-store.js +66 -11
  24. package/dist/storage/current-data-store.js.map +1 -1
  25. package/dist/types/models/SourceTable.d.ts +7 -2
  26. package/dist/types/models/SourceTable.js +6 -2
  27. package/dist/types/models/SourceTable.js.map +1 -1
  28. package/dist/types/models/WriteCheckpoint.d.ts +2 -0
  29. package/dist/types/models/WriteCheckpoint.js +5 -2
  30. package/dist/types/models/WriteCheckpoint.js.map +1 -1
  31. package/dist/utils/checkpoints.d.ts +9 -0
  32. package/dist/utils/checkpoints.js +26 -0
  33. package/dist/utils/checkpoints.js.map +1 -0
  34. package/package.json +8 -8
  35. package/src/migrations/scripts/1782950400000-checkpoint-requested-at.ts +51 -0
  36. package/src/migrations/scripts/1784900000000-source-metadata.ts +31 -0
  37. package/src/storage/PostgresBucketStorageFactory.ts +0 -3
  38. package/src/storage/PostgresCompactor.ts +24 -0
  39. package/src/storage/PostgresSyncRulesStorage.ts +219 -137
  40. package/src/storage/batch/PostgresBucketBatch.ts +227 -246
  41. package/src/storage/checkpoints/PostgresWriteCheckpointAPI.ts +165 -37
  42. package/src/storage/current-data-store.ts +66 -11
  43. package/src/types/models/SourceTable.ts +7 -2
  44. package/src/types/models/WriteCheckpoint.ts +5 -2
  45. package/src/utils/checkpoints.ts +31 -0
  46. package/test/src/__snapshots__/storage_sync.test.ts.snap +0 -582
  47. package/test/src/checkpoint_notifications.test.ts +522 -0
  48. package/test/src/storage.test.ts +214 -5
  49. package/test/src/storage_compacting.test.ts +3 -3
  50. package/test/src/storage_sync.test.ts +48 -4
  51. package/test/tsconfig.json +1 -1
  52. package/tsconfig.tsbuildinfo +1 -1
@@ -85,6 +85,7 @@ export class PostgresBucketBatch extends BaseObserver {
85
85
  }
86
86
  async resolveTables(options) {
87
87
  const syncRules = options.parsedSyncConfig?.hydratedSyncConfig ?? this.sync_rules;
88
+ const reconcile = options.reconcileSourceTables ?? storage.defaultSourceTableReconciler;
88
89
  const { connection_id, source } = options;
89
90
  const { schema, name: table, objectId, replicaIdColumns, connectionTag, sendsCompleteRows } = source;
90
91
  const normalizedReplicaIdColumns = replicaIdColumns.map((column) => ({
@@ -93,9 +94,10 @@ export class PostgresBucketBatch extends BaseObserver {
93
94
  type_oid: typeof column.typeId !== 'undefined' ? Number(column.typeId) : column.typeId
94
95
  }));
95
96
  return this.db.transaction(async (db) => {
96
- let sourceTableRow;
97
+ // Find records that overlap by name or relation id.
98
+ let candidateRows;
97
99
  if (objectId != null) {
98
- sourceTableRow = await db.sql `
100
+ candidateRows = await db.sql `
99
101
  SELECT
100
102
  *
101
103
  FROM
@@ -103,16 +105,19 @@ export class PostgresBucketBatch extends BaseObserver {
103
105
  WHERE
104
106
  group_id = ${{ type: 'int4', value: this.group_id }}
105
107
  AND connection_id = ${{ type: 'int4', value: connection_id }}
106
- AND relation_id = ${{ type: 'jsonb', value: { object_id: objectId } }}
107
- AND schema_name = ${{ type: 'varchar', value: schema }}
108
- AND table_name = ${{ type: 'varchar', value: table }}
109
- AND replica_id_columns = ${{ type: 'jsonb', value: normalizedReplicaIdColumns }}
108
+ AND (
109
+ relation_id = ${{ type: 'jsonb', value: { object_id: objectId } }}
110
+ OR (
111
+ schema_name = ${{ type: 'varchar', value: schema }}
112
+ AND table_name = ${{ type: 'varchar', value: table }}
113
+ )
114
+ )
110
115
  `
111
116
  .decoded(models.SourceTable)
112
- .first();
117
+ .rows();
113
118
  }
114
119
  else {
115
- sourceTableRow = await db.sql `
120
+ candidateRows = await db.sql `
116
121
  SELECT
117
122
  *
118
123
  FROM
@@ -122,14 +127,38 @@ export class PostgresBucketBatch extends BaseObserver {
122
127
  AND connection_id = ${{ type: 'int4', value: connection_id }}
123
128
  AND schema_name = ${{ type: 'varchar', value: schema }}
124
129
  AND table_name = ${{ type: 'varchar', value: table }}
125
- AND replica_id_columns = ${{ type: 'jsonb', value: normalizedReplicaIdColumns }}
126
130
  `
127
131
  .decoded(models.SourceTable)
128
- .first();
132
+ .rows();
133
+ }
134
+ const candidateTables = candidateRows.map((row) => {
135
+ const table = this.sourceTableFromRow(row, connectionTag, syncRules);
136
+ table.storeCurrentData = sendsCompleteRows !== true;
137
+ return table;
138
+ });
139
+ const candidates = candidateTables.map((table) => table.clone());
140
+ const resolution = await reconcile({ source, candidates });
141
+ storage.validateSourceTableCandidateResolution(candidates, resolution);
142
+ for (const { id, sourceMetadata } of storage.diffSourceTableUpdates(candidateTables, resolution)) {
143
+ await db.sql `
144
+ UPDATE source_tables
145
+ SET
146
+ source_metadata = ${{ type: 'jsonb', value: sourceMetadata ?? null }}
147
+ WHERE
148
+ id = ${{ type: 'varchar', value: id.toString() }}
149
+ `.execute();
150
+ }
151
+ const materializedResolution = storage.materializeSourceTableResolution(candidateTables, resolution);
152
+ const compatibleTables = materializedResolution.compatibleTables;
153
+ // Keep one record, preferring one that has already been snapshotted.
154
+ const reuse = compatibleTables.find((candidate) => candidate.snapshotComplete) ?? compatibleTables[0] ?? null;
155
+ let sourceTable;
156
+ if (reuse != null) {
157
+ sourceTable = reuse;
129
158
  }
130
- if (sourceTableRow == null) {
159
+ else {
131
160
  const id = options.idGenerator ? postgresTableId(options.idGenerator()) : uuid.v4();
132
- sourceTableRow = await db.sql `
161
+ const insertedRow = await db.sql `
133
162
  INSERT INTO
134
163
  source_tables (
135
164
  id,
@@ -138,7 +167,8 @@ export class PostgresBucketBatch extends BaseObserver {
138
167
  relation_id,
139
168
  schema_name,
140
169
  table_name,
141
- replica_id_columns
170
+ replica_id_columns,
171
+ source_metadata
142
172
  )
143
173
  VALUES
144
174
  (
@@ -148,90 +178,25 @@ export class PostgresBucketBatch extends BaseObserver {
148
178
  ${{ type: 'jsonb', value: { object_id: objectId } }},
149
179
  ${{ type: 'varchar', value: schema }},
150
180
  ${{ type: 'varchar', value: table }},
151
- ${{ type: 'jsonb', value: normalizedReplicaIdColumns }}
181
+ ${{ type: 'jsonb', value: normalizedReplicaIdColumns }},
182
+ ${{ type: 'jsonb', value: resolution.newTableValues.sourceMetadata ?? null }}
152
183
  )
153
184
  RETURNING
154
185
  *
155
186
  `
156
187
  .decoded(models.SourceTable)
157
188
  .first();
189
+ sourceTable = this.sourceTableFromRow(insertedRow, connectionTag, syncRules);
190
+ sourceTable.storeCurrentData = sendsCompleteRows !== true;
158
191
  }
159
- const sourceTable = new storage.SourceTable({
160
- id: sourceTableRow.id,
161
- ref: source,
162
- objectId,
163
- replicaIdColumns,
164
- snapshotComplete: sourceTableRow.snapshot_done ?? true,
165
- ...syncRules.getMatchingSources(source)
166
- });
167
- if (!sourceTable.snapshotComplete) {
168
- sourceTable.snapshotStatus = {
169
- totalEstimatedCount: Number(sourceTableRow.snapshot_total_estimated_count ?? -1n),
170
- replicatedCount: Number(sourceTableRow.snapshot_replicated_count ?? 0n),
171
- lastKey: sourceTableRow.snapshot_last_key
172
- };
173
- }
174
- sourceTable.syncEvent = syncRules.tableTriggersEvent(source);
175
- sourceTable.syncData = sourceTable.bucketDataSources.length > 0;
176
- sourceTable.syncParameters = sourceTable.parameterLookupSources.length > 0;
177
- sourceTable.storeCurrentData = sendsCompleteRows !== true;
178
- let truncatedTables = [];
179
- if (objectId != null) {
180
- truncatedTables = await db.sql `
181
- SELECT
182
- *
183
- FROM
184
- source_tables
185
- WHERE
186
- group_id = ${{ type: 'int4', value: this.group_id }}
187
- AND connection_id = ${{ type: 'int4', value: connection_id }}
188
- AND id != ${{ type: 'varchar', value: sourceTableRow.id }}
189
- AND (
190
- relation_id = ${{ type: 'jsonb', value: { object_id: objectId } }}
191
- OR (
192
- schema_name = ${{ type: 'varchar', value: schema }}
193
- AND table_name = ${{ type: 'varchar', value: table }}
194
- )
195
- )
196
- `
197
- .decoded(models.SourceTable)
198
- .rows();
199
- }
200
- else {
201
- truncatedTables = await db.sql `
202
- SELECT
203
- *
204
- FROM
205
- source_tables
206
- WHERE
207
- group_id = ${{ type: 'int4', value: this.group_id }}
208
- AND connection_id = ${{ type: 'int4', value: connection_id }}
209
- AND id != ${{ type: 'varchar', value: sourceTableRow.id }}
210
- AND (
211
- schema_name = ${{ type: 'varchar', value: schema }}
212
- AND table_name = ${{ type: 'varchar', value: table }}
213
- )
214
- `
215
- .decoded(models.SourceTable)
216
- .rows();
217
- }
192
+ const dropTables = [
193
+ ...materializedResolution.incompatibleTables,
194
+ // PostgreSQL storage only keeps one SourceTable per physical table.
195
+ ...compatibleTables.filter((candidate) => !storage.sourceTableIdEquals(candidate.id, sourceTable.id))
196
+ ];
218
197
  return {
219
198
  tables: [sourceTable],
220
- dropTables: truncatedTables.map((doc) => {
221
- const ref = { connectionTag, schema: doc.schema_name, name: doc.table_name };
222
- const dropTable = new storage.SourceTable({
223
- id: doc.id,
224
- ref,
225
- objectId: doc.relation_id?.object_id ?? 0,
226
- replicaIdColumns: doc.replica_id_columns?.map((c) => ({ name: c.name, typeId: c.typeId, type: c.type })) ?? [],
227
- snapshotComplete: doc.snapshot_done ?? true,
228
- ...syncRules.getMatchingSources(ref)
229
- });
230
- dropTable.syncEvent = syncRules.tableTriggersEvent(ref);
231
- dropTable.syncData = dropTable.bucketDataSources.length > 0;
232
- dropTable.syncParameters = dropTable.parameterLookupSources.length > 0;
233
- return dropTable;
234
- })
199
+ dropTables
235
200
  };
236
201
  });
237
202
  }
@@ -258,8 +223,9 @@ export class PostgresBucketBatch extends BaseObserver {
258
223
  id: row.id,
259
224
  ref,
260
225
  objectId: row.relation_id?.object_id,
261
- replicaIdColumns: row.replica_id_columns?.map((c) => ({ name: c.name, typeId: c.typeId, type: c.type })) ?? [],
226
+ replicaIdColumns: row.replica_id_columns?.map((column) => ({ name: column.name, typeId: column.type_oid, type: column.type })) ?? [],
262
227
  snapshotComplete: row.snapshot_done ?? true,
228
+ sourceMetadata: row.source_metadata,
263
229
  ...syncRules.getMatchingSources(ref)
264
230
  });
265
231
  if (!sourceTable.snapshotComplete) {
@@ -375,12 +341,11 @@ export class PostgresBucketBatch extends BaseObserver {
375
341
  }
376
342
  const { flushedAny } = await persistedBatch.flush(db);
377
343
  clearedError = flushedAny && !this.clearedError;
378
- if (clearedError) {
379
- // No need to clear an error more than once per batch, since an error would always result in restarting the batch.
380
- await this.clearError(db);
381
- }
382
344
  });
383
345
  if (clearedError) {
346
+ // No need to clear an error more than once per batch, since an error would always result in restarting the batch.
347
+ // Cleared outside the replication transaction - see flushInner.
348
+ await this.clearError();
384
349
  this.clearedError = true;
385
350
  }
386
351
  }
@@ -437,6 +402,10 @@ export class PostgresBucketBatch extends BaseObserver {
437
402
  return this.getLastOpIdSequence(db);
438
403
  });
439
404
  if (clearedError) {
405
+ // Clear the error outside the replication transaction (plain autocommit update,
406
+ // like the keepalive), to avoid serialization conflicts on the sync_rules row
407
+ // when multiple writers flush concurrently.
408
+ await this.clearError();
440
409
  this.clearedError = true;
441
410
  }
442
411
  // null if done, set if we need another flush
@@ -454,136 +423,143 @@ export class PostgresBucketBatch extends BaseObserver {
454
423
  await this.flush();
455
424
  const now = new Date().toISOString();
456
425
  const persisted_op = this.persisted_op ?? null;
457
- const result = await this.db.sql `
458
- WITH
459
- selected AS (
460
- SELECT
461
- id,
462
- state,
463
- last_checkpoint,
464
- last_checkpoint_lsn,
465
- snapshot_done,
466
- no_checkpoint_before,
467
- keepalive_op,
468
- (
469
- snapshot_done = TRUE
470
- AND (
471
- last_checkpoint_lsn IS NULL
472
- OR last_checkpoint_lsn <= ${{ type: 'varchar', value: lsn }}
473
- )
426
+ // Transaction failures restart replication from the last durable checkpoint.
427
+ const result = await this.db.transaction(async (db) => {
428
+ const checkpoint = await db.sql `
429
+ WITH
430
+ selected AS (
431
+ SELECT
432
+ id,
433
+ state,
434
+ last_checkpoint,
435
+ last_checkpoint_lsn,
436
+ snapshot_done,
437
+ no_checkpoint_before,
438
+ keepalive_op,
439
+ (
440
+ snapshot_done = TRUE
441
+ AND (
442
+ last_checkpoint_lsn IS NULL
443
+ OR last_checkpoint_lsn <= ${{ type: 'varchar', value: lsn }}
444
+ )
445
+ AND (
446
+ no_checkpoint_before IS NULL
447
+ OR no_checkpoint_before <= ${{ type: 'varchar', value: lsn }}
448
+ )
449
+ ) AS can_checkpoint
450
+ FROM
451
+ sync_rules
452
+ WHERE
453
+ id = ${{ type: 'int4', value: this.group_id }}
454
+ FOR UPDATE
455
+ ),
456
+ computed AS (
457
+ SELECT
458
+ selected.*,
459
+ CASE
460
+ WHEN selected.can_checkpoint THEN GREATEST(
461
+ selected.last_checkpoint,
462
+ ${{ type: 'int8', value: persisted_op }},
463
+ selected.keepalive_op,
464
+ 0
465
+ )
466
+ ELSE selected.last_checkpoint
467
+ END AS new_last_checkpoint,
468
+ CASE
469
+ WHEN selected.can_checkpoint THEN NULL
470
+ ELSE GREATEST(
471
+ selected.keepalive_op,
472
+ ${{ type: 'int8', value: persisted_op }},
473
+ 0
474
+ )
475
+ END AS new_keepalive_op
476
+ FROM
477
+ selected
478
+ ),
479
+ updated AS (
480
+ UPDATE sync_rules AS sr
481
+ SET
482
+ last_checkpoint_lsn = CASE
483
+ WHEN computed.can_checkpoint THEN ${{ type: 'varchar', value: lsn }}
484
+ ELSE sr.last_checkpoint_lsn
485
+ END,
486
+ last_checkpoint_ts = CASE
487
+ WHEN computed.can_checkpoint THEN ${{ type: 1184, value: now }}
488
+ ELSE sr.last_checkpoint_ts
489
+ END,
490
+ last_keepalive_ts = ${{ type: 1184, value: now }},
491
+ last_fatal_error = CASE
492
+ WHEN computed.can_checkpoint THEN NULL
493
+ ELSE sr.last_fatal_error
494
+ END,
495
+ keepalive_op = computed.new_keepalive_op,
496
+ last_checkpoint = computed.new_last_checkpoint,
497
+ snapshot_lsn = CASE
498
+ WHEN computed.can_checkpoint THEN NULL
499
+ ELSE sr.snapshot_lsn
500
+ END
501
+ FROM
502
+ computed
503
+ WHERE
504
+ sr.id = computed.id
474
505
  AND (
475
- no_checkpoint_before IS NULL
476
- OR no_checkpoint_before <= ${{ type: 'varchar', value: lsn }}
506
+ sr.keepalive_op IS DISTINCT FROM computed.new_keepalive_op
507
+ OR sr.last_checkpoint IS DISTINCT FROM computed.new_last_checkpoint
508
+ OR ${{ type: 'bool', value: createEmptyCheckpoints }}
477
509
  )
478
- ) AS can_checkpoint
479
- FROM
480
- sync_rules
481
- WHERE
482
- id = ${{ type: 'int4', value: this.group_id }}
483
- FOR UPDATE
484
- ),
485
- computed AS (
486
- SELECT
487
- selected.*,
488
- CASE
489
- WHEN selected.can_checkpoint THEN GREATEST(
490
- selected.last_checkpoint,
491
- ${{ type: 'int8', value: persisted_op }},
492
- selected.keepalive_op,
493
- 0
494
- )
495
- ELSE selected.last_checkpoint
496
- END AS new_last_checkpoint,
497
- CASE
498
- WHEN selected.can_checkpoint THEN NULL
499
- ELSE GREATEST(
500
- selected.keepalive_op,
501
- ${{ type: 'int8', value: persisted_op }},
502
- 0
503
- )
504
- END AS new_keepalive_op
505
- FROM
506
- selected
507
- ),
508
- updated AS (
509
- UPDATE sync_rules AS sr
510
- SET
511
- last_checkpoint_lsn = CASE
512
- WHEN computed.can_checkpoint THEN ${{ type: 'varchar', value: lsn }}
513
- ELSE sr.last_checkpoint_lsn
514
- END,
515
- last_checkpoint_ts = CASE
516
- WHEN computed.can_checkpoint THEN ${{ type: 1184, value: now }}
517
- ELSE sr.last_checkpoint_ts
518
- END,
519
- last_keepalive_ts = ${{ type: 1184, value: now }},
520
- last_fatal_error = CASE
521
- WHEN computed.can_checkpoint THEN NULL
522
- ELSE sr.last_fatal_error
523
- END,
524
- keepalive_op = computed.new_keepalive_op,
525
- last_checkpoint = computed.new_last_checkpoint,
526
- snapshot_lsn = CASE
527
- WHEN computed.can_checkpoint THEN NULL
528
- ELSE sr.snapshot_lsn
529
- END
530
- FROM
531
- computed
532
- WHERE
533
- sr.id = computed.id
534
- AND (
535
- sr.keepalive_op IS DISTINCT FROM computed.new_keepalive_op
536
- OR sr.last_checkpoint IS DISTINCT FROM computed.new_last_checkpoint
537
- OR ${{ type: 'bool', value: createEmptyCheckpoints }}
538
- )
539
- RETURNING
540
- sr.id,
541
- sr.state,
542
- sr.last_checkpoint,
543
- sr.last_checkpoint_lsn,
544
- sr.snapshot_done,
545
- sr.no_checkpoint_before,
546
- computed.can_checkpoint,
547
- computed.keepalive_op,
548
- computed.new_last_checkpoint
549
- )
550
- SELECT
551
- id,
552
- state,
553
- last_checkpoint,
554
- last_checkpoint_lsn,
555
- snapshot_done,
556
- no_checkpoint_before,
557
- can_checkpoint,
558
- keepalive_op,
559
- new_last_checkpoint,
560
- TRUE AS created_checkpoint
561
- FROM
562
- updated
563
- UNION ALL
564
- SELECT
565
- id,
566
- state,
567
- new_last_checkpoint AS last_checkpoint,
568
- last_checkpoint_lsn,
569
- snapshot_done,
570
- no_checkpoint_before,
571
- can_checkpoint,
572
- keepalive_op,
573
- new_last_checkpoint,
574
- FALSE AS created_checkpoint
575
- FROM
576
- computed
577
- WHERE
578
- NOT EXISTS (
579
- SELECT
580
- 1
581
- FROM
582
- updated
583
- )
584
- `
585
- .decoded(CheckpointWithStatus)
586
- .first();
510
+ RETURNING
511
+ sr.id,
512
+ sr.state,
513
+ sr.last_checkpoint,
514
+ sr.last_checkpoint_lsn,
515
+ sr.snapshot_done,
516
+ sr.no_checkpoint_before,
517
+ computed.can_checkpoint,
518
+ computed.keepalive_op,
519
+ computed.new_last_checkpoint
520
+ )
521
+ SELECT
522
+ id,
523
+ state,
524
+ last_checkpoint,
525
+ last_checkpoint_lsn,
526
+ snapshot_done,
527
+ no_checkpoint_before,
528
+ can_checkpoint,
529
+ keepalive_op,
530
+ new_last_checkpoint,
531
+ TRUE AS created_checkpoint
532
+ FROM
533
+ updated
534
+ UNION ALL
535
+ SELECT
536
+ id,
537
+ state,
538
+ new_last_checkpoint AS last_checkpoint,
539
+ last_checkpoint_lsn,
540
+ snapshot_done,
541
+ no_checkpoint_before,
542
+ can_checkpoint,
543
+ keepalive_op,
544
+ new_last_checkpoint,
545
+ FALSE AS created_checkpoint
546
+ FROM
547
+ computed
548
+ WHERE
549
+ NOT EXISTS (
550
+ SELECT
551
+ 1
552
+ FROM
553
+ updated
554
+ )
555
+ `
556
+ .decoded(CheckpointWithStatus)
557
+ .first();
558
+ if (checkpoint?.can_checkpoint && checkpoint.state == storage.SyncRuleState.ACTIVE) {
559
+ await notifySyncRulesUpdate(db, checkpoint);
560
+ }
561
+ return checkpoint;
562
+ });
587
563
  if (result == null) {
588
564
  throw new ReplicationAssertionError('Failed to update sync_rules during checkpoint');
589
565
  }
@@ -607,10 +583,8 @@ export class PostgresBucketBatch extends BaseObserver {
607
583
  });
608
584
  }
609
585
  }
610
- await this.autoActivate(lsn);
611
- await notifySyncRulesUpdate(this.db, {
586
+ await this.autoActivate(lsn, {
612
587
  id: result.id,
613
- state: result.state,
614
588
  last_checkpoint: result.last_checkpoint,
615
589
  last_checkpoint_lsn: result.last_checkpoint_lsn
616
590
  });
@@ -872,10 +846,8 @@ export class PostgresBucketBatch extends BaseObserver {
872
846
  didFlush ||= flushedAny;
873
847
  }
874
848
  }
849
+ // The error itself is cleared by the caller, outside the replication transaction - see flushInner.
875
850
  const clearedError = didFlush && !this.clearedError;
876
- if (clearedError) {
877
- await this.clearError(db);
878
- }
879
851
  // Don't return empty batches
880
852
  return {
881
853
  resumeBatch: resumeBatch?.batch.length ? resumeBatch : null,
@@ -1101,13 +1073,12 @@ export class PostgresBucketBatch extends BaseObserver {
1101
1073
  *
1102
1074
  * Called on new commits.
1103
1075
  */
1104
- async autoActivate(lsn) {
1076
+ async autoActivate(lsn, checkpoint) {
1105
1077
  if (!this.needsActivation) {
1106
1078
  // Already activated
1107
1079
  return;
1108
1080
  }
1109
- let didActivate = false;
1110
- await this.db.transaction(async (db) => {
1081
+ const activationResult = await this.db.transaction(async (db) => {
1111
1082
  const syncRulesRow = await db.sql `
1112
1083
  SELECT
1113
1084
  state,
@@ -1139,14 +1110,18 @@ export class PostgresBucketBatch extends BaseObserver {
1139
1110
  )
1140
1111
  AND id != ${{ type: 'int4', value: this.group_id }}
1141
1112
  `.execute();
1142
- didActivate = true;
1143
- this.needsActivation = false;
1113
+ await notifySyncRulesUpdate(db, checkpoint);
1114
+ return 'activated';
1144
1115
  }
1145
1116
  else if (syncRulesRow?.state != storage.SyncRuleState.PROCESSING) {
1146
- this.needsActivation = false;
1117
+ return 'not-processing';
1147
1118
  }
1119
+ return 'pending';
1148
1120
  });
1149
- if (didActivate) {
1121
+ if (activationResult != 'pending') {
1122
+ this.needsActivation = false;
1123
+ }
1124
+ if (activationResult == 'activated') {
1150
1125
  this.logger.info(`Activated new replication stream at ${lsn}`);
1151
1126
  }
1152
1127
  }
@@ -1224,11 +1199,13 @@ export class PostgresBucketBatch extends BaseObserver {
1224
1199
  * active checkpoint has been updated.
1225
1200
  */
1226
1201
  export const notifySyncRulesUpdate = async (db, update) => {
1227
- if (update.state != storage.SyncRuleState.ACTIVE) {
1228
- return;
1229
- }
1230
- await db.query({
1231
- statement: `NOTIFY ${NOTIFICATION_CHANNEL}, '${models.ActiveCheckpointNotification.encode({ active_checkpoint: update })}'`
1232
- });
1202
+ const payload = models.ActiveCheckpointNotification.encode({ active_checkpoint: update });
1203
+ await db.sql `
1204
+ SELECT
1205
+ pg_notify (
1206
+ ${{ type: 'varchar', value: NOTIFICATION_CHANNEL }},
1207
+ ${{ type: 'varchar', value: payload }}
1208
+ )
1209
+ `.execute();
1233
1210
  };
1234
1211
  //# sourceMappingURL=PostgresBucketBatch.js.map