@powersync/service-module-postgres 0.19.2 → 0.19.4

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 (76) hide show
  1. package/dist/api/PostgresRouteAPIAdapter.d.ts +1 -1
  2. package/dist/api/PostgresRouteAPIAdapter.js +63 -72
  3. package/dist/api/PostgresRouteAPIAdapter.js.map +1 -1
  4. package/dist/module/PostgresModule.js.map +1 -1
  5. package/dist/replication/MissingReplicationSlotError.d.ts +41 -0
  6. package/dist/replication/MissingReplicationSlotError.js +33 -0
  7. package/dist/replication/MissingReplicationSlotError.js.map +1 -0
  8. package/dist/replication/PostgresErrorRateLimiter.js +1 -1
  9. package/dist/replication/PostgresErrorRateLimiter.js.map +1 -1
  10. package/dist/replication/SnapshotQuery.js +2 -2
  11. package/dist/replication/SnapshotQuery.js.map +1 -1
  12. package/dist/replication/WalStream.d.ts +37 -14
  13. package/dist/replication/WalStream.js +145 -41
  14. package/dist/replication/WalStream.js.map +1 -1
  15. package/dist/replication/WalStreamReplicationJob.d.ts +1 -1
  16. package/dist/replication/WalStreamReplicationJob.js +7 -4
  17. package/dist/replication/WalStreamReplicationJob.js.map +1 -1
  18. package/dist/replication/WalStreamReplicator.d.ts +0 -1
  19. package/dist/replication/WalStreamReplicator.js +0 -22
  20. package/dist/replication/WalStreamReplicator.js.map +1 -1
  21. package/dist/replication/replication-index.d.ts +3 -1
  22. package/dist/replication/replication-index.js +3 -1
  23. package/dist/replication/replication-index.js.map +1 -1
  24. package/dist/replication/replication-utils.d.ts +3 -11
  25. package/dist/replication/replication-utils.js +101 -164
  26. package/dist/replication/replication-utils.js.map +1 -1
  27. package/dist/replication/wal-budget-utils.d.ts +23 -0
  28. package/dist/replication/wal-budget-utils.js +57 -0
  29. package/dist/replication/wal-budget-utils.js.map +1 -0
  30. package/dist/types/registry.js +1 -1
  31. package/dist/types/registry.js.map +1 -1
  32. package/package.json +15 -11
  33. package/sql/check-source-configuration.plpgsql +13 -0
  34. package/sql/debug-tables-info-batched.plpgsql +230 -0
  35. package/CHANGELOG.md +0 -843
  36. package/src/api/PostgresRouteAPIAdapter.ts +0 -356
  37. package/src/index.ts +0 -1
  38. package/src/module/PostgresModule.ts +0 -122
  39. package/src/replication/ConnectionManagerFactory.ts +0 -33
  40. package/src/replication/PgManager.ts +0 -122
  41. package/src/replication/PgRelation.ts +0 -41
  42. package/src/replication/PostgresErrorRateLimiter.ts +0 -48
  43. package/src/replication/SnapshotQuery.ts +0 -213
  44. package/src/replication/WalStream.ts +0 -1157
  45. package/src/replication/WalStreamReplicationJob.ts +0 -138
  46. package/src/replication/WalStreamReplicator.ts +0 -79
  47. package/src/replication/replication-index.ts +0 -5
  48. package/src/replication/replication-utils.ts +0 -398
  49. package/src/types/registry.ts +0 -275
  50. package/src/types/resolver.ts +0 -227
  51. package/src/types/types.ts +0 -44
  52. package/src/utils/application-name.ts +0 -8
  53. package/src/utils/migration_lib.ts +0 -80
  54. package/src/utils/populate_test_data.ts +0 -37
  55. package/src/utils/populate_test_data_worker.ts +0 -53
  56. package/src/utils/postgres_version.ts +0 -8
  57. package/test/src/checkpoints.test.ts +0 -86
  58. package/test/src/chunked_snapshots.test.ts +0 -161
  59. package/test/src/env.ts +0 -11
  60. package/test/src/large_batch.test.ts +0 -241
  61. package/test/src/pg_test.test.ts +0 -729
  62. package/test/src/resuming_snapshots.test.ts +0 -160
  63. package/test/src/route_api_adapter.test.ts +0 -62
  64. package/test/src/schema_changes.test.ts +0 -655
  65. package/test/src/setup.ts +0 -12
  66. package/test/src/slow_tests.test.ts +0 -519
  67. package/test/src/storage_combination.test.ts +0 -35
  68. package/test/src/types/registry.test.ts +0 -149
  69. package/test/src/util.ts +0 -151
  70. package/test/src/validation.test.ts +0 -63
  71. package/test/src/wal_stream.test.ts +0 -607
  72. package/test/src/wal_stream_utils.ts +0 -284
  73. package/test/tsconfig.json +0 -27
  74. package/tsconfig.json +0 -34
  75. package/tsconfig.tsbuildinfo +0 -1
  76. package/vitest.config.ts +0 -3
