@powersync/service-module-postgres-storage 0.0.0-dev-20250724091220 → 0.0.0-dev-20250724111728

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.
@@ -5,6 +5,7 @@ import { storage } from '@powersync/service-core';
5
5
  import { isPostgresStorageConfig, normalizePostgresStorageConfig, PostgresStorageConfig } from '../types/types.js';
6
6
  import { dropTables } from '../utils/db.js';
7
7
  import { PostgresBucketStorageFactory } from './PostgresBucketStorageFactory.js';
8
+ import { PostgresReportStorageFactory } from './PostgresReportStorageFactory.js';
8
9
 
9
10
  export class PostgresStorageProvider implements storage.BucketStorageProvider {
10
11
  get type() {
@@ -28,9 +29,14 @@ export class PostgresStorageProvider implements storage.BucketStorageProvider {
28
29
  config: normalizedConfig,
29
30
  slot_name_prefix: options.resolvedConfig.slot_name_prefix
30
31
  });
32
+
33
+ const reportStorageFactory = new PostgresReportStorageFactory({
34
+ config: normalizedConfig
35
+ });
36
+
31
37
  return {
32
38
  // TODO: IMPLEMENT REPORT STORAGE
33
- reportStorage: null,
39
+ reportStorage: reportStorageFactory,
34
40
  storage: storageFactory,
35
41
  shutDown: async () => storageFactory.db[Symbol.asyncDispose](),
36
42
  tearDown: async () => {
@@ -7,6 +7,7 @@ import {
7
7
  InternalOpId,
8
8
  internalToExternalOpId,
9
9
  LastValueSink,
10
+ maxLsn,
10
11
  storage,
11
12
  utils,
12
13
  WatchWriteCheckpointOptions
@@ -310,13 +311,14 @@ export class PostgresSyncRulesStorage
310
311
  SELECT
311
312
  last_checkpoint_lsn,
312
313
  no_checkpoint_before,
313
- keepalive_op
314
+ keepalive_op,
315
+ snapshot_lsn
314
316
  FROM
315
317
  sync_rules
316
318
  WHERE
317
319
  id = ${{ type: 'int4', value: this.group_id }}
318
320
  `
319
- .decoded(pick(models.SyncRules, ['last_checkpoint_lsn', 'no_checkpoint_before', 'keepalive_op']))
321
+ .decoded(pick(models.SyncRules, ['last_checkpoint_lsn', 'no_checkpoint_before', 'keepalive_op', 'snapshot_lsn']))
320
322
  .first();
321
323
 
322
324
  const checkpoint_lsn = syncRules?.last_checkpoint_lsn ?? null;
@@ -330,6 +332,7 @@ export class PostgresSyncRulesStorage
330
332
  last_checkpoint_lsn: checkpoint_lsn,
331
333
  keep_alive_op: syncRules?.keepalive_op,
332
334
  no_checkpoint_before_lsn: syncRules?.no_checkpoint_before ?? options.zeroLSN,
335
+ resumeFromLsn: maxLsn(syncRules?.snapshot_lsn, checkpoint_lsn),
333
336
  store_current_data: options.storeCurrentData,
334
337
  skip_existing_rows: options.skipExistingRows ?? false,
335
338
  batch_limits: this.options.batchLimits,
@@ -644,43 +647,6 @@ export class PostgresSyncRulesStorage
644
647
  `.execute();
645
648
  }
646
649
 
647
- async autoActivate(): Promise<void> {
648
- await this.db.transaction(async (db) => {
649
- const syncRulesRow = await db.sql`
650
- SELECT
651
- state
652
- FROM
653
- sync_rules
654
- WHERE
655
- id = ${{ type: 'int4', value: this.group_id }}
656
- `
657
- .decoded(pick(models.SyncRules, ['state']))
658
- .first();
659
-
660
- if (syncRulesRow && syncRulesRow.state == storage.SyncRuleState.PROCESSING) {
661
- await db.sql`
662
- UPDATE sync_rules
663
- SET
664
- state = ${{ type: 'varchar', value: storage.SyncRuleState.ACTIVE }}
665
- WHERE
666
- id = ${{ type: 'int4', value: this.group_id }}
667
- `.execute();
668
- }
669
-
670
- await db.sql`
671
- UPDATE sync_rules
672
- SET
673
- state = ${{ type: 'varchar', value: storage.SyncRuleState.STOP }}
674
- WHERE
675
- (
676
- state = ${{ value: storage.SyncRuleState.ACTIVE, type: 'varchar' }}
677
- OR state = ${{ value: storage.SyncRuleState.ERRORED, type: 'varchar' }}
678
- )
679
- AND id != ${{ type: 'int4', value: this.group_id }}
680
- `.execute();
681
- });
682
- }
683
-
684
650
  private async getChecksumsInternal(batch: storage.FetchPartialBucketChecksum[]): Promise<storage.PartialChecksumMap> {
685
651
  if (batch.length == 0) {
686
652
  return new Map();
@@ -31,6 +31,7 @@ export interface PostgresBucketBatchOptions {
31
31
  no_checkpoint_before_lsn: string;
32
32
  store_current_data: boolean;
33
33
  keep_alive_op?: InternalOpId | null;
34
+ resumeFromLsn: string | null;
34
35
  /**
35
36
  * Set to true for initial replication.
36
37
  */
@@ -61,6 +62,8 @@ export class PostgresBucketBatch
61
62
 
62
63
  public last_flushed_op: InternalOpId | null = null;
63
64
 
65
+ public resumeFromLsn: string | null;
66
+
64
67
  protected db: lib_postgres.DatabaseClient;
65
68
  protected group_id: number;
66
69
  protected last_checkpoint_lsn: string | null;
@@ -73,6 +76,7 @@ export class PostgresBucketBatch
73
76
  protected batch: OperationBatch | null;
74
77
  private lastWaitingLogThrottled = 0;
75
78
  private markRecordUnavailable: BucketStorageMarkRecordUnavailable | undefined;
79
+ private needsActivation = true;
76
80
 
77
81
  constructor(protected options: PostgresBucketBatchOptions) {
78
82
  super();
@@ -81,6 +85,7 @@ export class PostgresBucketBatch
81
85
  this.group_id = options.group_id;
82
86
  this.last_checkpoint_lsn = options.last_checkpoint_lsn;
83
87
  this.no_checkpoint_before_lsn = options.no_checkpoint_before_lsn;
88
+ this.resumeFromLsn = options.resumeFromLsn;
84
89
  this.write_checkpoint_batch = [];
85
90
  this.sync_rules = options.sync_rules;
86
91
  this.markRecordUnavailable = options.markRecordUnavailable;
@@ -321,6 +326,7 @@ export class PostgresBucketBatch
321
326
  // Don't create a checkpoint if there were no changes
322
327
  if (!createEmptyCheckpoints && this.persisted_op == null) {
323
328
  // Nothing to commit - return true
329
+ await this.autoActivate(lsn);
324
330
  return true;
325
331
  }
326
332
 
@@ -363,6 +369,7 @@ export class PostgresBucketBatch
363
369
  .decoded(StatefulCheckpoint)
364
370
  .first();
365
371
 
372
+ await this.autoActivate(lsn);
366
373
  await notifySyncRulesUpdate(this.db, doc!);
367
374
 
368
375
  this.persisted_op = null;
@@ -406,13 +413,14 @@ export class PostgresBucketBatch
406
413
  .decoded(StatefulCheckpoint)
407
414
  .first();
408
415
 
416
+ await this.autoActivate(lsn);
409
417
  await notifySyncRulesUpdate(this.db, updated!);
410
418
 
411
419
  this.last_checkpoint_lsn = lsn;
412
420
  return true;
413
421
  }
414
422
 
415
- async setSnapshotLsn(lsn: string): Promise<void> {
423
+ async setResumeLsn(lsn: string): Promise<void> {
416
424
  await this.db.sql`
417
425
  UPDATE sync_rules
418
426
  SET
@@ -916,6 +924,59 @@ export class PostgresBucketBatch
916
924
  return result;
917
925
  }
918
926
 
927
+ /**
928
+ * Switch from processing -> active if relevant.
929
+ *
930
+ * Called on new commits.
931
+ */
932
+ private async autoActivate(lsn: string): Promise<void> {
933
+ if (!this.needsActivation) {
934
+ // Already activated
935
+ return;
936
+ }
937
+
938
+ let didActivate = false;
939
+ await this.db.transaction(async (db) => {
940
+ const syncRulesRow = await db.sql`
941
+ SELECT
942
+ state
943
+ FROM
944
+ sync_rules
945
+ WHERE
946
+ id = ${{ type: 'int4', value: this.group_id }}
947
+ `
948
+ .decoded(pick(models.SyncRules, ['state']))
949
+ .first();
950
+
951
+ if (syncRulesRow && syncRulesRow.state == storage.SyncRuleState.PROCESSING) {
952
+ await db.sql`
953
+ UPDATE sync_rules
954
+ SET
955
+ state = ${{ type: 'varchar', value: storage.SyncRuleState.ACTIVE }}
956
+ WHERE
957
+ id = ${{ type: 'int4', value: this.group_id }}
958
+ `.execute();
959
+ didActivate = true;
960
+ }
961
+
962
+ await db.sql`
963
+ UPDATE sync_rules
964
+ SET
965
+ state = ${{ type: 'varchar', value: storage.SyncRuleState.STOP }}
966
+ WHERE
967
+ (
968
+ state = ${{ value: storage.SyncRuleState.ACTIVE, type: 'varchar' }}
969
+ OR state = ${{ value: storage.SyncRuleState.ERRORED, type: 'varchar' }}
970
+ )
971
+ AND id != ${{ type: 'int4', value: this.group_id }}
972
+ `.execute();
973
+ });
974
+ if (didActivate) {
975
+ this.logger.info(`Activated new sync rules at ${lsn}`);
976
+ }
977
+ this.needsActivation = false;
978
+ }
979
+
919
980
  /**
920
981
  * Gets relevant {@link SqlEventDescriptor}s for the given {@link SourceTable}
921
982
  * TODO maybe share this with an abstract class