@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.
- package/CHANGELOG.md +32 -0
- package/LICENSE +67 -0
- package/README.md +67 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/@types/index.d.ts +7 -0
- package/dist/@types/migrations/PostgresMigrationAgent.d.ts +12 -0
- package/dist/@types/migrations/PostgresMigrationStore.d.ts +14 -0
- package/dist/@types/migrations/migration-utils.d.ts +3 -0
- package/dist/@types/migrations/scripts/1684951997326-init.d.ts +3 -0
- package/dist/@types/module/PostgresStorageModule.d.ts +6 -0
- package/dist/@types/storage/PostgresBucketStorageFactory.d.ts +42 -0
- package/dist/@types/storage/PostgresCompactor.d.ts +40 -0
- package/dist/@types/storage/PostgresStorageProvider.d.ts +5 -0
- package/dist/@types/storage/PostgresSyncRulesStorage.d.ts +46 -0
- package/dist/@types/storage/PostgresTestStorageFactoryGenerator.d.ts +13 -0
- package/dist/@types/storage/batch/OperationBatch.d.ts +47 -0
- package/dist/@types/storage/batch/PostgresBucketBatch.d.ts +90 -0
- package/dist/@types/storage/batch/PostgresPersistedBatch.d.ts +64 -0
- package/dist/@types/storage/checkpoints/PostgresWriteCheckpointAPI.d.ts +20 -0
- package/dist/@types/storage/storage-index.d.ts +5 -0
- package/dist/@types/storage/sync-rules/PostgresPersistedSyncRulesContent.d.ts +17 -0
- package/dist/@types/types/codecs.d.ts +61 -0
- package/dist/@types/types/models/ActiveCheckpoint.d.ts +12 -0
- package/dist/@types/types/models/ActiveCheckpointNotification.d.ts +19 -0
- package/dist/@types/types/models/BucketData.d.ts +22 -0
- package/dist/@types/types/models/BucketParameters.d.ts +11 -0
- package/dist/@types/types/models/CurrentData.d.ts +22 -0
- package/dist/@types/types/models/Instance.d.ts +6 -0
- package/dist/@types/types/models/Migration.d.ts +12 -0
- package/dist/@types/types/models/SourceTable.d.ts +31 -0
- package/dist/@types/types/models/SyncRules.d.ts +47 -0
- package/dist/@types/types/models/WriteCheckpoint.d.ts +15 -0
- package/dist/@types/types/models/models-index.d.ts +10 -0
- package/dist/@types/types/types.d.ts +96 -0
- package/dist/@types/utils/bson.d.ts +6 -0
- package/dist/@types/utils/bucket-data.d.ts +18 -0
- package/dist/@types/utils/db.d.ts +8 -0
- package/dist/@types/utils/ts-codec.d.ts +5 -0
- package/dist/@types/utils/utils-index.d.ts +4 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/migrations/PostgresMigrationAgent.js +36 -0
- package/dist/migrations/PostgresMigrationAgent.js.map +1 -0
- package/dist/migrations/PostgresMigrationStore.js +60 -0
- package/dist/migrations/PostgresMigrationStore.js.map +1 -0
- package/dist/migrations/migration-utils.js +13 -0
- package/dist/migrations/migration-utils.js.map +1 -0
- package/dist/migrations/scripts/1684951997326-init.js +196 -0
- package/dist/migrations/scripts/1684951997326-init.js.map +1 -0
- package/dist/module/PostgresStorageModule.js +23 -0
- package/dist/module/PostgresStorageModule.js.map +1 -0
- package/dist/storage/PostgresBucketStorageFactory.js +433 -0
- package/dist/storage/PostgresBucketStorageFactory.js.map +1 -0
- package/dist/storage/PostgresCompactor.js +298 -0
- package/dist/storage/PostgresCompactor.js.map +1 -0
- package/dist/storage/PostgresStorageProvider.js +35 -0
- package/dist/storage/PostgresStorageProvider.js.map +1 -0
- package/dist/storage/PostgresSyncRulesStorage.js +619 -0
- package/dist/storage/PostgresSyncRulesStorage.js.map +1 -0
- package/dist/storage/PostgresTestStorageFactoryGenerator.js +110 -0
- package/dist/storage/PostgresTestStorageFactoryGenerator.js.map +1 -0
- package/dist/storage/batch/OperationBatch.js +93 -0
- package/dist/storage/batch/OperationBatch.js.map +1 -0
- package/dist/storage/batch/PostgresBucketBatch.js +732 -0
- package/dist/storage/batch/PostgresBucketBatch.js.map +1 -0
- package/dist/storage/batch/PostgresPersistedBatch.js +367 -0
- package/dist/storage/batch/PostgresPersistedBatch.js.map +1 -0
- package/dist/storage/checkpoints/PostgresWriteCheckpointAPI.js +148 -0
- package/dist/storage/checkpoints/PostgresWriteCheckpointAPI.js.map +1 -0
- package/dist/storage/storage-index.js +6 -0
- package/dist/storage/storage-index.js.map +1 -0
- package/dist/storage/sync-rules/PostgresPersistedSyncRulesContent.js +58 -0
- package/dist/storage/sync-rules/PostgresPersistedSyncRulesContent.js.map +1 -0
- package/dist/types/codecs.js +97 -0
- package/dist/types/codecs.js.map +1 -0
- package/dist/types/models/ActiveCheckpoint.js +12 -0
- package/dist/types/models/ActiveCheckpoint.js.map +1 -0
- package/dist/types/models/ActiveCheckpointNotification.js +8 -0
- package/dist/types/models/ActiveCheckpointNotification.js.map +1 -0
- package/dist/types/models/BucketData.js +23 -0
- package/dist/types/models/BucketData.js.map +1 -0
- package/dist/types/models/BucketParameters.js +11 -0
- package/dist/types/models/BucketParameters.js.map +1 -0
- package/dist/types/models/CurrentData.js +16 -0
- package/dist/types/models/CurrentData.js.map +1 -0
- package/dist/types/models/Instance.js +5 -0
- package/dist/types/models/Instance.js.map +1 -0
- package/dist/types/models/Migration.js +12 -0
- package/dist/types/models/Migration.js.map +1 -0
- package/dist/types/models/SourceTable.js +24 -0
- package/dist/types/models/SourceTable.js.map +1 -0
- package/dist/types/models/SyncRules.js +47 -0
- package/dist/types/models/SyncRules.js.map +1 -0
- package/dist/types/models/WriteCheckpoint.js +13 -0
- package/dist/types/models/WriteCheckpoint.js.map +1 -0
- package/dist/types/models/models-index.js +11 -0
- package/dist/types/models/models-index.js.map +1 -0
- package/dist/types/types.js +46 -0
- package/dist/types/types.js.map +1 -0
- package/dist/utils/bson.js +16 -0
- package/dist/utils/bson.js.map +1 -0
- package/dist/utils/bucket-data.js +25 -0
- package/dist/utils/bucket-data.js.map +1 -0
- package/dist/utils/db.js +24 -0
- package/dist/utils/db.js.map +1 -0
- package/dist/utils/ts-codec.js +11 -0
- package/dist/utils/ts-codec.js.map +1 -0
- package/dist/utils/utils-index.js +5 -0
- package/dist/utils/utils-index.js.map +1 -0
- package/package.json +50 -0
- package/src/index.ts +10 -0
- package/src/migrations/PostgresMigrationAgent.ts +46 -0
- package/src/migrations/PostgresMigrationStore.ts +70 -0
- package/src/migrations/migration-utils.ts +14 -0
- package/src/migrations/scripts/1684951997326-init.ts +141 -0
- package/src/module/PostgresStorageModule.ts +30 -0
- package/src/storage/PostgresBucketStorageFactory.ts +496 -0
- package/src/storage/PostgresCompactor.ts +366 -0
- package/src/storage/PostgresStorageProvider.ts +42 -0
- package/src/storage/PostgresSyncRulesStorage.ts +666 -0
- package/src/storage/PostgresTestStorageFactoryGenerator.ts +61 -0
- package/src/storage/batch/OperationBatch.ts +101 -0
- package/src/storage/batch/PostgresBucketBatch.ts +885 -0
- package/src/storage/batch/PostgresPersistedBatch.ts +441 -0
- package/src/storage/checkpoints/PostgresWriteCheckpointAPI.ts +176 -0
- package/src/storage/storage-index.ts +5 -0
- package/src/storage/sync-rules/PostgresPersistedSyncRulesContent.ts +67 -0
- package/src/types/codecs.ts +136 -0
- package/src/types/models/ActiveCheckpoint.ts +15 -0
- package/src/types/models/ActiveCheckpointNotification.ts +14 -0
- package/src/types/models/BucketData.ts +26 -0
- package/src/types/models/BucketParameters.ts +14 -0
- package/src/types/models/CurrentData.ts +23 -0
- package/src/types/models/Instance.ts +8 -0
- package/src/types/models/Migration.ts +19 -0
- package/src/types/models/SourceTable.ts +32 -0
- package/src/types/models/SyncRules.ts +50 -0
- package/src/types/models/WriteCheckpoint.ts +20 -0
- package/src/types/models/models-index.ts +10 -0
- package/src/types/types.ts +73 -0
- package/src/utils/bson.ts +17 -0
- package/src/utils/bucket-data.ts +25 -0
- package/src/utils/db.ts +27 -0
- package/src/utils/ts-codec.ts +14 -0
- package/src/utils/utils-index.ts +4 -0
- package/test/src/__snapshots__/storage.test.ts.snap +9 -0
- package/test/src/__snapshots__/storage_sync.test.ts.snap +332 -0
- package/test/src/env.ts +6 -0
- package/test/src/migrations.test.ts +34 -0
- package/test/src/setup.ts +16 -0
- package/test/src/storage.test.ts +131 -0
- package/test/src/storage_compacting.test.ts +5 -0
- package/test/src/storage_sync.test.ts +12 -0
- package/test/src/util.ts +34 -0
- package/test/tsconfig.json +20 -0
- package/tsconfig.json +36 -0
- 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
|
+
}
|