@powersync/service-module-postgres-storage 0.0.0-dev-20260602073133 → 0.0.0-dev-20260803144812

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 (105) hide show
  1. package/CHANGELOG.md +101 -6
  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/{@types/storage → storage}/PostgresBucketStorageFactory.d.ts +13 -12
  6. package/dist/storage/PostgresBucketStorageFactory.js +59 -55
  7. package/dist/storage/PostgresBucketStorageFactory.js.map +1 -1
  8. package/dist/{@types/storage → storage}/PostgresCompactor.d.ts +3 -1
  9. package/dist/storage/PostgresCompactor.js +21 -1
  10. package/dist/storage/PostgresCompactor.js.map +1 -1
  11. package/dist/storage/PostgresReportStorage.js.map +1 -1
  12. package/dist/storage/PostgresStorageProvider.js +2 -1
  13. package/dist/storage/PostgresStorageProvider.js.map +1 -1
  14. package/dist/{@types/storage → storage}/PostgresSyncRulesStorage.d.ts +36 -11
  15. package/dist/storage/PostgresSyncRulesStorage.js +250 -177
  16. package/dist/storage/PostgresSyncRulesStorage.js.map +1 -1
  17. package/dist/{@types/storage → storage}/batch/PostgresBucketBatch.d.ts +8 -24
  18. package/dist/storage/batch/PostgresBucketBatch.js +229 -194
  19. package/dist/storage/batch/PostgresBucketBatch.js.map +1 -1
  20. package/dist/{@types/storage → storage}/checkpoints/PostgresWriteCheckpointAPI.d.ts +1 -1
  21. package/dist/storage/checkpoints/PostgresWriteCheckpointAPI.js +161 -24
  22. package/dist/storage/checkpoints/PostgresWriteCheckpointAPI.js.map +1 -1
  23. package/dist/{@types/storage → storage}/current-data-store.d.ts +8 -2
  24. package/dist/storage/current-data-store.js +66 -11
  25. package/dist/storage/current-data-store.js.map +1 -1
  26. package/dist/{@types/storage → storage}/storage-index.d.ts +1 -0
  27. package/dist/storage/storage-index.js +1 -0
  28. package/dist/storage/storage-index.js.map +1 -1
  29. package/dist/storage/sync-rules/PostgresPersistedSyncConfigContent.d.ts +17 -0
  30. package/dist/storage/sync-rules/PostgresPersistedSyncConfigContent.js +90 -0
  31. package/dist/storage/sync-rules/PostgresPersistedSyncConfigContent.js.map +1 -0
  32. package/dist/{@types/types → types}/models/WriteCheckpoint.d.ts +2 -0
  33. package/dist/types/models/WriteCheckpoint.js +5 -2
  34. package/dist/types/models/WriteCheckpoint.js.map +1 -1
  35. package/dist/utils/checkpoints.d.ts +9 -0
  36. package/dist/utils/checkpoints.js +26 -0
  37. package/dist/utils/checkpoints.js.map +1 -0
  38. package/dist/utils/test-utils.js +2 -2
  39. package/dist/utils/test-utils.js.map +1 -1
  40. package/package.json +10 -22
  41. package/src/migrations/scripts/1782950400000-checkpoint-requested-at.ts +51 -0
  42. package/src/storage/PostgresBucketStorageFactory.ts +67 -58
  43. package/src/storage/PostgresCompactor.ts +25 -1
  44. package/src/storage/PostgresReportStorage.ts +1 -2
  45. package/src/storage/PostgresStorageProvider.ts +2 -1
  46. package/src/storage/PostgresSyncRulesStorage.ts +275 -191
  47. package/src/storage/batch/PostgresBucketBatch.ts +256 -214
  48. package/src/storage/checkpoints/PostgresWriteCheckpointAPI.ts +174 -24
  49. package/src/storage/current-data-store.ts +66 -11
  50. package/src/storage/storage-index.ts +1 -0
  51. package/src/storage/sync-rules/PostgresPersistedSyncConfigContent.ts +98 -0
  52. package/src/types/models/WriteCheckpoint.ts +5 -2
  53. package/src/utils/checkpoints.ts +31 -0
  54. package/src/utils/test-utils.ts +2 -2
  55. package/test/src/__snapshots__/storage_sync.test.ts.snap +0 -582
  56. package/test/src/checkpoint_notifications.test.ts +522 -0
  57. package/test/src/migrations.test.ts +1 -1
  58. package/test/src/storage.test.ts +226 -10
  59. package/test/src/storage_compacting.test.ts +15 -7
  60. package/test/src/storage_sync.test.ts +11 -8
  61. package/test/tsconfig.json +1 -3
  62. package/tsconfig.json +1 -4
  63. package/tsconfig.tsbuildinfo +1 -0
  64. package/dist/.tsbuildinfo +0 -1
  65. package/dist/@types/storage/sync-rules/PostgresPersistedSyncRulesContent.d.ts +0 -9
  66. package/dist/storage/sync-rules/PostgresPersistedSyncRulesContent.js +0 -49
  67. package/dist/storage/sync-rules/PostgresPersistedSyncRulesContent.js.map +0 -1
  68. package/src/storage/sync-rules/PostgresPersistedSyncRulesContent.ts +0 -54
  69. /package/dist/{@types/index.d.ts → index.d.ts} +0 -0
  70. /package/dist/{@types/migrations → migrations}/PostgresMigrationAgent.d.ts +0 -0
  71. /package/dist/{@types/migrations → migrations}/PostgresMigrationStore.d.ts +0 -0
  72. /package/dist/{@types/migrations → migrations}/migration-utils.d.ts +0 -0
  73. /package/dist/{@types/migrations → migrations}/scripts/1684951997326-init.d.ts +0 -0
  74. /package/dist/{@types/migrations → migrations}/scripts/1749024804042-snapshot-progress.d.ts +0 -0
  75. /package/dist/{@types/migrations → migrations}/scripts/1756282360128-connection-reporting.d.ts +0 -0
  76. /package/dist/{@types/migrations → migrations}/scripts/1771232439485-storage-version.d.ts +0 -0
  77. /package/dist/{@types/migrations → migrations}/scripts/1771424826685-current-data-pending-deletes.d.ts +0 -0
  78. /package/dist/{@types/migrations → migrations}/scripts/1771491856000-sync-plan.d.ts +0 -0
  79. /package/dist/{@types/module → module}/PostgresStorageModule.d.ts +0 -0
  80. /package/dist/{@types/storage → storage}/PostgresReportStorage.d.ts +0 -0
  81. /package/dist/{@types/storage → storage}/PostgresStorageProvider.d.ts +0 -0
  82. /package/dist/{@types/storage → storage}/batch/OperationBatch.d.ts +0 -0
  83. /package/dist/{@types/storage → storage}/batch/PostgresPersistedBatch.d.ts +0 -0
  84. /package/dist/{@types/storage → storage}/table-id.d.ts +0 -0
  85. /package/dist/{@types/types → types}/codecs.d.ts +0 -0
  86. /package/dist/{@types/types → types}/models/ActiveCheckpoint.d.ts +0 -0
  87. /package/dist/{@types/types → types}/models/ActiveCheckpointNotification.d.ts +0 -0
  88. /package/dist/{@types/types → types}/models/BucketData.d.ts +0 -0
  89. /package/dist/{@types/types → types}/models/BucketParameters.d.ts +0 -0
  90. /package/dist/{@types/types → types}/models/CurrentData.d.ts +0 -0
  91. /package/dist/{@types/types → types}/models/Instance.d.ts +0 -0
  92. /package/dist/{@types/types → types}/models/Migration.d.ts +0 -0
  93. /package/dist/{@types/types → types}/models/SdkReporting.d.ts +0 -0
  94. /package/dist/{@types/types → types}/models/SourceTable.d.ts +0 -0
  95. /package/dist/{@types/types → types}/models/SyncRules.d.ts +0 -0
  96. /package/dist/{@types/types → types}/models/json.d.ts +0 -0
  97. /package/dist/{@types/types → types}/models/models-index.d.ts +0 -0
  98. /package/dist/{@types/types → types}/types.d.ts +0 -0
  99. /package/dist/{@types/utils → utils}/application-name.d.ts +0 -0
  100. /package/dist/{@types/utils → utils}/bson.d.ts +0 -0
  101. /package/dist/{@types/utils → utils}/bucket-data.d.ts +0 -0
  102. /package/dist/{@types/utils → utils}/db.d.ts +0 -0
  103. /package/dist/{@types/utils → utils}/test-utils.d.ts +0 -0
  104. /package/dist/{@types/utils → utils}/ts-codec.d.ts +0 -0
  105. /package/dist/{@types/utils → utils}/utils-index.d.ts +0 -0
