@powersync/lib-service-postgres 0.0.0-dev-20250117095455 → 0.0.0-dev-20250227082606

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 (35) hide show
  1. package/CHANGELOG.md +48 -8
  2. package/dist/db/connection/AbstractPostgresConnection.d.ts +1 -1
  3. package/dist/db/connection/AbstractPostgresConnection.js +1 -1
  4. package/dist/db/connection/AbstractPostgresConnection.js.map +1 -1
  5. package/dist/db/connection/ConnectionSlot.d.ts +2 -3
  6. package/dist/db/connection/ConnectionSlot.js +18 -26
  7. package/dist/db/connection/ConnectionSlot.js.map +1 -1
  8. package/dist/db/connection/DatabaseClient.d.ts +1 -1
  9. package/dist/db/connection/DatabaseClient.js +13 -3
  10. package/dist/db/connection/DatabaseClient.js.map +1 -1
  11. package/dist/db/connection/WrappedConnection.js +1 -0
  12. package/dist/db/connection/WrappedConnection.js.map +1 -1
  13. package/dist/locks/PostgresLockManager.js +2 -1
  14. package/dist/locks/PostgresLockManager.js.map +1 -1
  15. package/dist/types/types.d.ts +3 -0
  16. package/dist/types/types.js +13 -11
  17. package/dist/types/types.js.map +1 -1
  18. package/dist/utils/identifier-utils.d.ts +8 -0
  19. package/dist/utils/identifier-utils.js +24 -0
  20. package/dist/utils/identifier-utils.js.map +1 -0
  21. package/dist/utils/pgwire_utils.js +2 -2
  22. package/dist/utils/pgwire_utils.js.map +1 -1
  23. package/dist/utils/utils-index.d.ts +1 -0
  24. package/dist/utils/utils-index.js +1 -0
  25. package/dist/utils/utils-index.js.map +1 -1
  26. package/package.json +4 -4
  27. package/src/db/connection/AbstractPostgresConnection.ts +1 -3
  28. package/src/db/connection/ConnectionSlot.ts +9 -24
  29. package/src/db/connection/DatabaseClient.ts +9 -4
  30. package/src/types/types.ts +24 -12
  31. package/src/utils/identifier-utils.ts +38 -0
  32. package/src/utils/pgwire_utils.ts +2 -2
  33. package/src/utils/utils-index.ts +1 -0
  34. package/tsconfig.json +1 -1
  35. package/tsconfig.tsbuildinfo +1 -1
@@ -1,7 +1,7 @@
1
1
  import * as framework from '@powersync/lib-services-framework';
2
2
  import * as pgwire from '@powersync/service-jpgwire';
3
3
 
