@powersync/service-module-postgres-storage 0.0.0-dev-20250116115804

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 (157) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/LICENSE +67 -0
  3. package/README.md +67 -0
  4. package/dist/.tsbuildinfo +1 -0
  5. package/dist/@types/index.d.ts +7 -0
  6. package/dist/@types/migrations/PostgresMigrationAgent.d.ts +12 -0
  7. package/dist/@types/migrations/PostgresMigrationStore.d.ts +14 -0
  8. package/dist/@types/migrations/migration-utils.d.ts +3 -0
  9. package/dist/@types/migrations/scripts/1684951997326-init.d.ts +3 -0
  10. package/dist/@types/module/PostgresStorageModule.d.ts +6 -0
  11. package/dist/@types/storage/PostgresBucketStorageFactory.d.ts +42 -0
  12. package/dist/@types/storage/PostgresCompactor.d.ts +40 -0
  13. package/dist/@types/storage/PostgresStorageProvider.d.ts +5 -0
  14. package/dist/@types/storage/PostgresSyncRulesStorage.d.ts +46 -0
  15. package/dist/@types/storage/PostgresTestStorageFactoryGenerator.d.ts +13 -0
  16. package/dist/@types/storage/batch/OperationBatch.d.ts +47 -0
  17. package/dist/@types/storage/batch/PostgresBucketBatch.d.ts +90 -0
  18. package/dist/@types/storage/batch/PostgresPersistedBatch.d.ts +64 -0
  19. package/dist/@types/storage/checkpoints/PostgresWriteCheckpointAPI.d.ts +20 -0
  20. package/dist/@types/storage/storage-index.d.ts +5 -0
  21. package/dist/@types/storage/sync-rules/PostgresPersistedSyncRulesContent.d.ts +17 -0
  22. package/dist/@types/types/codecs.d.ts +61 -0
  23. package/dist/@types/types/models/ActiveCheckpoint.d.ts +12 -0
  24. package/dist/@types/types/models/ActiveCheckpointNotification.d.ts +19 -0
  25. package/dist/@types/types/models/BucketData.d.ts +22 -0
  26. package/dist/@types/types/models/BucketParameters.d.ts +11 -0
  27. package/dist/@types/types/models/CurrentData.d.ts +22 -0
  28. package/dist/@types/types/models/Instance.d.ts +6 -0
  29. package/dist/@types/types/models/Migration.d.ts +12 -0
  30. package/dist/@types/types/models/SourceTable.d.ts +31 -0
  31. package/dist/@types/types/models/SyncRules.d.ts +47 -0
  32. package/dist/@types/types/models/WriteCheckpoint.d.ts +15 -0
  33. package/dist/@types/types/models/models-index.d.ts +10 -0
  34. package/dist/@types/types/types.d.ts +96 -0
  35. package/dist/@types/utils/bson.d.ts +6 -0
  36. package/dist/@types/utils/bucket-data.d.ts +18 -0
  37. package/dist/@types/utils/db.d.ts +8 -0
  38. package/dist/@types/utils/ts-codec.d.ts +5 -0
  39. package/dist/@types/utils/utils-index.d.ts +4 -0
  40. package/dist/index.js +8 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/migrations/PostgresMigrationAgent.js +36 -0
  43. package/dist/migrations/PostgresMigrationAgent.js.map +1 -0
  44. package/dist/migrations/PostgresMigrationStore.js +60 -0
  45. package/dist/migrations/PostgresMigrationStore.js.map +1 -0
  46. package/dist/migrations/migration-utils.js +13 -0
  47. package/dist/migrations/migration-utils.js.map +1 -0
  48. package/dist/migrations/scripts/1684951997326-init.js +196 -0
  49. package/dist/migrations/scripts/1684951997326-init.js.map +1 -0
  50. package/dist/module/PostgresStorageModule.js +23 -0
  51. package/dist/module/PostgresStorageModule.js.map +1 -0
  52. package/dist/storage/PostgresBucketStorageFactory.js +433 -0
  53. package/dist/storage/PostgresBucketStorageFactory.js.map +1 -0
  54. package/dist/storage/PostgresCompactor.js +298 -0
  55. package/dist/storage/PostgresCompactor.js.map +1 -0
  56. package/dist/storage/PostgresStorageProvider.js +35 -0
  57. package/dist/storage/PostgresStorageProvider.js.map +1 -0
  58. package/dist/storage/PostgresSyncRulesStorage.js +619 -0
  59. package/dist/storage/PostgresSyncRulesStorage.js.map +1 -0
  60. package/dist/storage/PostgresTestStorageFactoryGenerator.js +110 -0
  61. package/dist/storage/PostgresTestStorageFactoryGenerator.js.map +1 -0
  62. package/dist/storage/batch/OperationBatch.js +93 -0
  63. package/dist/storage/batch/OperationBatch.js.map +1 -0
  64. package/dist/storage/batch/PostgresBucketBatch.js +732 -0
  65. package/dist/storage/batch/PostgresBucketBatch.js.map +1 -0
  66. package/dist/storage/batch/PostgresPersistedBatch.js +367 -0
  67. package/dist/storage/batch/PostgresPersistedBatch.js.map +1 -0
  68. package/dist/storage/checkpoints/PostgresWriteCheckpointAPI.js +148 -0
  69. package/dist/storage/checkpoints/PostgresWriteCheckpointAPI.js.map +1 -0
  70. package/dist/storage/storage-index.js +6 -0
  71. package/dist/storage/storage-index.js.map +1 -0
  72. package/dist/storage/sync-rules/PostgresPersistedSyncRulesContent.js +58 -0
  73. package/dist/storage/sync-rules/PostgresPersistedSyncRulesContent.js.map +1 -0
  74. package/dist/types/codecs.js +97 -0
  75. package/dist/types/codecs.js.map +1 -0
  76. package/dist/types/models/ActiveCheckpoint.js +12 -0
  77. package/dist/types/models/ActiveCheckpoint.js.map +1 -0
  78. package/dist/types/models/ActiveCheckpointNotification.js +8 -0
  79. package/dist/types/models/ActiveCheckpointNotification.js.map +1 -0
  80. package/dist/types/models/BucketData.js +23 -0
  81. package/dist/types/models/BucketData.js.map +1 -0
  82. package/dist/types/models/BucketParameters.js +11 -0
  83. package/dist/types/models/BucketParameters.js.map +1 -0
  84. package/dist/types/models/CurrentData.js +16 -0
  85. package/dist/types/models/CurrentData.js.map +1 -0
  86. package/dist/types/models/Instance.js +5 -0
  87. package/dist/types/models/Instance.js.map +1 -0
  88. package/dist/types/models/Migration.js +12 -0
  89. package/dist/types/models/Migration.js.map +1 -0
  90. package/dist/types/models/SourceTable.js +24 -0
  91. package/dist/types/models/SourceTable.js.map +1 -0
  92. package/dist/types/models/SyncRules.js +47 -0
  93. package/dist/types/models/SyncRules.js.map +1 -0
  94. package/dist/types/models/WriteCheckpoint.js +13 -0
  95. package/dist/types/models/WriteCheckpoint.js.map +1 -0
  96. package/dist/types/models/models-index.js +11 -0
  97. package/dist/types/models/models-index.js.map +1 -0
  98. package/dist/types/types.js +46 -0
  99. package/dist/types/types.js.map +1 -0
  100. package/dist/utils/bson.js +16 -0
  101. package/dist/utils/bson.js.map +1 -0
  102. package/dist/utils/bucket-data.js +25 -0
  103. package/dist/utils/bucket-data.js.map +1 -0
  104. package/dist/utils/db.js +24 -0
  105. package/dist/utils/db.js.map +1 -0
  106. package/dist/utils/ts-codec.js +11 -0
  107. package/dist/utils/ts-codec.js.map +1 -0
  108. package/dist/utils/utils-index.js +5 -0
  109. package/dist/utils/utils-index.js.map +1 -0
  110. package/package.json +50 -0
  111. package/src/index.ts +10 -0
  112. package/src/migrations/PostgresMigrationAgent.ts +46 -0
  113. package/src/migrations/PostgresMigrationStore.ts +70 -0
  114. package/src/migrations/migration-utils.ts +14 -0
  115. package/src/migrations/scripts/1684951997326-init.ts +141 -0
  116. package/src/module/PostgresStorageModule.ts +30 -0
  117. package/src/storage/PostgresBucketStorageFactory.ts +496 -0
  118. package/src/storage/PostgresCompactor.ts +366 -0
  119. package/src/storage/PostgresStorageProvider.ts +42 -0
  120. package/src/storage/PostgresSyncRulesStorage.ts +666 -0
  121. package/src/storage/PostgresTestStorageFactoryGenerator.ts +61 -0
  122. package/src/storage/batch/OperationBatch.ts +101 -0
  123. package/src/storage/batch/PostgresBucketBatch.ts +885 -0
  124. package/src/storage/batch/PostgresPersistedBatch.ts +441 -0
  125. package/src/storage/checkpoints/PostgresWriteCheckpointAPI.ts +176 -0
  126. package/src/storage/storage-index.ts +5 -0
  127. package/src/storage/sync-rules/PostgresPersistedSyncRulesContent.ts +67 -0
  128. package/src/types/codecs.ts +136 -0
  129. package/src/types/models/ActiveCheckpoint.ts +15 -0
  130. package/src/types/models/ActiveCheckpointNotification.ts +14 -0
  131. package/src/types/models/BucketData.ts +26 -0
  132. package/src/types/models/BucketParameters.ts +14 -0
  133. package/src/types/models/CurrentData.ts +23 -0
  134. package/src/types/models/Instance.ts +8 -0
  135. package/src/types/models/Migration.ts +19 -0
  136. package/src/types/models/SourceTable.ts +32 -0
  137. package/src/types/models/SyncRules.ts +50 -0
  138. package/src/types/models/WriteCheckpoint.ts +20 -0
  139. package/src/types/models/models-index.ts +10 -0
  140. package/src/types/types.ts +73 -0
  141. package/src/utils/bson.ts +17 -0
  142. package/src/utils/bucket-data.ts +25 -0
  143. package/src/utils/db.ts +27 -0
  144. package/src/utils/ts-codec.ts +14 -0
  145. package/src/utils/utils-index.ts +4 -0
  146. package/test/src/__snapshots__/storage.test.ts.snap +9 -0
  147. package/test/src/__snapshots__/storage_sync.test.ts.snap +332 -0
  148. package/test/src/env.ts +6 -0
  149. package/test/src/migrations.test.ts +34 -0
  150. package/test/src/setup.ts +16 -0
  151. package/test/src/storage.test.ts +131 -0
  152. package/test/src/storage_compacting.test.ts +5 -0
  153. package/test/src/storage_sync.test.ts +12 -0
  154. package/test/src/util.ts +34 -0
  155. package/test/tsconfig.json +20 -0
  156. package/tsconfig.json +36 -0
  157. package/vitest.config.ts +13 -0