@@ -32,32 +32,172 @@ export class PostgresWriteCheckpointAPI implements storage.WriteCheckpointAPI {
32
32
  return batchCreateCustomWriteCheckpoints(this.db, checkpoints);
33
33
  }
34
34
 
35
- async createManagedWriteCheckpoint(checkpoint: storage.ManagedWriteCheckpointOptions): Promise<bigint> {
35
+ async createManagedWriteCheckpoints(
36
+ checkpoints: storage.ManagedWriteCheckpointOptions[]
37
+ ): Promise<storage.CreateManagedWriteCheckpointsResult> {
36
38
  if (this.writeCheckpointMode !== storage.WriteCheckpointMode.MANAGED) {
37
39
  throw new framework.errors.ValidationError(
38
40
  `Attempting to create a managed Write Checkpoint when the current Write Checkpoint mode is set to "${this.writeCheckpointMode}"`
39
41
  );
40
42
  }
41
43
 
42
- const row = await this.db.sql`
43
- INSERT INTO
44
- write_checkpoints (user_id, lsns, write_checkpoint)
45
- VALUES
46
- (
47
- ${{ type: 'varchar', value: checkpoint.user_id }},
48
- ${{ type: 'jsonb', value: checkpoint.heads }},
49
- ${{ type: 'int8', value: 1 }}
50
- )
51
- ON CONFLICT (user_id) DO UPDATE
52
- SET
53
- write_checkpoint = write_checkpoints.write_checkpoint + 1,
54
- lsns = EXCLUDED.lsns
55
- RETURNING
56
- *;
57
- `
58
- .decoded(models.WriteCheckpoint)
59
- .first();
60
- return row!.write_checkpoint;
44
+ const uniqueCheckpoints = storage.uniqueManagedWriteCheckpoints(checkpoints);
45
+ if (uniqueCheckpoints.length == 0) {
46
+ return { writeCheckpoints: new Map(), shouldAdvance: false };
47
+ }
48
+
49
+ const writeCheckpoints = new Map<string, bigint>();
50
+ const generatedCheckpoints = uniqueCheckpoints.filter((checkpoint) => checkpoint.checkpoint_request_id == null);
51
+ const suppliedCheckpoints = uniqueCheckpoints.filter((checkpoint) => checkpoint.checkpoint_request_id != null);
52
+
53
+ if (generatedCheckpoints.length > 0) {
54
+ const mappedCheckpoints = generatedCheckpoints.map((checkpoint) => ({
55
+ user_id: checkpoint.user_id,
56
+ lsns: checkpoint.heads
57
+ }));
58
+
59
+ const generatedRows = await this.db.sql`
60
+ WITH
61
+ json_data AS (
62
+ SELECT
63
+ CHECKPOINT ->> 'user_id' AS user_id,
64
+ CHECKPOINT -> 'lsns' AS lsns
65
+ FROM
66
+ jsonb_array_elements(${{ type: 'jsonb', value: mappedCheckpoints }}) AS
67
+ CHECKPOINT
68
+ )
69
+ INSERT INTO
70
+ write_checkpoints (
71
+ user_id,
72
+ lsns,
73
+ write_checkpoint,
74
+ checkpoint_requested_at
75
+ )
76
+ SELECT
77
+ user_id,
78
+ lsns,
79
+ 1,
80
+ NULL
81
+ FROM
82
+ json_data
83
+ ON CONFLICT (user_id) DO UPDATE
84
+ SET
85
+ write_checkpoint = write_checkpoints.write_checkpoint + 1,
86
+ lsns = EXCLUDED.lsns,
87
+ checkpoint_requested_at = NULL
88
+ RETURNING
89
+ *;
90
+ `
91
+ .decoded(models.WriteCheckpoint)
92
+ .rows();
93
+
94
+ for (const row of generatedRows) {
95
+ writeCheckpoints.set(row.user_id, row.write_checkpoint);
96
+ }
97
+ }
98
+
99
+ if (suppliedCheckpoints.length > 0) {
100
+ // Supplied request ids are monotonic. Greater values update the checkpoint
101
+ // id and heads, while equal retries only refresh the retention timestamp.
102
+ // Stale requests return the stored id without changing the row.
103
+ const mappedCheckpoints = suppliedCheckpoints.map((checkpoint) => ({
104
+ user_id: checkpoint.user_id,
105
+ lsns: checkpoint.heads,
106
+ checkpoint_request_id: String(checkpoint.checkpoint_request_id)
107
+ }));
108
+
109
+ const suppliedRows = await this.db.sql`
110
+ WITH
111
+ json_data AS (
112
+ SELECT
113
+ CHECKPOINT ->> 'user_id' AS user_id,
114
+ CHECKPOINT -> 'lsns' AS lsns,
115
+ (
116
+ CHECKPOINT ->> 'checkpoint_request_id'
117
+ )::int8 AS checkpoint_request_id
118
+ FROM
119
+ jsonb_array_elements(${{ type: 'jsonb', value: mappedCheckpoints }}) AS
120
+ CHECKPOINT
121
+ )
122
+ INSERT INTO
123
+ write_checkpoints (
124
+ user_id,
125
+ lsns,
126
+ write_checkpoint,
127
+ checkpoint_requested_at
128
+ )
129
+ SELECT
130
+ user_id,
131
+ lsns,
132
+ checkpoint_request_id,
133
+ NOW()
134
+ FROM
135
+ json_data
136
+ ON CONFLICT (user_id) DO UPDATE
137
+ SET
138
+ write_checkpoint = CASE
139
+ WHEN EXCLUDED.write_checkpoint > write_checkpoints.write_checkpoint THEN EXCLUDED.write_checkpoint
140
+ ELSE write_checkpoints.write_checkpoint
141
+ END,
142
+ lsns = CASE
143
+ WHEN EXCLUDED.write_checkpoint > write_checkpoints.write_checkpoint THEN EXCLUDED.lsns
144
+ ELSE write_checkpoints.lsns
145
+ END,
146
+ checkpoint_requested_at = NOW()
147
+ WHERE
148
+ EXCLUDED.write_checkpoint >= write_checkpoints.write_checkpoint
149
+ RETURNING
150
+ *;
151
+ `
152
+ .decoded(models.WriteCheckpoint)
153
+ .rows();
154
+
155
+ for (const row of suppliedRows) {
156
+ writeCheckpoints.set(row.user_id, row.write_checkpoint);
157
+ }
158
+
159
+ // RETURNING only excludes stale requests rejected by the monotonic
160
+ // conflict condition. Those requests still need the stored id.
161
+ const returnedSuppliedUserIds = new Set(suppliedRows.map((row) => row.user_id));
162
+ const unchangedUserIds = suppliedCheckpoints
163
+ .map((checkpoint) => checkpoint.user_id)
164
+ .filter((userId) => !returnedSuppliedUserIds.has(userId));
165
+
166
+ if (unchangedUserIds.length > 0) {
167
+ const mappedUserIds = unchangedUserIds.map((user_id) => ({ user_id }));
168
+ const unchangedRows = await this.db.sql`
169
+ WITH
170
+ json_data AS (
171
+ SELECT
172
+ CHECKPOINT ->> 'user_id' AS user_id
173
+ FROM
174
+ jsonb_array_elements(${{ type: 'jsonb', value: mappedUserIds }}) AS
175
+ CHECKPOINT
176
+ )
177
+ SELECT
178
+ write_checkpoints.*
179
+ FROM
180
+ write_checkpoints
181
+ JOIN json_data ON write_checkpoints.user_id = json_data.user_id;
182
+ `
183
+ .decoded(models.WriteCheckpoint)
184
+ .rows();
185
+
186
+ for (const row of unchangedRows) {
187
+ writeCheckpoints.set(row.user_id, row.write_checkpoint);
188
+ }
189
+ }
190
+ }
191
+
192
+ // Postgres storage does not track a per-row processed indicator: a write
193
+ // checkpoint is considered processed at read time by comparing its lsns
194
+ // against the replicated head (see lastManagedWriteCheckpoint). We therefore
195
+ // force the source marker whenever any checkpoint was matched, which also
196
+ // covers stale or duplicate requests whose stored checkpoint may still be
197
+ // pending. Forcing a marker for an already-processed checkpoint is wasteful
198
+ // but harmless.
199
+ const shouldAdvance = generatedCheckpoints.length > 0 || suppliedCheckpoints.length > 0;
200
+ return { writeCheckpoints, shouldAdvance };
61
201
  }
62
202
 
63
203
  async lastWriteCheckpoint(filters: storage.LastWriteCheckpointFilters): Promise<bigint | null> {
@@ -133,7 +273,8 @@ export async function batchCreateCustomWriteCheckpoints(
133
273
  // Cannot encode bigint directly using JSON.stringify.
134
274
  // The ::int8 in the query below will take care of casting back to a number
135
275
  checkpoint: String(cp.checkpoint),
136
- sync_rules_id: cp.sync_rules_id
276
+ sync_rules_id: cp.sync_rules_id,
277
+ checkpoint_requested_at: cp.checkpoint_requested_at?.toISOString() ?? null
137
278
  };
138
279
  });