4
- export interface NotificationListener extends framework.DisposableListener {
4
+ export interface NotificationListener {
5
5
  notification?: (payload: pgwire.PgNotification) => void;
6
6
  }
7
7
 
@@ -23,7 +23,7 @@ export type ConnectionSlotOptions = {
23
23
 
24
24
  export const MAX_CONNECTION_ATTEMPTS = 5;
25
25
 
26
- export class ConnectionSlot extends framework.DisposableObserver<ConnectionSlotListener> {
26
+ export class ConnectionSlot extends framework.BaseObserver<ConnectionSlotListener> {
27
27
  isAvailable: boolean;
28
28
  isPoking: boolean;
29
29
 
@@ -50,9 +50,12 @@ export class ConnectionSlot extends framework.DisposableObserver<ConnectionSlotL
50
50
  const connection = await this.connectingPromise;
51
51
  this.connectingPromise = null;
52
52
  await this.iterateAsyncListeners(async (l) => l.connectionCreated?.(connection));
53
- if (this.hasNotificationListener()) {
54
- await this.configureConnectionNotifications(connection);
55
- }
53
+
54
+ /**
55
+ * Configure the Postgres connection to listen to notifications.
56
+ * Subscribing to notifications, even without a registered listener, should not add much overhead.
57
+ */
58
+ await this.configureConnectionNotifications(connection);
56
59
  return connection;
57
60
  }
58
61
 
@@ -60,15 +63,10 @@ export class ConnectionSlot extends framework.DisposableObserver<ConnectionSlotL
60
63
  this.closed = true;
61
64
  const connection = this.connection ?? (await this.connectingPromise);
62
65
  await connection?.end();
63
- return super[Symbol.dispose]();
66
+ super.clearListeners();
64
67
  }
65
68
 
66
69
  protected async configureConnectionNotifications(connection: pgwire.PgConnection) {
67
- if (connection.onnotification == this.handleNotification || this.closed == true) {
68
- // Already configured
69
- return;
70
- }
71
-
72
70
  connection.onnotification = this.handleNotification;
73
71
 
74
72
  for (const channelName of this.options.notificationChannels ?? []) {
@@ -78,19 +76,6 @@ export class ConnectionSlot extends framework.DisposableObserver<ConnectionSlotL
78
76
  }
79
77
  }
80
78
 
81
- registerListener(listener: Partial<ConnectionSlotListener>): () => void {
82
- const dispose = super.registerListener(listener);
83
- if (this.connection && this.hasNotificationListener()) {
84
- this.configureConnectionNotifications(this.connection);
85
- }
86
- return () => {
87
- dispose();
88
- if (this.connection && !this.hasNotificationListener()) {
89
- this.connection.onnotification = () => {};
90
- }
91
- };
92
- }
93
-
94
79
  protected handleNotification = (payload: pgwire.PgNotification) => {
95
80
  if (!this.options.notificationChannels?.includes(payload.channel)) {
96
81
  return;
@@ -32,7 +32,8 @@ export const TRANSACTION_CONNECTION_COUNT = 5;
32
32
  export class DatabaseClient extends AbstractPostgresConnection<DatabaseClientListener> {
33
33
  closed: boolean;
34
34
 
35
- protected pool: pgwire.PgClient;
35
+ pool: pgwire.PgClient;
36
+
36
37
  protected connections: ConnectionSlot[];
37
38
 
38
39
  protected initialized: Promise<void>;
@@ -41,9 +42,13 @@ export class DatabaseClient extends AbstractPostgresConnection<DatabaseClientLis
41
42
  constructor(protected options: DatabaseClientOptions) {
42
43
  super();
43
44
  this.closed = false;
44
- this.pool = pgwire.connectPgWirePool(options.config);
45
- this.connections = Array.from({ length: TRANSACTION_CONNECTION_COUNT }, () => {
46
- const slot = new ConnectionSlot({ config: options.config, notificationChannels: options.notificationChannels });
45
+ this.pool = pgwire.connectPgWirePool(options.config, {
46
+ maxSize: options.config.max_pool_size
47
+ });
48
+ this.connections = Array.from({ length: TRANSACTION_CONNECTION_COUNT }, (v, index) => {
49
+ // Only listen to notifications on a single (the first) connection
50
+ const notificationChannels = index == 0 ? options.notificationChannels : [];
51
+ const slot = new ConnectionSlot({ config: options.config, notificationChannels });
47
52
  slot.registerListener({
48
53
  connectionAvailable: () => this.processConnectionQueue(),
49
54
  connectionError: (ex) => this.handleConnectionError(ex),
@@ -1,10 +1,12 @@
1
- import { makeHostnameLookupFunction } from '@powersync/lib-services-framework';
1
+ import { ErrorCode, makeHostnameLookupFunction, ServiceError } from '@powersync/lib-services-framework';
2
2
  import type * as jpgwire from '@powersync/service-jpgwire';
3
3
  import * as service_types from '@powersync/service-types';
4
4
  import * as t from 'ts-codec';
5
5
  import * as urijs from 'uri-js';
6
6
 
7
- export interface NormalizedBasePostgresConnectionConfig extends jpgwire.NormalizedConnectionConfig {}
7
+ export interface NormalizedBasePostgresConnectionConfig extends jpgwire.NormalizedConnectionConfig {
8
+ max_pool_size: number;
9
+ }
8
10
 
9
11
  export const POSTGRES_CONNECTION_TYPE = 'postgresql' as const;
10
12
 
@@ -42,7 +44,9 @@ export const BasePostgresConnectionConfig = t.object({
42
44
  /**
43
45
  * Prefix for the slot name. Defaults to "powersync_"
44
46
  */
45
- slot_name_prefix: t.string.optional()
47
+ slot_name_prefix: t.string.optional(),
48
+
49
+ max_pool_size: t.number.optional()
46
50
  });
47
51
 
48
52
  export type BasePostgresConnectionConfig = t.Encoded<typeof BasePostgresConnectionConfig>;
@@ -58,7 +62,10 @@ export function normalizeConnectionConfig(options: BasePostgresConnectionConfigD
58
62
  if (options.uri) {
59
63
  uri = urijs.parse(options.uri);
60
64
  if (uri.scheme != 'postgresql' && uri.scheme != 'postgres') {
61
- `Invalid URI - protocol must be postgresql, got ${uri.scheme}`;
65
+ throw new ServiceError(
66
+ ErrorCode.PSYNC_S1109,
67
+ `Invalid URI - protocol must be postgresql, got ${JSON.stringify(uri.scheme)}`
68
+ );
62
69
  } else if (uri.scheme != 'postgresql') {
63
70
  uri.scheme = 'postgresql';
64
71
  }
@@ -80,23 +87,26 @@ export function normalizeConnectionConfig(options: BasePostgresConnectionConfigD
80
87
  const cacert = options.cacert;
81
88
 
82
89
  if (sslmode == 'verify-ca' && cacert == null) {
83
- throw new Error('Explicit cacert is required for sslmode=verify-ca');
90
+ throw new ServiceError(
91
+ ErrorCode.PSYNC_S1104,
92
+ 'Postgres connection: Explicit cacert is required for `sslmode: verify-ca`'
93
+ );
84
94
  }
85
95
 
86
96
  if (hostname == '') {
87
- throw new Error(`hostname required`);
97
+ throw new ServiceError(ErrorCode.PSYNC_S1106, `Postgres connection: hostname required`);
88
98
  }
89
99
 
90
100
  if (username == '') {
91
- throw new Error(`username required`);
101
+ throw new ServiceError(ErrorCode.PSYNC_S1107, `Postgres connection: username required`);
92
102
  }
93
103
 
94
104
  if (password == '') {
95
- throw new Error(`password required`);
105
+ throw new ServiceError(ErrorCode.PSYNC_S1108, `Postgres connection: password required`);
96
106
  }
97
107
 
98
108
  if (database == '') {
99
- throw new Error(`database required`);
109
+ throw new ServiceError(ErrorCode.PSYNC_S1105, `Postgres connection: database required`);
100
110
  }
101
111
 
102
112
  const lookupOptions = { reject_ip_ranges: options.reject_ip_ranges ?? [] };
@@ -119,7 +129,9 @@ export function normalizeConnectionConfig(options: BasePostgresConnectionConfigD
119
129
  lookup,
120
130
 
121
131
  client_certificate: options.client_certificate ?? undefined,
122
- client_private_key: options.client_private_key ?? undefined
132
+ client_private_key: options.client_private_key ?? undefined,
133
+
134
+ max_pool_size: options.max_pool_size ?? 8
123
135
  } satisfies NormalizedBasePostgresConnectionConfig;
124
136
  }
125
137
 
@@ -132,8 +144,8 @@ export function validatePort(port: string | number): number {
132
144
  if (typeof port == 'string') {
133
145
  port = parseInt(port);
134
146
  }
135
- if (port < 1024) {
136
- throw new Error(`Port ${port} not supported`);
147
+ if (port < 1024 || port > 65535) {
148
+ throw new ServiceError(ErrorCode.PSYNC_S1110, `Port ${port} not supported`);
137
149
  }
138
150
  return port;
139
151
  }
@@ -0,0 +1,38 @@
1
+ import * as pgwire from '@powersync/service-jpgwire';
2
+ import { retriedQuery } from './pgwire_utils.js';
3
+
4
+ export interface DecodedPostgresIdentifier {
5
+ server_id: string;
6
+ database_name: string;
7
+ }
8
+
9
+ export const decodePostgresSystemIdentifier = (identifier: string): DecodedPostgresIdentifier => {
10
+ const [server_id, database_name] = identifier.split('.');
11
+ return { server_id, database_name };
12
+ };
13
+
14
+ export const encodePostgresSystemIdentifier = (decoded: DecodedPostgresIdentifier): string => {
15
+ return `${decoded.server_id}.${decoded.database_name}`;
16
+ };
17
+
18
+ export const queryPostgresSystemIdentifier = async (
19
+ connection: pgwire.PgClient
20
+ ): Promise<DecodedPostgresIdentifier> => {
21
+ const result = pgwire.pgwireRows(
22
+ await retriedQuery(
23
+ connection,
24
+ /* sql */ `
25
+ SELECT
26
+ current_database() AS database_name,
27
+ system_identifier
28
+ FROM
29
+ pg_control_system();
30
+ `
31
+ )
32
+ ) as Array<{ database_name: string; system_identifier: bigint }>;
33
+
34
+ return {
35
+ database_name: result[0].database_name,
36
+ server_id: result[0].system_identifier.toString()
37
+ };
38
+ };
@@ -2,7 +2,7 @@
2
2
 
3
3
  import * as pgwire from '@powersync/service-jpgwire';
4
4
 
5
- import { logger } from '@powersync/lib-services-framework';
5
+ import { logger, ServiceAssertionError } from '@powersync/lib-services-framework';
6
6
 
7
7
  export function escapeIdentifier(identifier: string) {
8
8
  return `"${identifier.replace(/"/g, '""').replace(/\./g, '"."')}"`;
@@ -24,7 +24,7 @@ export function autoParameter(arg: any): pgwire.StatementParam {
24
24
  } else if (typeof arg == 'bigint') {
25
25
  return { type: 'int8', value: arg };
26
26
  } else {
27
- throw new Error(`Unsupported query parameter: ${typeof arg}`);
27
+ throw new ServiceAssertionError(`Unsupported query parameter: ${typeof arg}`);
28
28
  }
29
29
  }
30
30
 
@@ -1 +1,2 @@
1
+ export * from './identifier-utils.js';
1
2
  export * from './pgwire_utils.js';
package/tsconfig.json CHANGED
@@ -8,5 +8,5 @@
8
8
  "sourceMap": true
9
9
  },
10
10
  "include": ["src"],
11
- "references": [{ "path": "../../packages/jpgwire" }, { "path": "../lib-services" }]
11
+ "references": [{ "path": "../lib-services" }, { "path": "../../packages/jpgwire" }]
12
12
  }