@@ -0,0 +1,496 @@
1
+ import * as framework from '@powersync/lib-services-framework';
2
+ import { storage, sync, utils } from '@powersync/service-core';
3
+ import * as pg_wire from '@powersync/service-jpgwire';
4
+ import * as sync_rules from '@powersync/service-sync-rules';
5
+ import crypto from 'crypto';
6
+ import { wrapWithAbort } from 'ix/asynciterable/operators/withabort.js';
7
+ import { LRUCache } from 'lru-cache/min';
8
+ import * as timers from 'timers/promises';
9
+ import * as uuid from 'uuid';
10
+
11
+ import * as lib_postgres from '@powersync/lib-service-postgres';
12
+ import { models, NormalizedPostgresStorageConfig } from '../types/types.js';
13
+
14
+ import { NOTIFICATION_CHANNEL, STORAGE_SCHEMA_NAME } from '../utils/db.js';
15
+ import { notifySyncRulesUpdate } from './batch/PostgresBucketBatch.js';
16
+ import { PostgresSyncRulesStorage } from './PostgresSyncRulesStorage.js';
17
+ import { PostgresPersistedSyncRulesContent } from './sync-rules/PostgresPersistedSyncRulesContent.js';
18
+
19
+ export type PostgresBucketStorageOptions = {
20
+ config: NormalizedPostgresStorageConfig;
21
+ slot_name_prefix: string;
22
+ };
23
+
24
+ export class PostgresBucketStorageFactory
25
+ extends framework.DisposableObserver<storage.BucketStorageFactoryListener>
26
+ implements storage.BucketStorageFactory
27
+ {
28
+ readonly db: lib_postgres.DatabaseClient;
29
+ public readonly slot_name_prefix: string;
30
+
31
+ private sharedIterator = new sync.BroadcastIterable((signal) => this.watchActiveCheckpoint(signal));
32
+
33
+ private readonly storageCache = new LRUCache<number, storage.SyncRulesBucketStorage>({
34
+ max: 3,
35
+ fetchMethod: async (id) => {
36
+ const syncRulesRow = await this.db.sql`
37
+ SELECT
38
+ *
39
+ FROM
40
+ sync_rules
41
+ WHERE
42
+ id = ${{ value: id, type: 'int4' }}
43
+ `
44
+ .decoded(models.SyncRules)
45
+ .first();
46
+ if (syncRulesRow == null) {
47
+ // Deleted in the meantime?
48
+ return undefined;
49
+ }
50
+ const rules = new PostgresPersistedSyncRulesContent(this.db, syncRulesRow);
51
+ return this.getInstance(rules);
52
+ },
53
+ dispose: (storage) => {
54
+ storage[Symbol.dispose]();
55
+ }
56
+ });
57
+
58
+ constructor(protected options: PostgresBucketStorageOptions) {
59
+ super();
60
+ this.db = new lib_postgres.DatabaseClient({
61
+ config: options.config,
62
+ schema: STORAGE_SCHEMA_NAME,
63
+ notificationChannels: [NOTIFICATION_CHANNEL]
64
+ });
65
+ this.slot_name_prefix = options.slot_name_prefix;
66
+
67
+ this.db.registerListener({
68
+ connectionCreated: async (connection) => this.prepareStatements(connection)
69
+ });
70
+ }
71
+
72
+ async [Symbol.asyncDispose]() {
73
+ super[Symbol.dispose]();
74
+ await this.db[Symbol.asyncDispose]();
75
+ }
76
+
77
+ async prepareStatements(connection: pg_wire.PgConnection) {
78
+ // It should be possible to prepare statements for some common operations here.
79
+ // This has not been implemented yet.
80
+ }
81
+
82
+ getInstance(syncRules: storage.PersistedSyncRulesContent): storage.SyncRulesBucketStorage {
83
+ const storage = new PostgresSyncRulesStorage({
84
+ factory: this,
85
+ db: this.db,
86
+ sync_rules: syncRules,
87
+ batchLimits: this.options.config.batch_limits
88
+ });
89
+ this.iterateListeners((cb) => cb.syncStorageCreated?.(storage));
90
+ storage.registerListener({
91
+ batchStarted: (batch) => {
92
+ // This nested listener will be automatically disposed when the storage is disposed
93
+ batch.registerManagedListener(storage, {
94
+ replicationEvent: (payload) => this.iterateListeners((cb) => cb.replicationEvent?.(payload))
95
+ });
96
+ }
97
+ });
98
+ return storage;
99
+ }
100
+
101
+ async getStorageMetrics(): Promise<storage.StorageMetrics> {
102
+ const active_sync_rules = await this.getActiveSyncRules({ defaultSchema: 'public' });
103
+ if (active_sync_rules == null) {
104
+ return {
105
+ operations_size_bytes: 0,
106
+ parameters_size_bytes: 0,
107
+ replication_size_bytes: 0
108
+ };
109
+ }
110
+
111
+ const sizes = await this.db.sql`
112
+ SELECT
113
+ pg_total_relation_size('current_data') AS current_size_bytes,
114
+ pg_total_relation_size('bucket_parameters') AS parameter_size_bytes,
115
+ pg_total_relation_size('bucket_data') AS operations_size_bytes;
116
+ `.first<{ current_size_bytes: bigint; parameter_size_bytes: bigint; operations_size_bytes: bigint }>();
117
+
118
+ return {
119
+ operations_size_bytes: Number(sizes!.operations_size_bytes),
120
+ parameters_size_bytes: Number(sizes!.parameter_size_bytes),
121
+ replication_size_bytes: Number(sizes!.current_size_bytes)
122
+ };
123
+ }
124
+
125
+ async getPowerSyncInstanceId(): Promise<string> {
126
+ const instanceRow = await this.db.sql`
127
+ SELECT
128
+ id
129
+ FROM
130
+ instance
131
+ `
132
+ .decoded(models.Instance)
133
+ .first();
134
+ if (instanceRow) {
135
+ return instanceRow.id;
136
+ }
137
+ const lockManager = new lib_postgres.PostgresLockManager({
138
+ db: this.db,
139
+ name: `instance-id-insertion-lock`
140
+ });
141
+ await lockManager.lock(async () => {
142
+ await this.db.sql`
143
+ INSERT INTO
144
+ instance (id)
145
+ VALUES
146
+ (${{ type: 'varchar', value: uuid.v4() }})
147
+ `.execute();
148
+ });
149
+ const newInstanceRow = await this.db.sql`
150
+ SELECT
151
+ id
152
+ FROM
153
+ instance
154
+ `
155
+ .decoded(models.Instance)
156
+ .first();
157
+ return newInstanceRow!.id;
158
+ }
159
+
160
+ // TODO possibly share implementation in abstract class
161
+ async configureSyncRules(
162
+ sync_rules: string,
163
+ options?: { lock?: boolean }
164
+ ): Promise<{
165
+ updated: boolean;
166
+ persisted_sync_rules?: storage.PersistedSyncRulesContent;
167
+ lock?: storage.ReplicationLock;
168
+ }> {
169
+ const next = await this.getNextSyncRulesContent();
170
+ const active = await this.getActiveSyncRulesContent();
171
+
172
+ if (next?.sync_rules_content == sync_rules) {
173
+ framework.logger.info('Sync rules from configuration unchanged');
174
+ return { updated: false };
175
+ } else if (next == null && active?.sync_rules_content == sync_rules) {
176
+ framework.logger.info('Sync rules from configuration unchanged');
177
+ return { updated: false };
178
+ } else {
179
+ framework.logger.info('Sync rules updated from configuration');
180
+ const persisted_sync_rules = await this.updateSyncRules({
181
+ content: sync_rules,
182
+ lock: options?.lock
183
+ });
184
+ return { updated: true, persisted_sync_rules, lock: persisted_sync_rules.current_lock ?? undefined };
185
+ }
186
+ }
187
+
188
+ async updateSyncRules(options: storage.UpdateSyncRulesOptions): Promise<PostgresPersistedSyncRulesContent> {
189
+ // TODO some shared implementation for this might be nice
190
+ // Parse and validate before applying any changes
191
+ sync_rules.SqlSyncRules.fromYaml(options.content, {
192
+ // No schema-based validation at this point
193
+ schema: undefined,
194
+ defaultSchema: 'not_applicable', // Not needed for validation
195
+ throwOnError: true
196
+ });
197
+
198
+ return this.db.transaction(async (db) => {
199
+ await db.sql`
200
+ UPDATE sync_rules
201
+ SET
202
+ state = ${{ type: 'varchar', value: storage.SyncRuleState.STOP }}
203
+ WHERE
204
+ state = ${{ type: 'varchar', value: storage.SyncRuleState.PROCESSING }}
205
+ `.execute();
206
+
207
+ const newSyncRulesRow = await db.sql`
208
+ WITH
209
+ next_id AS (
210
+ SELECT
211
+ nextval('sync_rules_id_sequence') AS id
212
+ )
213
+ INSERT INTO
214
+ sync_rules (id, content, state, slot_name)
215
+ VALUES
216
+ (
217
+ (
218
+ SELECT
219
+ id
220
+ FROM
221
+ next_id
222
+ ),
223
+ ${{ type: 'varchar', value: options.content }},
224
+ ${{ type: 'varchar', value: storage.SyncRuleState.PROCESSING }},
225
+ CONCAT(
226
+ ${{ type: 'varchar', value: this.slot_name_prefix }},
227
+ (
228
+ SELECT
229
+ id
230
+ FROM
231
+ next_id
232
+ ),
233
+ '_',
234
+ ${{ type: 'varchar', value: crypto.randomBytes(2).toString('hex') }}
235
+ )
236
+ )
237
+ RETURNING
238
+ *
239
+ `
240
+ .decoded(models.SyncRules)
241
+ .first();
242
+
243
+ await notifySyncRulesUpdate(this.db, newSyncRulesRow!);
244
+
245
+ return new PostgresPersistedSyncRulesContent(this.db, newSyncRulesRow!);
246
+ });
247
+ }
248
+
249
+ async slotRemoved(slot_name: string): Promise<void> {
250
+ const next = await this.getNextSyncRulesContent();
251
+ const active = await this.getActiveSyncRulesContent();
252
+
253
+ // In both the below cases, we create a new sync rules instance.
254
+ // The current one will continue erroring until the next one has finished processing.
255
+ if (next != null && next.slot_name == slot_name) {
256
+ // We need to redo the "next" sync rules
257
+ await this.updateSyncRules({
258
+ content: next.sync_rules_content
259
+ });
260
+ // Pro-actively stop replicating
261
+ await this.db.sql`
262
+ UPDATE sync_rules
263
+ SET
264
+ state = ${{ value: storage.SyncRuleState.STOP, type: 'varchar' }}
265
+ WHERE
266
+ id = ${{ value: next.id, type: 'int4' }}
267
+ AND state = ${{ value: storage.SyncRuleState.PROCESSING, type: 'varchar' }}
268
+ `.execute();
269
+ } else if (next == null && active?.slot_name == slot_name) {
270
+ // Slot removed for "active" sync rules, while there is no "next" one.
271
+ await this.updateSyncRules({
272
+ content: active.sync_rules_content
273
+ });
274
+
275
+ // Pro-actively stop replicating
276
+ await this.db.sql`
277
+ UPDATE sync_rules
278
+ SET
279
+ state = ${{ value: storage.SyncRuleState.STOP, type: 'varchar' }}
280
+ WHERE
281
+ id = ${{ value: active.id, type: 'int4' }}
282
+ AND state = ${{ value: storage.SyncRuleState.ACTIVE, type: 'varchar' }}
283
+ `.execute();
284
+ }
285
+ }
286
+
287
+ // TODO possibly share via abstract class
288
+ async getActiveSyncRules(options: storage.ParseSyncRulesOptions): Promise<storage.PersistedSyncRules | null> {
289
+ const content = await this.getActiveSyncRulesContent();
290
+ return content?.parsed(options) ?? null;
291
+ }
292
+
293
+ async getActiveSyncRulesContent(): Promise<storage.PersistedSyncRulesContent | null> {
294
+ const activeRow = await this.db.sql`
295
+ SELECT
296
+ *
297
+ FROM
298
+ sync_rules
299
+ WHERE
300
+ state = ${{ value: storage.SyncRuleState.ACTIVE, type: 'varchar' }}
301
+ ORDER BY
302
+ id DESC
303
+ LIMIT
304
+ 1
305
+ `
306
+ .decoded(models.SyncRules)
307
+ .first();
308
+ if (!activeRow) {
309
+ return null;
310
+ }
311
+
312
+ return new PostgresPersistedSyncRulesContent(this.db, activeRow);
313
+ }
314
+
315
+ // TODO possibly share via abstract class
316
+ async getNextSyncRules(options: storage.ParseSyncRulesOptions): Promise<storage.PersistedSyncRules | null> {
317
+ const content = await this.getNextSyncRulesContent();
318
+ return content?.parsed(options) ?? null;
319
+ }
320
+
321
+ async getNextSyncRulesContent(): Promise<storage.PersistedSyncRulesContent | null> {
322
+ const nextRow = await this.db.sql`
323
+ SELECT
324
+ *
325
+ FROM
326
+ sync_rules
327
+ WHERE
328
+ state = ${{ value: storage.SyncRuleState.PROCESSING, type: 'varchar' }}
329
+ ORDER BY
330
+ id DESC
331
+ LIMIT
332
+ 1
333
+ `
334
+ .decoded(models.SyncRules)
335
+ .first();
336
+ if (!nextRow) {
337
+ return null;
338
+ }
339
+
340
+ return new PostgresPersistedSyncRulesContent(this.db, nextRow);
341
+ }
342
+
343
+ async getReplicatingSyncRules(): Promise<storage.PersistedSyncRulesContent[]> {
344
+ const rows = await this.db.sql`
345
+ SELECT
346
+ *
347
+ FROM
348
+ sync_rules
349
+ WHERE
350
+ state = ${{ value: storage.SyncRuleState.ACTIVE, type: 'varchar' }}
351
+ OR state = ${{ value: storage.SyncRuleState.PROCESSING, type: 'varchar' }}
352
+ `
353
+ .decoded(models.SyncRules)
354
+ .rows();
355
+
356
+ return rows.map((row) => new PostgresPersistedSyncRulesContent(this.db, row));
357
+ }
358
+
359
+ async getStoppedSyncRules(): Promise<storage.PersistedSyncRulesContent[]> {
360
+ const rows = await this.db.sql`
361
+ SELECT
362
+ *
363
+ FROM
364
+ sync_rules
365
+ WHERE
366
+ state = ${{ value: storage.SyncRuleState.STOP, type: 'varchar' }}
367
+ `
368
+ .decoded(models.SyncRules)
369
+ .rows();
370
+
371
+ return rows.map((row) => new PostgresPersistedSyncRulesContent(this.db, row));
372
+ }
373
+
374
+ async getActiveCheckpoint(): Promise<storage.ActiveCheckpoint> {
375
+ const activeCheckpoint = await this.db.sql`
376
+ SELECT
377
+ id,
378
+ last_checkpoint,
379
+ last_checkpoint_lsn
380
+ FROM
381
+ sync_rules
382
+ WHERE
383
+ state = ${{ value: storage.SyncRuleState.ACTIVE, type: 'varchar' }}
384
+ ORDER BY
385
+ id DESC
386
+ LIMIT
387
+ 1
388
+ `
389
+ .decoded(models.ActiveCheckpoint)
390
+ .first();
391
+
392
+ return this.makeActiveCheckpoint(activeCheckpoint);
393
+ }
394
+
395
+ async *watchWriteCheckpoint(user_id: string, signal: AbortSignal): AsyncIterable<storage.WriteCheckpoint> {
396
+ let lastCheckpoint: utils.OpId | null = null;
397
+ let lastWriteCheckpoint: bigint | null = null;
398
+
399
+ const iter = wrapWithAbort(this.sharedIterator, signal);
400
+ for await (const cp of iter) {
401
+ const { checkpoint, lsn } = cp;
402
+
403
+ // lsn changes are not important by itself.
404
+ // What is important is:
405
+ // 1. checkpoint (op_id) changes.
406
+ // 2. write checkpoint changes for the specific user
407
+ const bucketStorage = await cp.getBucketStorage();
408
+ if (!bucketStorage) {
409
+ continue;
410
+ }
411
+
412
+ const lsnFilters: Record<string, string> = lsn ? { 1: lsn } : {};
413
+
414
+ const currentWriteCheckpoint = await bucketStorage.lastWriteCheckpoint({
415
+ user_id,
416
+ heads: {
417
+ ...lsnFilters
418
+ }
419
+ });
420
+
421
+ if (currentWriteCheckpoint == lastWriteCheckpoint && checkpoint == lastCheckpoint) {
422
+ // No change - wait for next one
423
+ // In some cases, many LSNs may be produced in a short time.
424
+ // Add a delay to throttle the write checkpoint lookup a bit.
425
+ await timers.setTimeout(20 + 10 * Math.random());
426
+ continue;
427
+ }
428
+
429
+ lastWriteCheckpoint = currentWriteCheckpoint;
430
+ lastCheckpoint = checkpoint;
431
+
432
+ yield { base: cp, writeCheckpoint: currentWriteCheckpoint };
433
+ }
434
+ }
435
+
436
+ protected async *watchActiveCheckpoint(signal: AbortSignal): AsyncIterable<storage.ActiveCheckpoint> {
437
+ const doc = await this.db.sql`
438
+ SELECT
439
+ id,
440
+ last_checkpoint,
441
+ last_checkpoint_lsn
442
+ FROM
443
+ sync_rules
444
+ WHERE
445
+ state = ${{ type: 'varchar', value: storage.SyncRuleState.ACTIVE }}
446
+ LIMIT
447
+ 1
448
+ `
449
+ .decoded(models.ActiveCheckpoint)
450
+ .first();
451
+
452
+ const sink = new sync.LastValueSink<string>(undefined);
453
+
454
+ const disposeListener = this.db.registerListener({
455
+ notification: (notification) => sink.next(notification.payload)
456
+ });
457
+
458
+ signal.addEventListener('aborted', async () => {
459
+ disposeListener();
460
+ sink.complete();
461
+ });
462
+
463
+ yield this.makeActiveCheckpoint(doc);
464
+
465
+ let lastOp: storage.ActiveCheckpoint | null = null;
466
+ for await (const payload of sink.withSignal(signal)) {
467
+ if (signal.aborted) {
468
+ return;
469
+ }
470
+
471
+ const notification = models.ActiveCheckpointNotification.decode(payload);
472
+ const activeCheckpoint = this.makeActiveCheckpoint(notification.active_checkpoint);
473
+
474
+ if (lastOp == null || activeCheckpoint.lsn != lastOp.lsn || activeCheckpoint.checkpoint != lastOp.checkpoint) {
475
+ lastOp = activeCheckpoint;
476
+ yield activeCheckpoint;
477
+ }
478
+ }
479
+ }
480
+
481
+ private makeActiveCheckpoint(row: models.ActiveCheckpointDecoded | null) {
482
+ return {
483
+ checkpoint: utils.timestampToOpId(row?.last_checkpoint ?? 0n),
484
+ lsn: row?.last_checkpoint_lsn ?? null,
485
+ hasSyncRules() {
486
+ return row != null;
487
+ },
488
+ getBucketStorage: async () => {
489
+ if (row == null) {
490
+ return null;
491
+ }
492
+ return (await this.storageCache.fetch(Number(row.id))) ?? null;
493
+ }
494
+ } satisfies storage.ActiveCheckpoint;
495
+ }
496
+ }