@@ -1,356 +0,0 @@
1
- import * as lib_postgres from '@powersync/lib-service-postgres';
2
- import { ErrorCode, ServiceError } from '@powersync/lib-services-framework';
3
- import { api, ParseSyncRulesOptions, ReplicationHeadCallback } from '@powersync/service-core';
4
- import * as pgwire from '@powersync/service-jpgwire';
5
- import * as sync_rules from '@powersync/service-sync-rules';
6
- import * as service_types from '@powersync/service-types';
7
- import * as replication_utils from '../replication/replication-utils.js';
8
- import { getDebugTableInfo } from '../replication/replication-utils.js';
9
- import { KEEPALIVE_STATEMENT, PUBLICATION_NAME } from '../replication/WalStream.js';
10
- import * as types from '../types/types.js';
11
- import { getApplicationName } from '../utils/application-name.js';
12
- import { CustomTypeRegistry } from '../types/registry.js';
13
- import { PostgresTypeResolver } from '../types/resolver.js';
14
-
15
- export class PostgresRouteAPIAdapter implements api.RouteAPI {
16
- private typeCache: PostgresTypeResolver;
17
- connectionTag: string;
18
- // TODO this should probably be configurable one day
19
- publicationName = PUBLICATION_NAME;
20
-
21
- static withConfig(config: types.ResolvedConnectionConfig) {
22
- const pool = pgwire.connectPgWirePool(config, {
23
- idleTimeout: 30_000,
24
- applicationName: getApplicationName()
25
- });
26
- return new PostgresRouteAPIAdapter(pool, config.tag, config);
27
- }
28
-
29
- /**
30
- * @param config - Required for the service; optional for tests.
31
- */
32
- constructor(
33
- protected pool: pgwire.PgClient,
34
- connectionTag?: string,
35
- private config?: types.ResolvedConnectionConfig
36
- ) {
37
- this.typeCache = new PostgresTypeResolver(pool);
38
- this.connectionTag = connectionTag ?? sync_rules.DEFAULT_TAG;
39
- }
40
-
41
- getParseSyncRulesOptions(): ParseSyncRulesOptions {
42
- return {
43
- defaultSchema: 'public'
44
- };
45
- }
46
-
47
- async shutdown(): Promise<void> {
48
- await this.pool.end();
49
- }
50
-
51
- async getSourceConfig(): Promise<service_types.configFile.ResolvedDataSourceConfig> {
52
- return this.config!;
53
- }
54
-
55
- async getConnectionStatus(): Promise<service_types.ConnectionStatusV2> {
56
- const base = {
57
- id: this.config?.id ?? '',
58
- uri: this.config == null ? '' : types.baseUri(this.config)
59
- };
60
-
61
- try {
62
- await lib_postgres.retriedQuery(this.pool, `SELECT 'PowerSync connection test'`);
63
- } catch (e) {
64
- return {
65
- ...base,
66
- connected: false,
67
- errors: [{ level: 'fatal', message: e.message }]
68
- };
69
- }
70
-
71
- try {
72
- await replication_utils.checkSourceConfiguration(this.pool, this.publicationName);
73
- } catch (e) {
74
- return {
75
- ...base,
76
- connected: true,
77
- errors: [{ level: 'fatal', message: e.message }]
78
- };
79
- }
80
-
81
- return {
82
- ...base,
83
- connected: true,
84
- errors: []
85
- };
86
- }
87
-
88
- async executeQuery(query: string, params: any[]): Promise<service_types.internal_routes.ExecuteSqlResponse> {
89
- if (!this.config?.debug_api) {
90
- return service_types.internal_routes.ExecuteSqlResponse.encode({
91
- results: {
92
- columns: [],
93
- rows: []
94
- },
95
- success: false,
96
- error: 'SQL querying is not enabled'
97
- });
98
- }
99
-
100
- try {
101
- const result = await this.pool.query({
102
- statement: query,
103
- params: params.map(lib_postgres.autoParameter)
104
- });
105
-
106
- return service_types.internal_routes.ExecuteSqlResponse.encode({
107
- success: true,
108
- results: {
109
- columns: result.columns.map((c) => c.name),
110
- rows: result.rows.map((row) => {
111
- return row.raw.map((raw, i) => {
112
- const value = pgwire.PgType.decode(raw, row.columns[i].typeOid);
113
- const sqlValue = sync_rules.applyValueContext(
114
- sync_rules.toSyncRulesValue(value),
115
- sync_rules.CompatibilityContext.FULL_BACKWARDS_COMPATIBILITY
116
- );
117
- if (typeof sqlValue == 'bigint') {
118
- return Number(value);
119
- } else if (sync_rules.isJsonValue(sqlValue)) {
120
- return sqlValue;
121
- } else {
122
- return null;
123
- }
124
- });
125
- })
126
- }
127
- });
128
- } catch (e) {
129
- return service_types.internal_routes.ExecuteSqlResponse.encode({
130
- results: {
131
- columns: [],
132
- rows: []
133
- },
134
- success: false,
135
- error: e.message
136
- });
137
- }
138
- }
139
-
140
- async getDebugTablesInfo(
141
- tablePatterns: sync_rules.TablePattern[],
142
- sqlSyncRules: sync_rules.SqlSyncRules
143
- ): Promise<api.PatternResult[]> {
144
- let result: api.PatternResult[] = [];
145
-
146
- for (let tablePattern of tablePatterns) {
147
- const schema = tablePattern.schema;
148
-
149
- let patternResult: api.PatternResult = {
150
- schema: schema,
151
- pattern: tablePattern.tablePattern,
152
- wildcard: tablePattern.isWildcard
153
- };
154
- result.push(patternResult);
155
-
156
- if (tablePattern.isWildcard) {
157
- patternResult.tables = [];
158
- const prefix = tablePattern.tablePrefix;
159
- const results = await lib_postgres.retriedQuery(this.pool, {
160
- statement: `SELECT c.oid AS relid, c.relname AS table_name
161
- FROM pg_class c
162
- JOIN pg_namespace n ON n.oid = c.relnamespace
163
- WHERE n.nspname = $1
164
- AND c.relkind = 'r'
165
- AND c.relname LIKE $2`,
166
- params: [
167
- { type: 'varchar', value: schema },
168
- { type: 'varchar', value: tablePattern.tablePattern }
169
- ]
170
- });
171
-
172
- for (let row of pgwire.pgwireRows(results)) {
173
- const name = row.table_name as string;
174
- const relationId = row.relid as number;
175
- if (!name.startsWith(prefix)) {
176
- continue;
177
- }
178
- const details = await this.getDebugTableInfo(tablePattern, name, relationId, sqlSyncRules);
179
- patternResult.tables.push(details);
180
- }
181
- } else {
182
- const results = await lib_postgres.retriedQuery(this.pool, {
183
- statement: `SELECT c.oid AS relid, c.relname AS table_name
184
- FROM pg_class c
185
- JOIN pg_namespace n ON n.oid = c.relnamespace
186
- WHERE n.nspname = $1
187
- AND c.relkind = 'r'
188
- AND c.relname = $2`,
189
- params: [
190
- { type: 'varchar', value: schema },
191
- { type: 'varchar', value: tablePattern.tablePattern }
192
- ]
193
- });
194
- if (results.rows.length == 0) {
195
- // Table not found
196
- patternResult.table = await this.getDebugTableInfo(tablePattern, tablePattern.name, null, sqlSyncRules);
197
- } else {
198
- const row = pgwire.pgwireRows(results)[0];
199
- const name = row.table_name as string;
200
- const relationId = row.relid as number;
201
- patternResult.table = await this.getDebugTableInfo(tablePattern, name, relationId, sqlSyncRules);
202
- }
203
- }
204
- }
205
- return result;
206
- }
207
-
208
- protected async getDebugTableInfo(
209
- tablePattern: sync_rules.TablePattern,
210
- name: string,
211
- relationId: number | null,
212
- syncRules: sync_rules.SqlSyncRules
213
- ): Promise<service_types.TableInfo> {
214
- return getDebugTableInfo({
215
- db: this.pool,
216
- name: name,
217
- publicationName: this.publicationName,
218
- connectionTag: this.connectionTag,
219
- tablePattern: tablePattern,
220
- relationId: relationId,
221
- syncRules: syncRules
222
- });
223
- }
224
-
225
- async getReplicationLagBytes(options: api.ReplicationLagOptions): Promise<number | undefined> {
226
- const { bucketStorage } = options;
227
- const slotName = bucketStorage.slot_name;
228
- const results = await lib_postgres.retriedQuery(this.pool, {
229
- statement: `SELECT
230
- slot_name,
231
- confirmed_flush_lsn,
232
- pg_current_wal_lsn(),
233
- (pg_current_wal_lsn() - confirmed_flush_lsn) AS lsn_distance
234
- FROM pg_replication_slots WHERE slot_name = $1 LIMIT 1;`,
235
- params: [{ type: 'varchar', value: slotName }]
236
- });
237
- const [row] = pgwire.pgwireRows(results);
238
- if (row) {
239
- return Number(row.lsn_distance);
240
- }
241
-
242
- throw new ServiceError({
243
- status: 500,
244
- code: ErrorCode.PSYNC_S4001,
245
- description: `Could not determine replication lag for slot ${slotName}`
246
- });
247
- }
248
-
249
- async getReplicationHead(): Promise<string> {
250
- // On most Postgres versions, pg_logical_emit_message() returns the correct LSN.
251
- // However, on Aurora (Postgres compatible), it can return an entirely different LSN,
252
- // causing the write checkpoints to never be replicated back to the client.
253
- // For those, we need to use pg_current_wal_lsn() instead.
254
- const { results } = await lib_postgres.retriedQuery(this.pool, `SELECT pg_current_wal_lsn() as lsn`);
255
-
256
- const lsn = results[0].rows[0].decodeWithoutCustomTypes(0);
257
- return String(lsn);
258
- }
259
-
260
- async createReplicationHead<T>(callback: ReplicationHeadCallback<T>): Promise<T> {
261
- const currentLsn = await this.getReplicationHead();
262
-
263
- const r = await callback(currentLsn);
264
-
265
- // Note: This may not reliably trigger a new replication message on Postgres 11 or 12,
266
- // in which case there could be a delay in the client receiving the write checkpoint acknowledgement.
267
- // Postgres 12 already reached EOL, and this is not a critical issue, so we're not fixing it.
268
- // On postgres 13+, this works reliably.
269
- await lib_postgres.retriedQuery(this.pool, KEEPALIVE_STATEMENT);
270
-
271
- return r;
272
- }
273
-
274
- async getConnectionSchema(): Promise<service_types.DatabaseSchema[]> {
275
- // https://github.com/Borvik/vscode-postgres/blob/88ec5ed061a0c9bced6c5d4ec122d0759c3f3247/src/language/server.ts
276
- const results = await lib_postgres.retriedQuery(
277
- this.pool,
278
- `SELECT
279
- tbl.schemaname,
280
- tbl.tablename,
281
- tbl.quoted_name,
282
- json_agg(a ORDER BY attnum) as columns
283
- FROM
284
- (
285
- SELECT
286
- n.nspname as schemaname,
287
- c.relname as tablename,
288
- (quote_ident(n.nspname) || '.' || quote_ident(c.relname)) as quoted_name
289
- FROM
290
- pg_catalog.pg_class c
291
- JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
292
- WHERE
293
- c.relkind = 'r'
294
- AND n.nspname not in ('information_schema', 'pg_catalog', 'pg_toast')
295
- AND n.nspname not like 'pg_temp_%'
296
- AND n.nspname not like 'pg_toast_temp_%'
297
- AND c.relnatts > 0
298
- AND has_schema_privilege(n.oid, 'USAGE') = true
299
- AND has_table_privilege(quote_ident(n.nspname) || '.' || quote_ident(c.relname), 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER') = true
300
- ) as tbl
301
- LEFT JOIN (
302
- SELECT
303
- attrelid,
304
- attname,
305
- atttypid,
306
- format_type(atttypid, atttypmod) as data_type,
307
- (SELECT typname FROM pg_catalog.pg_type WHERE oid = atttypid) as pg_type,
308
- attnum,
309
- attisdropped
310
- FROM
311
- pg_attribute
312
- ) as a ON (
313
- a.attrelid = tbl.quoted_name::regclass
314
- AND a.attnum > 0
315
- AND NOT a.attisdropped
316
- AND has_column_privilege(tbl.quoted_name, a.attname, 'SELECT, INSERT, UPDATE, REFERENCES')
317
- )
318
- GROUP BY schemaname, tablename, quoted_name`
319
- );
320
- await this.typeCache.fetchTypesForSchema();
321
- const rows = pgwire.pgwireRows(results);
322
-
323
- let schemas: Record<string, service_types.DatabaseSchema> = {};
324
-
325
- for (let row of rows) {
326
- const schema = (schemas[row.schemaname] ??= {
327
- name: row.schemaname,
328
- tables: []
329
- });
330
- const table: service_types.TableSchema = {
331
- name: row.tablename,
332
- columns: [] as any[]
333
- };
334
- schema.tables.push(table);
335
-
336
- const columnInfo = JSON.parse(row.columns);
337
- for (let column of columnInfo) {
338
- let pg_type = column.pg_type as string;
339
- if (pg_type.startsWith('_')) {
340
- pg_type = `${pg_type.substring(1)}[]`;
341
- }
342
-
343
- const knownType = this.typeCache.registry.lookupType(Number(column.atttypid));
344
- table.columns.push({
345
- name: column.attname,
346
- sqlite_type: sync_rules.ExpressionType.fromTypeText(knownType.sqliteType()).typeFlags,
347
- type: column.data_type,
348
- internal_type: column.data_type,
349
- pg_type: pg_type
350
- });
351
- }
352
- }
353
-
354
- return Object.values(schemas);
355
- }
356
- }
package/src/index.ts DELETED
@@ -1 +0,0 @@
1
- export * from './module/PostgresModule.js';
@@ -1,122 +0,0 @@
1
- import { baseUri, NormalizedBasePostgresConnectionConfig } from '@powersync/lib-service-postgres';
2
- import {
3
- api,
4
- ConfigurationFileSyncRulesProvider,
5
- ConnectionTestResult,
6
- modules,
7
- replication,
8
- system
9
- } from '@powersync/service-core';
10
- import * as jpgwire from '@powersync/service-jpgwire';
11
- import { ReplicationMetric } from '@powersync/service-types';
12
- import { PostgresRouteAPIAdapter } from '../api/PostgresRouteAPIAdapter.js';
13
- import { ConnectionManagerFactory } from '../replication/ConnectionManagerFactory.js';
14
- import { PgManager } from '../replication/PgManager.js';
15
- import { PostgresErrorRateLimiter } from '../replication/PostgresErrorRateLimiter.js';
16
- import { checkSourceConfiguration, cleanUpReplicationSlot } from '../replication/replication-utils.js';
17
- import { PUBLICATION_NAME } from '../replication/WalStream.js';
18
- import { WalStreamReplicator } from '../replication/WalStreamReplicator.js';
19
- import * as types from '../types/types.js';
20
- import { PostgresConnectionConfig } from '../types/types.js';
21
- import { getApplicationName } from '../utils/application-name.js';
22
- import { CustomTypeRegistry } from '../types/registry.js';
23
-
24
- export class PostgresModule extends replication.ReplicationModule<types.PostgresConnectionConfig> {
25
- constructor() {
26
- super({
27
- name: 'Postgres',
28
- type: types.POSTGRES_CONNECTION_TYPE,
29
- configSchema: types.PostgresConnectionConfig
30
- });
31
- }
32
-
33
- async onInitialized(context: system.ServiceContextContainer): Promise<void> {
34
- // Record replicated bytes using global jpgwire metrics. Only registered if this module is replicating
35
- if (context.replicationEngine) {
36
- jpgwire.setMetricsRecorder({
37
- addBytesRead(bytes) {
38
- context.metricsEngine.getCounter(ReplicationMetric.DATA_REPLICATED_BYTES).add(bytes);
39
- }
40
- });
41
- this.logger.info('Successfully set up connection metrics recorder for PostgresModule.');
42
- }
43
- }
44
-
45
- protected createRouteAPIAdapter(): api.RouteAPI {
46
- return PostgresRouteAPIAdapter.withConfig(this.resolveConfig(this.decodedConfig!));
47
- }
48
-
49
- protected createReplicator(context: system.ServiceContext): replication.AbstractReplicator {
50
- const normalisedConfig = this.resolveConfig(this.decodedConfig!);
51
- const syncRuleProvider = new ConfigurationFileSyncRulesProvider(context.configuration.sync_rules);
52
- const connectionFactory = new ConnectionManagerFactory(normalisedConfig);
53
-
54
- return new WalStreamReplicator({
55
- id: this.getDefaultId(normalisedConfig.database),
56
- syncRuleProvider: syncRuleProvider,
57
- storageEngine: context.storageEngine,
58
- metricsEngine: context.metricsEngine,
59
- connectionFactory: connectionFactory,
60
- rateLimiter: new PostgresErrorRateLimiter()
61
- });
62
- }
63
-
64
- /**
65
- * Combines base config with normalized connection settings
66
- */
67
- private resolveConfig(config: types.PostgresConnectionConfig): types.ResolvedConnectionConfig {
68
- return {
69
- ...config,
70
- ...types.normalizeConnectionConfig(config)
71
- };
72
- }
73
-
74
- async teardown(options: modules.TearDownOptions): Promise<void> {
75
- const normalisedConfig = this.resolveConfig(this.decodedConfig!);
76
- const connectionManager = new PgManager(normalisedConfig, {
77
- idleTimeout: 30_000,
78
- maxSize: 1,
79
- applicationName: getApplicationName()
80
- });
81
-
82
- try {
83
- if (options.syncRules) {
84
- // TODO: In the future, once we have more replication types, we will need to check if these syncRules are for Postgres
85
- for (let syncRules of options.syncRules) {
86
- try {
87
- await cleanUpReplicationSlot(syncRules.slot_name, connectionManager.pool);
88
- } catch (e) {
89
- // Not really much we can do here for failures, most likely the database is no longer accessible
90
- this.logger.warn(`Failed to fully clean up Postgres replication slot: ${syncRules.slot_name}`, e);
91
- }
92
- }
93
- }
94
- } finally {
95
- await connectionManager.end();
96
- }
97
- }
98
-
99
- async testConnection(config: PostgresConnectionConfig): Promise<ConnectionTestResult> {
100
- this.decodeConfig(config);
101
- const normalizedConfig = this.resolveConfig(this.decodedConfig!);
102
- return await this.testConnection(normalizedConfig);
103
- }
104
-
105
- static async testConnection(normalizedConfig: NormalizedBasePostgresConnectionConfig): Promise<ConnectionTestResult> {
106
- // FIXME: This is not a complete implementation yet.
107
- const connectionManager = new PgManager(normalizedConfig, {
108
- idleTimeout: 30_000,
109
- maxSize: 1,
110
- applicationName: getApplicationName()
111
- });
112
- const connection = await connectionManager.snapshotConnection();
113
- try {
114
- await checkSourceConfiguration(connection, PUBLICATION_NAME);
115
- } finally {
116
- await connectionManager.end();
117
- }
118
- return {
119
- connectionDescription: baseUri(normalizedConfig)
120
- };
121
- }
122
- }
@@ -1,33 +0,0 @@
1
- import { logger } from '@powersync/lib-services-framework';
2
- import { PgPoolOptions } from '@powersync/service-jpgwire';
3
- import { NormalizedPostgresConnectionConfig } from '../types/types.js';
4
- import { PgManager } from './PgManager.js';
5
-
6
- export class ConnectionManagerFactory {
7
- private readonly connectionManagers = new Set<PgManager>();
8
- public readonly dbConnectionConfig: NormalizedPostgresConnectionConfig;
9
-
10
- constructor(dbConnectionConfig: NormalizedPostgresConnectionConfig) {
11
- this.dbConnectionConfig = dbConnectionConfig;
12
- }
13
-
14
- create(poolOptions: PgPoolOptions) {
15
- const manager = new PgManager(this.dbConnectionConfig, { ...poolOptions });
16
- this.connectionManagers.add(manager);
17
-
18
- manager.registerListener({
19
- onEnded: () => {
20
- this.connectionManagers.delete(manager);
21
- }
22
- });
23
- return manager;
24
- }
25
-
26
- async shutdown() {
27
- logger.info('Shutting down Postgres connection Managers...');
28
- for (const manager of [...this.connectionManagers]) {
29
- await manager.end();
30
- }
31
- logger.info('Postgres connection Managers shutdown completed.');
32
- }
33
- }
@@ -1,122 +0,0 @@
1
- import { BaseObserver } from '@powersync/lib-services-framework';
2
- import * as pgwire from '@powersync/service-jpgwire';
3
- import semver from 'semver';
4
- import { PostgresTypeResolver } from '../types/resolver.js';
5
- import { NormalizedPostgresConnectionConfig } from '../types/types.js';
6
- import { getApplicationName } from '../utils/application-name.js';
7
- import { getServerVersion } from '../utils/postgres_version.js';
8
-
9
- export interface PgManagerOptions extends pgwire.PgPoolOptions {}
10
-
11
- /**
12
- * Shorter timeout for snapshot connections than for replication connections.
13
- */
14
- const SNAPSHOT_SOCKET_TIMEOUT = 30_000;
15
-
16
- export interface PgManagerListener {
17
- onEnded(): void;
18
- }
19
-
20
- export class PgManager extends BaseObserver<PgManagerListener> {
21
- /**
22
- * Do not use this for any transactions.
23
- */
24
- public readonly pool: pgwire.PgClient;
25
-
26
- public readonly types: PostgresTypeResolver;
27
-
28
- private connectionPromises: Promise<pgwire.PgConnection>[] = [];
29
-
30
- constructor(
31
- public options: NormalizedPostgresConnectionConfig,
32
- public poolOptions: PgManagerOptions
33
- ) {
34
- super();
35
- // The pool is lazy - no connections are opened until a query is performed.
36
- this.pool = pgwire.connectPgWirePool(this.options, poolOptions);
37
- this.types = new PostgresTypeResolver(this.pool);
38
- }
39
-
40
- public get connectionTag() {
41
- return this.options.tag;
42
- }
43
-
44
- /**
45
- * Create a new replication connection.
46
- */
47
- async replicationConnection(): Promise<pgwire.PgConnection> {
48
- const p = pgwire.connectPgWire(this.options, { type: 'replication', applicationName: getApplicationName() });
49
- this.connectionPromises.push(p);
50
- return await p;
51
- }
52
-
53
- /**
54
- * @returns The Postgres server version in a parsed Semver instance
55
- */
56
- async getServerVersion(): Promise<semver.SemVer | null> {
57
- return await getServerVersion(this.pool);
58
- }
59
-
60
- /**
61
- * Create a new standard connection, used for initial snapshot.
62
- *
63
- * This connection must not be shared between multiple async contexts.
64
- */
65
- async snapshotConnection(): Promise<pgwire.PgConnection> {
66
- const p = pgwire.connectPgWire(this.options, { type: 'standard', applicationName: getApplicationName() });
67
- this.connectionPromises.push(p);
68
- const connection = await p;
69
-
70
- // Use an shorter timeout for snapshot connections.
71
- // This is to detect broken connections early, instead of waiting
72
- // for the full 6 minutes.
73
- // This we are constantly using the connection, we don't need any
74
- // custom keepalives.
75
-
76
- (connection as any)._socket.setTimeout(SNAPSHOT_SOCKET_TIMEOUT);
77
-
78
- // Disable statement timeout for snapshot queries.
79
- // On Supabase, the default is 2 minutes.
80
- await connection.query(`set session statement_timeout = 0`);
81
-
82
- return connection;
83
- }
84
-
85
- async end(): Promise<void> {
86
- for (let result of await Promise.allSettled([
87
- this.pool.end(),
88
- ...this.connectionPromises.map(async (promise) => {
89
- // Wait for connection attempts to finish, but do not throw connection errors here
90
- const connection = await promise.catch((_) => {});
91
- return await connection?.end();
92
- })
93
- ])) {
94
- // Throw the first error, if any
95
- if (result.status == 'rejected') {
96
- throw result.reason;
97
- }
98
- }
99
- this.iterateListeners((listener) => {
100
- listener.onEnded?.();
101
- });
102
- }
103
-
104
- async destroy() {
105
- this.pool.destroy();
106
- for (let result of await Promise.allSettled([
107
- ...this.connectionPromises.map(async (promise) => {
108
- // Wait for connection attempts to finish, but do not throw connection errors here
109
- const connection = await promise.catch((_) => {});
110
- return connection?.destroy();
111
- })
112
- ])) {
113
- // Throw the first error, if any
114
- if (result.status == 'rejected') {
115
- throw result.reason;
116
- }
117
- }
118
- this.iterateListeners((listener) => {
119
- listener.onEnded?.();
120
- });
121
- }
122
- }
@@ -1,41 +0,0 @@
1
- import { ReplicationAssertionError } from '@powersync/lib-services-framework';
2
- import { storage } from '@powersync/service-core';
3
- import { PgoutputRelation } from '@powersync/service-jpgwire';
4
-
5
- export type ReplicationIdentity = 'default' | 'nothing' | 'full' | 'index';
6
-
7
- export function getReplicaIdColumns(relation: PgoutputRelation): storage.ColumnDescriptor[] {
8
- if (relation.replicaIdentity == 'nothing') {
9
- return [];
10
- } else {
11
- return relation.columns
12
- .filter((c) => (c.flags & 0b1) != 0)
13
- .map((c) => ({ name: c.name, typeId: c.typeOid }) satisfies storage.ColumnDescriptor);
14
- }
15
- }
16
- export function getRelId(source: PgoutputRelation): number {
17
- // Source types are wrong here
18
- const relId = (source as any).relationOid as number;
19
- if (!relId) {
20
- throw new ReplicationAssertionError(`No relation id found`);
21
- }
22
- return relId;
23
- }
24
-
25
- export function getPgOutputRelation(source: PgoutputRelation): storage.SourceEntityDescriptor {
26
- return {
27
- name: source.name,
28
- schema: source.schema,
29
- objectId: getRelId(source),
30
- replicaIdColumns: getReplicaIdColumns(source)
31
- } satisfies storage.SourceEntityDescriptor;
32
- }
33
-
34
- export function referencedColumnTypeIds(source: PgoutputRelation): number[] {
35
- const oids = new Set<number>();
36
- for (const column of source.columns) {
37
- oids.add(column.typeOid);
38
- }
39
-
40
- return [...oids];
41
- }