@juit/pgproxy-client 1.0.0
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/README.md +136 -0
- package/dist/assert.cjs +34 -0
- package/dist/assert.cjs.map +6 -0
- package/dist/assert.d.ts +2 -0
- package/dist/assert.mjs +9 -0
- package/dist/assert.mjs.map +6 -0
- package/dist/client.cjs +97 -0
- package/dist/client.cjs.map +6 -0
- package/dist/client.d.ts +67 -0
- package/dist/client.mjs +72 -0
- package/dist/client.mjs.map +6 -0
- package/dist/index.cjs +33 -0
- package/dist/index.cjs.map +6 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.mjs +7 -0
- package/dist/index.mjs.map +6 -0
- package/dist/provider.cjs +59 -0
- package/dist/provider.cjs.map +6 -0
- package/dist/provider.d.ts +33 -0
- package/dist/provider.mjs +32 -0
- package/dist/provider.mjs.map +6 -0
- package/dist/result.cjs +59 -0
- package/dist/result.cjs.map +6 -0
- package/dist/result.d.ts +30 -0
- package/dist/result.mjs +34 -0
- package/dist/result.mjs.map +6 -0
- package/dist/websocket.cjs +184 -0
- package/dist/websocket.cjs.map +6 -0
- package/dist/websocket.d.ts +60 -0
- package/dist/websocket.mjs +159 -0
- package/dist/websocket.mjs.map +6 -0
- package/package.json +47 -0
- package/src/assert.ts +4 -0
- package/src/client.ts +172 -0
- package/src/index.ts +5 -0
- package/src/provider.ts +80 -0
- package/src/result.ts +83 -0
- package/src/websocket.ts +269 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { Registry, serialize } from '@juit/pgproxy-types'
|
|
2
|
+
|
|
3
|
+
import { assert } from './assert'
|
|
4
|
+
import { createProvider } from './provider'
|
|
5
|
+
import { PGResult } from './result'
|
|
6
|
+
|
|
7
|
+
import type { PGConnection, PGProvider } from './provider'
|
|
8
|
+
|
|
9
|
+
function serializeParams(params: any[]): (string | null)[] {
|
|
10
|
+
if (params.length == 0) return []
|
|
11
|
+
|
|
12
|
+
const result: (string | null)[] = new Array(params.length)
|
|
13
|
+
for (let i = 0; i < params.length; i ++) {
|
|
14
|
+
result[i] =
|
|
15
|
+
params[i] === undefined ? null :
|
|
16
|
+
params[i] === null ? null :
|
|
17
|
+
serialize(params[i])
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return result
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** An interface for an object that can execute queries on a database */
|
|
24
|
+
export interface PGQueryable {
|
|
25
|
+
/**
|
|
26
|
+
* Execute a query on the database
|
|
27
|
+
*
|
|
28
|
+
* @param text - The SQL query to execute optionally containing placeholders.
|
|
29
|
+
* @param params - Any parameter replacement for `$x` placeholders.
|
|
30
|
+
*/
|
|
31
|
+
query<
|
|
32
|
+
Row extends Record<string, any> = Record<string, any>,
|
|
33
|
+
Tuple extends readonly any[] = readonly any [],
|
|
34
|
+
>(text: string, params?: any[]): Promise<PGResult<Row, Tuple>>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* An interface for an object that can execute queries _and transactions_
|
|
39
|
+
* on a database */
|
|
40
|
+
export interface PGTransactionable extends PGQueryable {
|
|
41
|
+
/** Start a transaction by issuing a `BEGIN` statement */
|
|
42
|
+
begin(): Promise<this>
|
|
43
|
+
/** Commit a transaction by issuing a `COMMIT` statement */
|
|
44
|
+
commit(): Promise<this>
|
|
45
|
+
/** Cancel a transaction by issuing a `ROLLBACK` statement */
|
|
46
|
+
rollback(): Promise<this>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
/** A consumer for a {@link PGTransactionable} connection */
|
|
51
|
+
export type PGConsumer<T> = (connection: PGTransactionable) => T | PromiseLike<T>
|
|
52
|
+
|
|
53
|
+
/** The PostgreSQL client */
|
|
54
|
+
export interface PGClient extends PGQueryable {
|
|
55
|
+
/** The {@link @juit/pgproxy-types#Registry} used to parse results from PostgreSQL */
|
|
56
|
+
readonly registry: Registry
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Execute a _single_ query on the database.
|
|
60
|
+
*
|
|
61
|
+
* Invoking the `query` method on a {@link (PGClient:interface)} does NOT guarantee that
|
|
62
|
+
* the query will be executed on the same connection, therefore things like
|
|
63
|
+
* _transactions_ will be immediately rolled back after the query.
|
|
64
|
+
*
|
|
65
|
+
* @param text - The SQL query to execute optionally containing placeholders.
|
|
66
|
+
* @param params - Any parameter replacement for `$x` placeholders.
|
|
67
|
+
*/
|
|
68
|
+
query<
|
|
69
|
+
Row extends Record<string, any> = Record<string, any>,
|
|
70
|
+
Tuple extends readonly any[] = readonly any [],
|
|
71
|
+
>(text: string, params?: any[]): Promise<PGResult<Row, Tuple>>
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Connect to the database to execute a number of different queries.
|
|
75
|
+
*
|
|
76
|
+
* The `consumer` will be passed a {@link PGTransactionable} instance backed
|
|
77
|
+
* by the _same_ connection to the database, therefore transactions can be
|
|
78
|
+
* safely executed in the context of the consumer function itself.
|
|
79
|
+
*/
|
|
80
|
+
connect<T>(consumer: PGConsumer<T>): Promise<T>
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Destroy any resource and underlying connection associated with this
|
|
84
|
+
* instance's {@link PGProvider}.
|
|
85
|
+
*/
|
|
86
|
+
destroy(): Promise<void>
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A constructor for {@link (PGClient:interface)} instances */
|
|
90
|
+
export interface PGClientConstructor {
|
|
91
|
+
new (url?: string | URL): PGClient
|
|
92
|
+
new (provider: PGProvider<PGConnection>): PGClient
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The PostgreSQL client
|
|
97
|
+
*
|
|
98
|
+
* @constructor
|
|
99
|
+
*/
|
|
100
|
+
export const PGClient: PGClientConstructor = class PGClientImpl implements PGClient {
|
|
101
|
+
readonly registry: Registry = new Registry()
|
|
102
|
+
|
|
103
|
+
private _provider: PGProvider<PGConnection>
|
|
104
|
+
|
|
105
|
+
constructor(url?: string | URL)
|
|
106
|
+
constructor(provider: PGProvider<PGConnection>)
|
|
107
|
+
constructor(urlOrProvider?: string | URL | PGProvider<PGConnection>) {
|
|
108
|
+
urlOrProvider = urlOrProvider || globalThis?.process?.env?.PGURL
|
|
109
|
+
assert(urlOrProvider, 'No URL to connect to (PGURL environment variable missing?)')
|
|
110
|
+
if (typeof urlOrProvider === 'string') urlOrProvider = new URL(urlOrProvider, 'psql:///')
|
|
111
|
+
assert(urlOrProvider, 'Missing URL or provider for client')
|
|
112
|
+
|
|
113
|
+
if (urlOrProvider instanceof URL) {
|
|
114
|
+
if (!(urlOrProvider.username || urlOrProvider.password)) {
|
|
115
|
+
const username = globalThis?.process?.env?.PGUSER || ''
|
|
116
|
+
const password = globalThis?.process?.env?.PGPASSWORD || ''
|
|
117
|
+
urlOrProvider.username = encodeURIComponent(username)
|
|
118
|
+
urlOrProvider.password = encodeURIComponent(password)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
this._provider = urlOrProvider instanceof URL ?
|
|
123
|
+
createProvider(urlOrProvider) :
|
|
124
|
+
urlOrProvider
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async query<
|
|
128
|
+
Row extends Record<string, any> = Record<string, any>,
|
|
129
|
+
Tuple extends readonly any[] = readonly any [],
|
|
130
|
+
>(text: string, params: any[] = []): Promise<PGResult<Row, Tuple>> {
|
|
131
|
+
const result = await this._provider.query(text, serializeParams(params))
|
|
132
|
+
return new PGResult<Row, Tuple>(result, this.registry)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async connect<T>(consumer: PGConsumer<T>): Promise<T> {
|
|
136
|
+
const connection = await this._provider.acquire()
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const registry = this.registry
|
|
140
|
+
|
|
141
|
+
const consumable: PGTransactionable = {
|
|
142
|
+
async query<
|
|
143
|
+
Row extends Record<string, any> = Record<string, any>,
|
|
144
|
+
Tuple extends readonly any[] = readonly any [],
|
|
145
|
+
>(text: string, params: any[] = []): Promise<PGResult<Row, Tuple>> {
|
|
146
|
+
const result = await connection.query(text, serializeParams(params))
|
|
147
|
+
return new PGResult(result, registry)
|
|
148
|
+
},
|
|
149
|
+
async begin(): Promise<PGTransactionable> {
|
|
150
|
+
await this.query('BEGIN')
|
|
151
|
+
return this
|
|
152
|
+
},
|
|
153
|
+
async commit(): Promise<PGTransactionable> {
|
|
154
|
+
await this.query('COMMIT')
|
|
155
|
+
return this
|
|
156
|
+
},
|
|
157
|
+
async rollback(): Promise<PGTransactionable> {
|
|
158
|
+
await this.query('ROLLBACK')
|
|
159
|
+
return this
|
|
160
|
+
},
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return await consumer(consumable)
|
|
164
|
+
} finally {
|
|
165
|
+
await this._provider.release(connection)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async destroy(): Promise<void> {
|
|
170
|
+
return await this._provider.destroy()
|
|
171
|
+
}
|
|
172
|
+
}
|
package/src/index.ts
ADDED
package/src/provider.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { assert } from './assert'
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
/* ========================================================================== *
|
|
5
|
+
* EXPORTED TYPES *
|
|
6
|
+
* ========================================================================== */
|
|
7
|
+
|
|
8
|
+
/** Describes the result of a query from a {@link PGProvider} */
|
|
9
|
+
export interface PGConnectionResult {
|
|
10
|
+
/** The SQL command that generated this result (`SELECT`, `INSERT`, ...) */
|
|
11
|
+
command: string
|
|
12
|
+
/** Number of rows affected by this query (e.g. added rows in `INSERT`) */
|
|
13
|
+
rowCount: number
|
|
14
|
+
/** Fields description with `name` (column name) and `oid` (type) */
|
|
15
|
+
fields: [ name: string, oid: number ][]
|
|
16
|
+
/** Result rows, as an array of unparsed `string` results from `libpq` */
|
|
17
|
+
rows: (string | null)[][]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PGConnection {
|
|
21
|
+
query(text: string, params: (string | null)[]): Promise<PGConnectionResult>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface PGProviderConstructor<Connection extends PGConnection> {
|
|
25
|
+
new (url: URL): PGProvider<Connection>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PGProvider<Connection extends PGConnection> extends PGConnection {
|
|
29
|
+
acquire(): Promise<Connection>
|
|
30
|
+
release(connection: Connection): Promise<void>
|
|
31
|
+
destroy(): Promise<void>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* ========================================================================== *
|
|
35
|
+
* ABSTRACT PROVIDER IMPLEMENTATION *
|
|
36
|
+
* ========================================================================== */
|
|
37
|
+
|
|
38
|
+
export abstract class AbstractPGProvider<Connection extends PGConnection>
|
|
39
|
+
implements PGProvider<Connection> {
|
|
40
|
+
abstract acquire(): Promise<Connection>
|
|
41
|
+
abstract release(connection: PGConnection): Promise<void>
|
|
42
|
+
|
|
43
|
+
async query(text: string, params: string[]): Promise<PGConnectionResult> {
|
|
44
|
+
const connection = await this.acquire()
|
|
45
|
+
try {
|
|
46
|
+
return await connection.query(text, params)
|
|
47
|
+
} finally {
|
|
48
|
+
await this.release(connection)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async destroy(): Promise<void> {
|
|
53
|
+
/* Nothing to do here... */
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* ========================================================================== *
|
|
58
|
+
* PROVIDERS REGISTRATION *
|
|
59
|
+
* ========================================================================== */
|
|
60
|
+
|
|
61
|
+
/** All known providers, mapped by protocol */
|
|
62
|
+
const providers = new Map<string, PGProviderConstructor<PGConnection>>()
|
|
63
|
+
|
|
64
|
+
/** Register a provider, associating it with the specified protocol */
|
|
65
|
+
export function registerProvider(
|
|
66
|
+
protocol: string,
|
|
67
|
+
constructor: PGProviderConstructor<PGConnection>,
|
|
68
|
+
): void {
|
|
69
|
+
protocol = `${protocol}:` // URL always has protocol with _colon_
|
|
70
|
+
assert(! providers.has(protocol), `Connection provider for "${protocol}..." already registered`)
|
|
71
|
+
providers.set(protocol, constructor)
|
|
72
|
+
providers.set(protocol, constructor)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Create a new {@link PGProvider} instance for the specified URL */
|
|
76
|
+
export function createProvider(url: URL): PGProvider<PGConnection> {
|
|
77
|
+
const Provider = providers.get(url.protocol)
|
|
78
|
+
assert(Provider, `No connection provider registered for "${url.protocol}..."`)
|
|
79
|
+
return new Provider(url)
|
|
80
|
+
}
|
package/src/result.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { Registry } from '@juit/pgproxy-types'
|
|
2
|
+
import type { PGConnectionResult } from './provider'
|
|
3
|
+
|
|
4
|
+
/* ========================================================================== *
|
|
5
|
+
* EXPORTED TYPES *
|
|
6
|
+
* ========================================================================== */
|
|
7
|
+
|
|
8
|
+
/** The result of a database query */
|
|
9
|
+
export interface PGResult<
|
|
10
|
+
Row extends Record<string, any> = Record<string, any>,
|
|
11
|
+
Tuple extends readonly any[] = readonly any [],
|
|
12
|
+
> {
|
|
13
|
+
/** The SQL command that generated this result (`SELECT`, `INSERT`, ...) */
|
|
14
|
+
command: string
|
|
15
|
+
/** Result description describing column names and relative OIDs */
|
|
16
|
+
fields: { name: string, oid: number }[]
|
|
17
|
+
/**
|
|
18
|
+
* The number of rows affected by the query.
|
|
19
|
+
*
|
|
20
|
+
* This can be the number of lines returned in `rows` (for `SELECT`
|
|
21
|
+
* statements, for example) or the number of lines _affected_ by the query
|
|
22
|
+
* (the number of records inserted by an `INSERT` query).
|
|
23
|
+
*/
|
|
24
|
+
rowCount: number
|
|
25
|
+
/** The rows returned by the database query, keyed by the column name. */
|
|
26
|
+
rows: Row[]
|
|
27
|
+
/** The tuples returned by the database query, keyed by the column index. */
|
|
28
|
+
tuples: Tuple[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Constructor for {@link (PGResult:interface)} instances */
|
|
32
|
+
export interface PGResultConstructor {
|
|
33
|
+
new <
|
|
34
|
+
Row extends Record<string, any> = Record<string, any>,
|
|
35
|
+
Tuple extends readonly any[] = readonly any [],
|
|
36
|
+
>(result: PGConnectionResult, registry: Registry): PGResult<Row, Tuple>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* ========================================================================== *
|
|
40
|
+
* PGRESULT IMPLEMENTATION *
|
|
41
|
+
* ========================================================================== */
|
|
42
|
+
|
|
43
|
+
/** The result of a database query */
|
|
44
|
+
export const PGResult: PGResultConstructor = class PGResultImpl<
|
|
45
|
+
Row extends Record<string, any> = Record<string, any>,
|
|
46
|
+
Tuple extends readonly any[] = readonly any [],
|
|
47
|
+
> implements PGResult<Row, Tuple> {
|
|
48
|
+
command: string
|
|
49
|
+
fields: { name: string, oid: number }[]
|
|
50
|
+
rowCount: number
|
|
51
|
+
rows: Row[]
|
|
52
|
+
tuples: Tuple[]
|
|
53
|
+
|
|
54
|
+
constructor(result: PGConnectionResult, registry: Registry) {
|
|
55
|
+
this.rowCount = result.rowCount
|
|
56
|
+
this.command = result.command
|
|
57
|
+
this.fields = result.fields.map(([ name, oid ]) => ({ name, oid }))
|
|
58
|
+
|
|
59
|
+
const rowCount = result.rows.length
|
|
60
|
+
const colCount = result.fields.length
|
|
61
|
+
|
|
62
|
+
const mappers = result.fields.map(([ name, oid ]) => ([
|
|
63
|
+
name, registry.getParser(oid),
|
|
64
|
+
] as const))
|
|
65
|
+
|
|
66
|
+
const rows = this.rows = new Array(rowCount)
|
|
67
|
+
const tuples = this.tuples = new Array(rowCount)
|
|
68
|
+
|
|
69
|
+
for (let row = 0; row < rowCount; row ++) {
|
|
70
|
+
const tupleData = tuples[row] = new Array(colCount)
|
|
71
|
+
const rowData = rows[row] = {} as Record<string, any>
|
|
72
|
+
|
|
73
|
+
for (let col = 0; col < colCount; col ++) {
|
|
74
|
+
const [ name, parser ] = mappers[col]!
|
|
75
|
+
const value = result.rows[row]![col]!
|
|
76
|
+
tupleData[col] = rowData[name] =
|
|
77
|
+
value === null ? null :
|
|
78
|
+
value === undefined ? null :
|
|
79
|
+
parser(value)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
package/src/websocket.ts
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { assert } from './assert'
|
|
2
|
+
|
|
3
|
+
import type { Request, Response } from '@juit/pgproxy-server'
|
|
4
|
+
import type { PGConnection, PGConnectionResult, PGProvider } from './provider'
|
|
5
|
+
|
|
6
|
+
/* ========================================================================== *
|
|
7
|
+
* WEBSOCKET TYPES: in order to work with both WHATWG WebSockets and NodeJS's *
|
|
8
|
+
* "ws" package, let's abstract our _minimal_ requirement for implementation *
|
|
9
|
+
* ensuring type compatibility between the two variants. *
|
|
10
|
+
* ========================================================================== */
|
|
11
|
+
|
|
12
|
+
const pgWebSocketReadyState = {
|
|
13
|
+
CONNECTING: 0,
|
|
14
|
+
OPEN: 1,
|
|
15
|
+
CLOSING: 2,
|
|
16
|
+
CLOSED: 3,
|
|
17
|
+
} as const
|
|
18
|
+
|
|
19
|
+
interface PGWebSocketCloseEvent {
|
|
20
|
+
readonly code: number;
|
|
21
|
+
readonly reason: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface PGWebSocketMessageEvent {
|
|
25
|
+
readonly data?: any
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface PGWebSocketErrorEvent {
|
|
29
|
+
readonly error?: any
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface PGWebSocket {
|
|
33
|
+
addEventListener(event: 'close', handler: (event: PGWebSocketCloseEvent) => void): void
|
|
34
|
+
addEventListener(event: 'error', handler: (event: PGWebSocketErrorEvent) => void): void
|
|
35
|
+
addEventListener(event: 'message', handler: (event: PGWebSocketMessageEvent) => void): void
|
|
36
|
+
addEventListener(event: 'open', handler: () => void): void
|
|
37
|
+
removeEventListener(event: 'close', handler: (event: PGWebSocketCloseEvent) => void): void
|
|
38
|
+
removeEventListener(event: 'error', handler: (event: PGWebSocketErrorEvent) => void): void
|
|
39
|
+
removeEventListener(event: 'message', handler: (event: PGWebSocketMessageEvent) => void): void
|
|
40
|
+
removeEventListener(event: 'open', handler: () => void): void
|
|
41
|
+
|
|
42
|
+
readonly readyState: number;
|
|
43
|
+
|
|
44
|
+
send(message: string): void
|
|
45
|
+
close(code?: number, reason?: string): void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* ========================================================================== *
|
|
49
|
+
* INTERNALS *
|
|
50
|
+
* ========================================================================== */
|
|
51
|
+
|
|
52
|
+
/* Return the specified message or the default one */
|
|
53
|
+
function msg(message: string | null | undefined, defaultMessage: string): string {
|
|
54
|
+
return message || defaultMessage
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** A request, simply an unwrapped {@link PGConnectionResult} promise */
|
|
58
|
+
class WebSocketRequest {
|
|
59
|
+
readonly promise: Promise<PGConnectionResult>
|
|
60
|
+
readonly resolve!: (result: PGConnectionResult) => void
|
|
61
|
+
readonly reject!: (reason: any) => void
|
|
62
|
+
|
|
63
|
+
constructor(public id: string) {
|
|
64
|
+
this.promise = new Promise((resolve, reject) => Object.defineProperties(this, {
|
|
65
|
+
resolve: { value: resolve },
|
|
66
|
+
reject: { value: reject },
|
|
67
|
+
}))
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Connection implementation, wrapping a `WebSocket` */
|
|
72
|
+
class WebSocketConnectionImpl implements WebSocketConnection {
|
|
73
|
+
/** Open requests to correlate, keyed by their unique request id */
|
|
74
|
+
private _requests = new Map<string, WebSocketRequest>()
|
|
75
|
+
/** Our error, set also when the websocket is closed */
|
|
76
|
+
private _error?: any
|
|
77
|
+
|
|
78
|
+
constructor(private _socket: PGWebSocket, private _getRequestId: () => string) {
|
|
79
|
+
/* On close, set the error to "WebSocket Closed" if none was set before */
|
|
80
|
+
_socket.addEventListener('close', (event) => {
|
|
81
|
+
/* Keep the first error we received... */
|
|
82
|
+
if (! this._error) {
|
|
83
|
+
const reason = msg(event.reason, 'Unknown Reason')
|
|
84
|
+
const message = `WebSocket Closed (${event.code}): ${reason}`
|
|
85
|
+
this._error = new Error(message)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* Reject all open/pending requests */
|
|
89
|
+
for (const req of this._requests.values()) req.reject(this._error)
|
|
90
|
+
this._requests.clear()
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
/* On errors, make sure that the websocket is closed */
|
|
94
|
+
_socket.addEventListener('error', (event) => {
|
|
95
|
+
if (event.error) this._error = event.error
|
|
96
|
+
else this._error = new Error('Unknown WebSocket Error')
|
|
97
|
+
|
|
98
|
+
/* Reject all open/pending requests */
|
|
99
|
+
for (const req of this._requests.values()) req.reject(this._error)
|
|
100
|
+
this._requests.clear()
|
|
101
|
+
|
|
102
|
+
/* Make sure that the websocket is closed */
|
|
103
|
+
this.close()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
/* On messages, correlate the message with a request and resolve it */
|
|
107
|
+
_socket.addEventListener('message', (event) => {
|
|
108
|
+
try {
|
|
109
|
+
/* Make sure we have a _text_ message (yup, it's JSON) */
|
|
110
|
+
const data = event.data
|
|
111
|
+
assert(typeof data === 'string', 'Data not a "string"')
|
|
112
|
+
|
|
113
|
+
/* Parse the response */
|
|
114
|
+
let payload: Response
|
|
115
|
+
try {
|
|
116
|
+
payload = JSON.parse(data)
|
|
117
|
+
} catch (error) {
|
|
118
|
+
throw new Error('Unable to parse JSON payload')
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* Ensure the payload is a proper object */
|
|
122
|
+
assert(payload && (typeof payload === 'object'), 'JSON payload is not an object')
|
|
123
|
+
|
|
124
|
+
/* Correlate the response ID with a previous request */
|
|
125
|
+
const request = this._requests.get(payload.id)
|
|
126
|
+
assert(request, `Invalid response ID "${payload.id}"`)
|
|
127
|
+
|
|
128
|
+
/* Determine what kind of response we're dealing with */
|
|
129
|
+
if (payload.statusCode === 200) {
|
|
130
|
+
this._requests.delete(payload.id)
|
|
131
|
+
return request.resolve(payload)
|
|
132
|
+
} else if (payload.statusCode === 400) {
|
|
133
|
+
this._requests.delete(payload.id)
|
|
134
|
+
return request.reject(new Error(`${msg(payload.error, 'Unknown error')} (${payload.statusCode})`))
|
|
135
|
+
} else {
|
|
136
|
+
throw new Error(`${msg(payload.error, 'Unknown error')} (${payload.statusCode})`)
|
|
137
|
+
}
|
|
138
|
+
} catch (error: any) {
|
|
139
|
+
_socket.close(1003, msg(error.message, 'Uknown error'))
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
close(): void {
|
|
145
|
+
if (this._socket.readyState === pgWebSocketReadyState.CLOSED) return
|
|
146
|
+
/* coverage ignore if */
|
|
147
|
+
if (this._socket.readyState === pgWebSocketReadyState.CLOSING) return
|
|
148
|
+
this._socket.close(1000, 'Normal termination')
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
query(query: string, params: (string | null)[]): Promise<PGConnectionResult> {
|
|
152
|
+
/* The error is set also when the websocket is closed, soooooo... */
|
|
153
|
+
if (this._error) return Promise.reject(this._error)
|
|
154
|
+
|
|
155
|
+
/* Wrap sending into a promise, both request.promise and send can fail... */
|
|
156
|
+
return new Promise((resolve, reject) => {
|
|
157
|
+
/* Get a unique request ID, and prepare our request */
|
|
158
|
+
const id = this._getRequestId()
|
|
159
|
+
const request = new WebSocketRequest(id)
|
|
160
|
+
this._requests.set(id, request)
|
|
161
|
+
|
|
162
|
+
/* Handle responses from the request first */
|
|
163
|
+
request.promise.then(resolve, reject)
|
|
164
|
+
|
|
165
|
+
/* Send our message to the server after the request promise is handled */
|
|
166
|
+
try {
|
|
167
|
+
this._socket.send(JSON.stringify({ id, query, params } satisfies Request))
|
|
168
|
+
} catch (error) {
|
|
169
|
+
reject(error)
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* ========================================================================== *
|
|
176
|
+
* EXPORTED *
|
|
177
|
+
* ========================================================================== */
|
|
178
|
+
|
|
179
|
+
/** A connection to the database backed by a `WebSocket` */
|
|
180
|
+
export interface WebSocketConnection extends PGConnection {
|
|
181
|
+
/** Close this connection and the underlying `WebSocket` */
|
|
182
|
+
close(): void
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** An abstract provider implementing `connect(...)` via WHATWG WebSockets */
|
|
186
|
+
export abstract class WebSocketProvider implements PGProvider<WebSocketConnection> {
|
|
187
|
+
private readonly _connections = new Set<WebSocketConnection>()
|
|
188
|
+
|
|
189
|
+
abstract query(text: string, params: (string | null)[]): Promise<PGConnectionResult>
|
|
190
|
+
|
|
191
|
+
/** Return a unique request identifier to correlate responses */
|
|
192
|
+
protected abstract _getUniqueRequestId(): string
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Create a new WebSocket.
|
|
196
|
+
*
|
|
197
|
+
* This method can be asynchronous and can return a `Promise`. This is
|
|
198
|
+
* due to the fact that in order to create our authentication token with the
|
|
199
|
+
* Web Cryptography API, we need to _await_ the resolution of our token.
|
|
200
|
+
*
|
|
201
|
+
* This method should call _synchronously_ the {@link WebSocketProvider._connectWebSocket}
|
|
202
|
+
* as soon as the WebSocket instance is created, in order to handle `open`,
|
|
203
|
+
* `close`, or `error` events before the event loop has a chance to resolve
|
|
204
|
+
* the `Promise` asynchronously.
|
|
205
|
+
*/
|
|
206
|
+
protected abstract _getWebSocket(): Promise<PGWebSocket>
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Handle the initial connection of a WebSocket.
|
|
210
|
+
*
|
|
211
|
+
* This method should be called _synchronously_ by {@link WebSocketProvider._getWebSocket} as
|
|
212
|
+
* soon as the WebSocket instance is created.
|
|
213
|
+
*/
|
|
214
|
+
protected _connectWebSocket<S extends PGWebSocket>(socket: S): Promise<S> {
|
|
215
|
+
return new Promise<S>((resolve, reject) => {
|
|
216
|
+
/* The socket might have already connected (or failed connecting) in the
|
|
217
|
+
* time it takes for the event loop to resolve our promise... */
|
|
218
|
+
if (socket.readyState === pgWebSocketReadyState.OPEN) return resolve(socket)
|
|
219
|
+
if (socket.readyState !== pgWebSocketReadyState.CONNECTING) {
|
|
220
|
+
return reject(new Error(`Invalid WebSocket ready state ${socket.readyState}`))
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const onopen = (): void => {
|
|
224
|
+
removeEventListeners()
|
|
225
|
+
resolve(socket)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const onerror = (event: PGWebSocketErrorEvent): void => {
|
|
229
|
+
removeEventListeners()
|
|
230
|
+
if ('error' in event) return reject(event.error)
|
|
231
|
+
reject(new Error('Uknown error opening WebSocket'))
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const onclose = (event: PGWebSocketCloseEvent): void => {
|
|
235
|
+
removeEventListeners()
|
|
236
|
+
reject(new Error(`Connection closed with code ${event.code}: ${event.reason}`))
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const removeEventListeners = (): void => {
|
|
240
|
+
socket.removeEventListener('open', onopen)
|
|
241
|
+
socket.removeEventListener('error', onerror)
|
|
242
|
+
socket.removeEventListener('close', onclose)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
socket.addEventListener('open', onopen)
|
|
246
|
+
socket.addEventListener('error', onerror)
|
|
247
|
+
socket.addEventListener('close', onclose)
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async acquire(): Promise<WebSocketConnection> {
|
|
252
|
+
const socket = await this._getWebSocket()
|
|
253
|
+
|
|
254
|
+
/* Wrap the WebSocket into a _connection_, register and return it */
|
|
255
|
+
const connection = new WebSocketConnectionImpl(socket, () => this._getUniqueRequestId())
|
|
256
|
+
this._connections.add(connection)
|
|
257
|
+
return connection
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async release(connection: WebSocketConnection): Promise<void> {
|
|
261
|
+
this._connections.delete(connection)
|
|
262
|
+
connection.close()
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async destroy(): Promise<void> {
|
|
266
|
+
this._connections.forEach((connection) => connection.close())
|
|
267
|
+
this._connections.clear()
|
|
268
|
+
}
|
|
269
|
+
}
|