139
280
 
@@ -145,7 +286,12 @@ export async function batchCreateCustomWriteCheckpoints(
145
286
  CHECKPOINT
146
287
  )
147
288
  INSERT INTO
148
- custom_write_checkpoints (user_id, write_checkpoint, sync_rules_id)
289
+ custom_write_checkpoints (
290
+ user_id,
291
+ write_checkpoint,
292
+ sync_rules_id,
293
+ checkpoint_requested_at
294
+ )
149
295
  SELECT
150
296
  CHECKPOINT ->> 'user_id'::varchar,
151
297
  (
@@ -153,11 +299,15 @@ export async function batchCreateCustomWriteCheckpoints(
153
299
  )::int8,
154
300
  (
155
301
  CHECKPOINT ->> 'sync_rules_id'
156
- )::int4
302
+ )::int4,
303
+ (
304
+ CHECKPOINT ->> 'checkpoint_requested_at'
305
+ )::timestamptz
157
306
  FROM
158
307
  json_data
159
308
  ON CONFLICT (user_id, sync_rules_id) DO UPDATE
160
309
  SET
161
- write_checkpoint = EXCLUDED.write_checkpoint;
310
+ write_checkpoint = EXCLUDED.write_checkpoint,
311
+ checkpoint_requested_at = EXCLUDED.checkpoint_requested_at;
162
312
  `.execute();
163
313
  }
@@ -301,19 +301,74 @@ export class PostgresCurrentDataStore {
301
301
  `.execute();
