@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.
- package/CHANGELOG.md +48 -8
- package/dist/db/connection/AbstractPostgresConnection.d.ts +1 -1
- package/dist/db/connection/AbstractPostgresConnection.js +1 -1
- package/dist/db/connection/AbstractPostgresConnection.js.map +1 -1
- package/dist/db/connection/ConnectionSlot.d.ts +2 -3
- package/dist/db/connection/ConnectionSlot.js +18 -26
- package/dist/db/connection/ConnectionSlot.js.map +1 -1
- package/dist/db/connection/DatabaseClient.d.ts +1 -1
- package/dist/db/connection/DatabaseClient.js +13 -3
- package/dist/db/connection/DatabaseClient.js.map +1 -1
- package/dist/db/connection/WrappedConnection.js +1 -0
- package/dist/db/connection/WrappedConnection.js.map +1 -1
- package/dist/locks/PostgresLockManager.js +2 -1
- package/dist/locks/PostgresLockManager.js.map +1 -1
- package/dist/types/types.d.ts +3 -0
- package/dist/types/types.js +13 -11
- package/dist/types/types.js.map +1 -1
- package/dist/utils/identifier-utils.d.ts +8 -0
- package/dist/utils/identifier-utils.js +24 -0
- package/dist/utils/identifier-utils.js.map +1 -0
- package/dist/utils/pgwire_utils.js +2 -2
- package/dist/utils/pgwire_utils.js.map +1 -1
- package/dist/utils/utils-index.d.ts +1 -0
- package/dist/utils/utils-index.js +1 -0
- package/dist/utils/utils-index.js.map +1 -1
- package/package.json +4 -4
- package/src/db/connection/AbstractPostgresConnection.ts +1 -3
- package/src/db/connection/ConnectionSlot.ts +9 -24
- package/src/db/connection/DatabaseClient.ts +9 -4
- package/src/types/types.ts +24 -12
- package/src/utils/identifier-utils.ts +38 -0
- package/src/utils/pgwire_utils.ts +2 -2
- package/src/utils/utils-index.ts +1 -0
- package/tsconfig.json +1 -1
- 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
|
|
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.
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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),
|
package/src/types/types.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
97
|
+
throw new ServiceError(ErrorCode.PSYNC_S1106, `Postgres connection: hostname required`);
|
|
88
98
|
}
|
|
89
99
|
|
|
90
100
|
if (username == '') {
|
|
91
|
-
throw new
|
|
101
|
+
throw new ServiceError(ErrorCode.PSYNC_S1107, `Postgres connection: username required`);
|
|
92
102
|
}
|
|
93
103
|
|
|
94
104
|
if (password == '') {
|
|
95
|
-
throw new
|
|
105
|
+
throw new ServiceError(ErrorCode.PSYNC_S1108, `Postgres connection: password required`);
|
|
96
106
|
}
|
|
97
107
|
|
|
98
108
|
if (database == '') {
|
|
99
|
-
throw new
|
|
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
|
|
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
|
|
27
|
+
throw new ServiceAssertionError(`Unsupported query parameter: ${typeof arg}`);
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
package/src/utils/utils-index.ts
CHANGED
package/tsconfig.json
CHANGED