302
302
  }
303
303
 
304
- async deleteGroupRows(db: Queryable, options: { groupId: number }) {
304
+ /**
305
+ * Delete up to `limit` rows for the group, returning the number of candidate
306
+ * rows found by the scan (see PostgresSyncRulesStorage#deleteGroupBatch for
307
+ * why the scan is counted rather than the delete).
308
+ */
309
+ async deleteGroupRowsBatch(db: Queryable, options: { groupId: number; limit: number }): Promise<bigint> {
305
310
  if (this.softDeleteEnabled) {
306
- await db.sql`
307
- DELETE FROM v3_current_data
308
- WHERE
309
- group_id = ${{ type: 'int4', value: options.groupId }}
310
- `.execute();
311
+ const result = await db.sql`
312
+ WITH
313
+ batch AS (
314
+ SELECT
315
+ ctid
316
+ FROM
317
+ v3_current_data
318
+ WHERE
319
+ group_id = ${{ type: 'int4', value: options.groupId }}
320
+ LIMIT
321
+ ${{ type: 'int4', value: options.limit }}
322
+ ),
323
+ deleted AS (
324
+ DELETE FROM v3_current_data
325
+ WHERE
326
+ ctid IN (
327
+ SELECT
328
+ ctid
329
+ FROM
330
+ batch
331
+ )
332
+ RETURNING
333
+ 1
334
+ )
335
+ SELECT
336
+ COUNT(*) AS count
337
+ FROM
338
+ batch
339
+ `.first<{ count: bigint }>();
340
+ return result?.count ?? 0n;
311
341
  } else {
312
- await db.sql`
313
- DELETE FROM current_data
314
- WHERE
315
- group_id = ${{ type: 'int4', value: options.groupId }}
316
- `.execute();
342
+ const result = await db.sql`
343
+ WITH
344
+ batch AS (
345
+ SELECT
346
+ ctid
347
+ FROM
348
+ current_data
349
+ WHERE
350
+ group_id = ${{ type: 'int4', value: options.groupId }}
351
+ LIMIT
352
+ ${{ type: 'int4', value: options.limit }}
353
+ ),
354
+ deleted AS (
355
+ DELETE FROM current_data
356
+ WHERE
357
+ ctid IN (
358
+ SELECT
359
+ ctid
360
+ FROM
361
+ batch
362
+ )
363
+ RETURNING
364
+ 1
365
+ )
366
+ SELECT
367
+ COUNT(*) AS count
368
+ FROM
369
+ batch
370
+ `.first<{ count: bigint }>();
371
+ return result?.count ?? 0n;
317
372
  }
318
373
  }
319
374
 
@@ -1,4 +1,5 @@
1
1
  export * from './PostgresBucketStorageFactory.js';
2
2
  export * from './PostgresCompactor.js';
3
+ export * from './PostgresReportStorage.js';
3
4
  export * from './PostgresStorageProvider.js';
4
5
  export * from './PostgresSyncRulesStorage.js';
@@ -0,0 +1,98 @@
1
+ import * as lib_postgres from '@powersync/lib-service-postgres';
2
+ import { ErrorCode, ServiceError } from '@powersync/lib-services-framework';
3
+ import { storage } from '@powersync/service-core';
4
+ import { models } from '../../types/types.js';
5
+
6
+ export class PostgresPersistedSyncConfigContent extends storage.PersistedSyncConfigContent {
7
+ constructor(
8
+ private db: lib_postgres.DatabaseClient,
9
+ row: models.SyncRulesDecoded
10
+ ) {
11
+ super({
12
+ replicationStreamId: Number(row.id),
13
+ sync_rules_content: row.content,
14
+ compiled_plan: row.sync_plan,
15
+ replicationStreamName: row.slot_name,
16
+ storageVersion: row.storage_version ?? storage.LEGACY_STORAGE_VERSION,
17
+ syncConfigState: row.state as storage.SyncRuleState
18
+ });
19
+ }
20
+
21
+ async getSyncConfigStatus(): Promise<storage.PersistedSyncConfigStatus | null> {
22
+ const row = await this.db.sql`
23
+ SELECT
24
+ *
25
+ FROM
26
+ sync_rules
27
+ WHERE
28
+ id = ${{ value: this.replicationStreamId, type: 'int4' }}
29
+ `
30
+ .decoded(models.SyncRules)
31
+ .first();
32
+
33
+ return row == null ? null : syncConfigStatusFromRow(row);
34
+ }
35
+ }
36
+
37
+ function syncConfigStatusFromRow(row: models.SyncRulesDecoded): storage.PersistedSyncConfigStatus {
38
+ return {
39
+ id: String(row.id),
40
+ replicationStreamId: Number(row.id),
41
+ state: row.state as storage.SyncRuleState,
42
+ snapshot_done: row.snapshot_done,
43
+ last_checkpoint_lsn: row.last_checkpoint_lsn,
44
+ last_fatal_error: row.last_fatal_error,
45
+ last_keepalive_ts: row.last_keepalive_ts ? new Date(row.last_keepalive_ts) : null,
46
+ last_checkpoint_ts: row.last_checkpoint_ts ? new Date(row.last_checkpoint_ts) : null
47
+ };
48
+ }
49
+
50
+ export class PostgresPersistedReplicationStream extends storage.PersistedReplicationStream {
51
+ current_lock: storage.ReplicationLock | null = null;
52
+ readonly syncConfigContent: readonly PostgresPersistedSyncConfigContent[];
53
+
54
+ constructor(
55
+ private db: lib_postgres.DatabaseClient,
56
+ private readonly row: models.SyncRulesDecoded
57
+ ) {
58
+ super({
59
+ replicationStreamId: Number(row.id),
60
+ replicationStreamName: row.slot_name,
61
+ state: row.state as storage.SyncRuleState,
62
+ storageVersion: row.storage_version ?? storage.LEGACY_STORAGE_VERSION
63
+ });
64
+ this.syncConfigContent = [new PostgresPersistedSyncConfigContent(this.db, this.row)];
65
+ }
66
+
67
+ parsed(options: storage.ParseSyncConfigOptions): storage.ParsedSyncConfigSet {
68
+ return this.syncConfigContent[0].parsed(options);
69
+ }
70
+
71
+ async lock(): Promise<storage.ReplicationLock> {
72
+ const manager = new lib_postgres.PostgresLockManager({
73
+ db: this.db,
74
+ name: `sync_rules_${this.replicationStreamId}_${this.replicationStreamName}`
75
+ });
76
+ const lockHandle = await manager.acquire();
77
+ if (!lockHandle) {
78
+ throw new ServiceError(ErrorCode.PSYNC_S1003, `Replication stream is locked by another process, standing by.`);
79
+ }
80
+
81
+ const interval = setInterval(async () => {
82
+ try {
83
+ await lockHandle.refresh();
84
+ } catch (e) {
85
+ this.logger.error('Failed to refresh lock', e);
86
+ clearInterval(interval);
87
+ }
88
+ }, 30_130);
89
+
90
+ return (this.current_lock = {
91
+ sync_rules_id: this.replicationStreamId,
92
+ release: async () => {
93
+ clearInterval(interval);
94
+ return lockHandle.release();
95
+ }
96
+ });
97
+ }
98
+ }
@@ -1,10 +1,12 @@
1
+ import { framework } from '@powersync/service-core';
1
2
  import * as t from 'ts-codec';
2
3
  import { bigint, jsonb } from '../codecs.js';
3
4
 
4
5
  export const WriteCheckpoint = t.object({
5
6
  user_id: t.string,
6
7
  lsns: jsonb(t.record(t.string)),
7
- write_checkpoint: bigint
8
+ write_checkpoint: bigint,
9
+ checkpoint_requested_at: t.Null.or(framework.codecs.date)
8
10
  });
9
11
 
10
12
  export type WriteCheckpoint = t.Encoded<typeof WriteCheckpoint>;
@@ -13,7 +15,8 @@ export type WriteCheckpointDecoded = t.Decoded<typeof WriteCheckpoint>;
13
15
  export const CustomWriteCheckpoint = t.object({
14
16
  user_id: t.string,
15
17
  write_checkpoint: bigint,
16
- sync_rules_id: bigint
18
+ sync_rules_id: bigint,
19
+ checkpoint_requested_at: t.Null.or(framework.codecs.date)
17
20
  });
18
21
 
19
22
  export type CustomWriteCheckpoint = t.Encoded<typeof CustomWriteCheckpoint>;
@@ -0,0 +1,31 @@
1
+ import * as lib_postgres from '@powersync/lib-service-postgres';
2
+ import { storage } from '@powersync/service-core';
3
+ import { models } from '../types/types.js';
4
+
5
+ /**
6
+ * Gets the latest active checkpoint document.
7
+ * This is mainly exported for mocking in tests.
8
+ */
9
+ export async function getActiveCheckpointDocument({
10
+ db
11
+ }: {
12
+ db: lib_postgres.DatabaseClient;
13
+ }): Promise<models.ActiveCheckpointDecoded | null> {
14
+ return db.sql`
15
+ SELECT
16
+ id,
17
+ last_checkpoint,
18
+ last_checkpoint_lsn
19
+ FROM
20
+ sync_rules
21
+ WHERE
22
+ state = ${{ value: storage.SyncRuleState.ACTIVE, type: 'varchar' }}
23
+ OR state = ${{ value: storage.SyncRuleState.ERRORED, type: 'varchar' }}
24
+ ORDER BY
25
+ id DESC
26
+ LIMIT
27
+ 1
28
+ `
29
+ .decoded(models.ActiveCheckpoint)
30
+ .first();
31
+ }
@@ -73,7 +73,7 @@ export function postgresTestSetup(factoryOptions: PostgresTestStorageOptions) {
73
73
 
74
74
  await using storageFactory = new PostgresBucketStorageFactory({
75
75
  config: TEST_CONNECTION_OPTIONS,
76
- slot_name_prefix: 'test_'
76
+ replicationStreamNamePrefix: 'test_'
77
77
  });
78
78
  await truncateTables(storageFactory.db);
79
79
  };
@@ -102,7 +102,7 @@ export function postgresTestSetup(factoryOptions: PostgresTestStorageOptions) {
102
102
 
103
103
  return new PostgresBucketStorageFactory({
104
104
  config: TEST_CONNECTION_OPTIONS,
105
- slot_name_prefix: 'test_'
105
+ replicationStreamNamePrefix: 'test_'
106
106
  });
107
107
  } catch (ex) {
108
108
  // Vitest does not display these errors nicely when using the `await using` syntx