@steadybrew/postgresjs 4.2.0 → 4.4.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 +70 -2
- package/cf/src/connection.js +63 -21
- package/cf/src/errors.js +8 -0
- package/cf/src/index.js +100 -33
- package/cf/src/subscribe.js +122 -47
- package/cjs/src/connection.js +63 -21
- package/cjs/src/errors.js +8 -0
- package/cjs/src/index.js +100 -33
- package/cjs/src/subscribe.js +122 -47
- package/package.json +1 -1
- package/src/connection.js +63 -21
- package/src/errors.js +8 -0
- package/src/index.js +100 -33
- package/src/subscribe.js +122 -47
- package/types/index.d.ts +41 -9
package/src/subscribe.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import { callback } from './errors.js'
|
|
2
|
+
import { defaultBackoff, backoffSeconds } from './connection.js'
|
|
3
|
+
|
|
1
4
|
const noop = () => { /* noop */ }
|
|
5
|
+
, commands = { 68: 'delete', 73: 'insert', 85: 'update' }
|
|
2
6
|
|
|
3
7
|
export default function Subscribe(postgres, options) {
|
|
4
8
|
const subscribers = new Map()
|
|
@@ -7,6 +11,9 @@ export default function Subscribe(postgres, options) {
|
|
|
7
11
|
|
|
8
12
|
let connection
|
|
9
13
|
, stream
|
|
14
|
+
, timer
|
|
15
|
+
, inflight = false
|
|
16
|
+
, retries = 0
|
|
10
17
|
, ended = false
|
|
11
18
|
|
|
12
19
|
const sql = subscribe.sql = postgres({
|
|
@@ -21,13 +28,12 @@ export default function Subscribe(postgres, options) {
|
|
|
21
28
|
...options.connection,
|
|
22
29
|
replication: 'database'
|
|
23
30
|
},
|
|
24
|
-
onclose:
|
|
31
|
+
onclose: function() {
|
|
25
32
|
if (ended)
|
|
26
33
|
return
|
|
27
34
|
stream = null
|
|
28
35
|
state.pid = state.secret = undefined
|
|
29
|
-
|
|
30
|
-
subscribers.forEach(event => event.forEach(({ onsubscribe }) => onsubscribe()))
|
|
36
|
+
inflight || timer || !connection || connect().catch(noop)
|
|
31
37
|
},
|
|
32
38
|
no_subscribe: true
|
|
33
39
|
})
|
|
@@ -37,6 +43,9 @@ export default function Subscribe(postgres, options) {
|
|
|
37
43
|
|
|
38
44
|
sql.end = async() => {
|
|
39
45
|
ended = true
|
|
46
|
+
clearTimeout(timer)
|
|
47
|
+
timer = null
|
|
48
|
+
connection && await connection.catch(noop)
|
|
40
49
|
stream && (await new Promise(r => (stream.once('close', r), stream.end())))
|
|
41
50
|
return end()
|
|
42
51
|
}
|
|
@@ -48,34 +57,74 @@ export default function Subscribe(postgres, options) {
|
|
|
48
57
|
|
|
49
58
|
return subscribe
|
|
50
59
|
|
|
51
|
-
async function subscribe(event, fn, onsubscribe = noop, onerror
|
|
60
|
+
async function subscribe(event, fn, onsubscribe = noop, onerror) {
|
|
52
61
|
event = parseEvent(event)
|
|
53
62
|
|
|
54
|
-
|
|
55
|
-
connection = init(sql, slot, options.publications)
|
|
63
|
+
const subscriber = { fn, onsubscribe, onerror }
|
|
56
64
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
65
|
+
return (connection || connect()).then(() => {
|
|
66
|
+
const fns = subscribers.has(event)
|
|
67
|
+
? subscribers.get(event).add(subscriber)
|
|
68
|
+
: subscribers.set(event, new Set([subscriber])).get(event)
|
|
61
69
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
const unsubscribe = () => {
|
|
71
|
+
fns.delete(subscriber)
|
|
72
|
+
fns.size === 0 && subscribers.delete(event)
|
|
73
|
+
subscribers.size === 0 && (clearTimeout(timer), timer = null)
|
|
74
|
+
}
|
|
66
75
|
|
|
67
|
-
|
|
68
|
-
connected(x)
|
|
69
|
-
onsubscribe()
|
|
70
|
-
stream && stream.on('error', onerror)
|
|
76
|
+
callback(onsubscribe)
|
|
71
77
|
return { unsubscribe, state, sql }
|
|
72
78
|
})
|
|
73
79
|
}
|
|
74
80
|
|
|
75
|
-
function
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
81
|
+
function connect() {
|
|
82
|
+
clearTimeout(timer)
|
|
83
|
+
timer = null
|
|
84
|
+
inflight = true
|
|
85
|
+
const resume = subscribers.size > 0
|
|
86
|
+
const current = connection = init(sql, slot, options.publications).then(x => {
|
|
87
|
+
stream = x.stream
|
|
88
|
+
state.pid = x.state.pid
|
|
89
|
+
state.secret = x.state.secret
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
current.then(
|
|
93
|
+
() => {
|
|
94
|
+
inflight = false
|
|
95
|
+
retries = 0
|
|
96
|
+
resume && !ended && subscribers.forEach(event => event.forEach(({ onsubscribe }) => callback(onsubscribe)))
|
|
97
|
+
},
|
|
98
|
+
error => {
|
|
99
|
+
inflight = false
|
|
100
|
+
connection === current && (connection = null)
|
|
101
|
+
if (ended || subscribers.size === 0)
|
|
102
|
+
return
|
|
103
|
+
reportConnection(error)
|
|
104
|
+
timer = setTimeout(() => connect().catch(noop), retryDelay(retries++))
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return current
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function retryDelay(n) {
|
|
112
|
+
let seconds = backoffSeconds(sql.options.backoff, n)
|
|
113
|
+
seconds = Number.isFinite(seconds) ? seconds : defaultBackoff(n)
|
|
114
|
+
return Math.min(Math.max(seconds * 1000, 100), 2 ** 31 - 1)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function reportConnection(error) {
|
|
118
|
+
console.error('Unexpected error during logical streaming - reconnecting', error) // eslint-disable-line
|
|
119
|
+
subscribers.forEach(fns => fns.forEach(({ onerror }) => callback(onerror, error)))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function report(error, match) {
|
|
123
|
+
let uncaught = false
|
|
124
|
+
subscribers.forEach((fns, event) => match(event) && fns.forEach(({ onerror }) =>
|
|
125
|
+
onerror ? callback(onerror, error) : uncaught = true
|
|
126
|
+
))
|
|
127
|
+
uncaught && callback(() => { throw error })
|
|
79
128
|
}
|
|
80
129
|
|
|
81
130
|
async function init(sql, slot, publications) {
|
|
@@ -99,18 +148,18 @@ export default function Subscribe(postgres, options) {
|
|
|
99
148
|
}
|
|
100
149
|
|
|
101
150
|
stream.on('data', data)
|
|
102
|
-
stream.on('error',
|
|
151
|
+
stream.on('error', reportConnection)
|
|
103
152
|
stream.on('close', sql.close)
|
|
104
153
|
|
|
105
154
|
return { stream, state: xs.state }
|
|
106
155
|
|
|
107
|
-
function error(e) {
|
|
108
|
-
console.error('Unexpected error during logical streaming - reconnecting', e) // eslint-disable-line
|
|
109
|
-
}
|
|
110
|
-
|
|
111
156
|
function data(x) {
|
|
112
157
|
if (x[0] === 0x77) {
|
|
113
|
-
|
|
158
|
+
try {
|
|
159
|
+
parse(x.subarray(25), state, sql.options.parsers, handle, options.transform)
|
|
160
|
+
} catch (error) {
|
|
161
|
+
skip(x.subarray(25), error)
|
|
162
|
+
}
|
|
114
163
|
} else if (x[0] === 0x6b && x[17]) {
|
|
115
164
|
state.lsn = x.subarray(1, 9)
|
|
116
165
|
pong()
|
|
@@ -119,12 +168,26 @@ export default function Subscribe(postgres, options) {
|
|
|
119
168
|
|
|
120
169
|
function handle(a, b) {
|
|
121
170
|
const path = b.relation.schema + '.' + b.relation.table
|
|
171
|
+
, key = b.relation.keys.length && '=' + b.relation.keys.map(x => a[x.name])
|
|
122
172
|
call('*', a, b)
|
|
123
173
|
call('*:' + path, a, b)
|
|
124
|
-
|
|
174
|
+
key && call('*:' + path + key, a, b)
|
|
125
175
|
call(b.command, a, b)
|
|
126
176
|
call(b.command + ':' + path, a, b)
|
|
127
|
-
|
|
177
|
+
key && call(b.command + ':' + path + key, a, b)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function skip(x, error) {
|
|
181
|
+
const command = commands[x[0]]
|
|
182
|
+
, relation = command && state[x.readUInt32BE(1)]
|
|
183
|
+
|
|
184
|
+
if (!relation)
|
|
185
|
+
throw error
|
|
186
|
+
|
|
187
|
+
const path = relation.schema + '.' + relation.table
|
|
188
|
+
, events = ['*', '*:' + path, command, command + ':' + path]
|
|
189
|
+
|
|
190
|
+
report(error, event => events.includes(event.split('=')[0]))
|
|
128
191
|
}
|
|
129
192
|
|
|
130
193
|
function pong() {
|
|
@@ -137,7 +200,7 @@ export default function Subscribe(postgres, options) {
|
|
|
137
200
|
}
|
|
138
201
|
|
|
139
202
|
function call(x, a, b) {
|
|
140
|
-
subscribers.has(x) && subscribers.get(x).forEach(({ fn }) => fn
|
|
203
|
+
subscribers.has(x) && subscribers.get(x).forEach(({ fn }) => callback(fn, a, b, x))
|
|
141
204
|
}
|
|
142
205
|
}
|
|
143
206
|
|
|
@@ -162,19 +225,24 @@ function parse(x, state, parsers, handle, transform) {
|
|
|
162
225
|
let columnIndex = 0
|
|
163
226
|
, column
|
|
164
227
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
228
|
+
try {
|
|
229
|
+
while (i < x.length) {
|
|
230
|
+
column = r.columns[columnIndex++] = {
|
|
231
|
+
key: x[i++],
|
|
232
|
+
name: transform.column.from
|
|
233
|
+
? transform.column.from(x.toString('utf8', i, i = x.indexOf(0, i)))
|
|
234
|
+
: x.toString('utf8', i, i = x.indexOf(0, i)),
|
|
235
|
+
type: x.readUInt32BE(i += 1),
|
|
236
|
+
parser: parsers[x.readUInt32BE(i)],
|
|
237
|
+
atttypmod: x.readUInt32BE(i += 4)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
column.key && r.keys.push(column)
|
|
241
|
+
i += 4
|
|
174
242
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
243
|
+
} catch (error) {
|
|
244
|
+
r.error = error
|
|
245
|
+
r.failed = true
|
|
178
246
|
}
|
|
179
247
|
},
|
|
180
248
|
Y: () => { /* noop */ }, // Type
|
|
@@ -185,7 +253,7 @@ function parse(x, state, parsers, handle, transform) {
|
|
|
185
253
|
},
|
|
186
254
|
I: x => { // Insert
|
|
187
255
|
let i = 1
|
|
188
|
-
const relation = state[x.readUInt32BE(i)]
|
|
256
|
+
const relation = decodable(state[x.readUInt32BE(i)])
|
|
189
257
|
const { row } = tuples(x, relation.columns, i += 7, transform)
|
|
190
258
|
|
|
191
259
|
handle(row, {
|
|
@@ -195,7 +263,7 @@ function parse(x, state, parsers, handle, transform) {
|
|
|
195
263
|
},
|
|
196
264
|
D: x => { // Delete
|
|
197
265
|
let i = 1
|
|
198
|
-
const relation = state[x.readUInt32BE(i)]
|
|
266
|
+
const relation = decodable(state[x.readUInt32BE(i)])
|
|
199
267
|
i += 4
|
|
200
268
|
const key = x[i] === 75
|
|
201
269
|
handle(key || x[i] === 79
|
|
@@ -209,7 +277,7 @@ function parse(x, state, parsers, handle, transform) {
|
|
|
209
277
|
},
|
|
210
278
|
U: x => { // Update
|
|
211
279
|
let i = 1
|
|
212
|
-
const relation = state[x.readUInt32BE(i)]
|
|
280
|
+
const relation = decodable(state[x.readUInt32BE(i)])
|
|
213
281
|
i += 4
|
|
214
282
|
const key = x[i] === 75
|
|
215
283
|
const xs = key || x[i] === 79
|
|
@@ -232,6 +300,13 @@ function parse(x, state, parsers, handle, transform) {
|
|
|
232
300
|
}).reduce(char, {})[x[0]](x)
|
|
233
301
|
}
|
|
234
302
|
|
|
303
|
+
function decodable(relation) {
|
|
304
|
+
if (relation.failed)
|
|
305
|
+
throw relation.error
|
|
306
|
+
|
|
307
|
+
return relation
|
|
308
|
+
}
|
|
309
|
+
|
|
235
310
|
function tuples(x, columns, xi, transform) {
|
|
236
311
|
let type
|
|
237
312
|
, column
|
|
@@ -265,7 +340,7 @@ function tuples(x, columns, xi, transform) {
|
|
|
265
340
|
}
|
|
266
341
|
|
|
267
342
|
function parseEvent(x) {
|
|
268
|
-
const xs = x.match(/^(\*|insert|update|delete)?:?([
|
|
343
|
+
const xs = x.match(/^(\*|insert|update|delete)?:?([^.=]+(?:\.[^=]+)?)?=?(.+)?/i) || []
|
|
269
344
|
|
|
270
345
|
if (!xs)
|
|
271
346
|
throw new Error('Malformed subscribe pattern: ' + x)
|
package/types/index.d.ts
CHANGED
|
@@ -67,14 +67,11 @@ interface BaseOptions<T extends Record<string, postgres.PostgresType>> {
|
|
|
67
67
|
* @default true
|
|
68
68
|
*/
|
|
69
69
|
prepare: boolean;
|
|
70
|
-
/**
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
*/
|
|
74
|
-
onnotice: (notice: postgres.Notice) => void;
|
|
75
|
-
/** (key; value) when a server param change */
|
|
70
|
+
/** Called when a notice is received, console.log by default and false to ignore them; if it throws, the error is rethrown as an uncaught exception */
|
|
71
|
+
onnotice: ((notice: postgres.Notice) => void) | false;
|
|
72
|
+
/** (key; value) when a server param change; if it throws, the error is rethrown as an uncaught exception */
|
|
76
73
|
onparameter: (key: string, value: any) => void;
|
|
77
|
-
/** Is called with (connection; query; parameters) */
|
|
74
|
+
/** Is called with (connection; query; parameters); if it throws, the error is rethrown as an uncaught exception and the query still runs */
|
|
78
75
|
debug: boolean | ((connection: number, query: string, parameters: any[], paramTypes: any[]) => void);
|
|
79
76
|
/** Transform hooks */
|
|
80
77
|
transform: {
|
|
@@ -118,6 +115,7 @@ interface BaseOptions<T extends Record<string, postgres.PostgresType>> {
|
|
|
118
115
|
* @default 'alltables'
|
|
119
116
|
*/
|
|
120
117
|
publications: string
|
|
118
|
+
/** Called with the connection id each time a connection closes; if it throws, the error is rethrown as an uncaught exception */
|
|
121
119
|
onclose: (connId: number) => void;
|
|
122
120
|
/** Called with the connection id each time a connection returns to the pool idle and its idle timer starts */
|
|
123
121
|
onidle: (connId: number) => void;
|
|
@@ -463,7 +461,8 @@ declare namespace postgres {
|
|
|
463
461
|
| 'COPY_IN_PROGRESS'
|
|
464
462
|
| 'INVALID_TRANSACTION_NAME'
|
|
465
463
|
| 'TARGET_SESSION_ATTRS'
|
|
466
|
-
| 'UNSAFE_TRANSACTION'
|
|
464
|
+
| 'UNSAFE_TRANSACTION'
|
|
465
|
+
| 'TRANSACTION_ROLLED_BACK';
|
|
467
466
|
message: string;
|
|
468
467
|
}
|
|
469
468
|
|
|
@@ -704,6 +703,35 @@ declare namespace postgres {
|
|
|
704
703
|
json(value: JSONValue): Parameter;
|
|
705
704
|
}
|
|
706
705
|
|
|
706
|
+
interface PoolStats {
|
|
707
|
+
/** The configured maximum number of pool connections */
|
|
708
|
+
max: number;
|
|
709
|
+
/** Pool connections that exist: connecting, idle or in use */
|
|
710
|
+
totalCount: number;
|
|
711
|
+
/** Pool connections with a live session that are not in use */
|
|
712
|
+
idleCount: number;
|
|
713
|
+
/** Issued queries and reserve or begin calls that are not running yet, whether queued in the pool, held by an opening connection, pipelined or queued on a reserved connection */
|
|
714
|
+
waitingCount: number;
|
|
715
|
+
/** Pool connections in use: busyCount plus reservedCount */
|
|
716
|
+
activeCount: number;
|
|
717
|
+
/** Connections a query can get without waiting behind other work: idleCount plus the slots the pool can still open */
|
|
718
|
+
availableCount: number;
|
|
719
|
+
/** Pool connections running queries, including those at the pipeline limit */
|
|
720
|
+
busyCount: number;
|
|
721
|
+
/** Pool connections held by reserve() or begin() */
|
|
722
|
+
reservedCount: number;
|
|
723
|
+
/** Pool connections opening a session */
|
|
724
|
+
connectingCount: number;
|
|
725
|
+
/** Pool connections draining or closing, not included in totalCount */
|
|
726
|
+
closingCount: number;
|
|
727
|
+
/** Queries the server is running right now, at most one per connection, startup queries and COPY included */
|
|
728
|
+
executingCount: number;
|
|
729
|
+
/** Connections held by the internal listen pool, outside max */
|
|
730
|
+
listenCount: number;
|
|
731
|
+
/** Connections held by the internal subscribe pool, outside max */
|
|
732
|
+
subscribeCount: number;
|
|
733
|
+
}
|
|
734
|
+
|
|
707
735
|
interface Sql<TTypes extends Record<string, unknown> = {}> extends ISql<TTypes> {
|
|
708
736
|
CLOSE: {};
|
|
709
737
|
END: this['CLOSE'];
|
|
@@ -716,7 +744,8 @@ declare namespace postgres {
|
|
|
716
744
|
|
|
717
745
|
listen(channel: string, onnotify: (value: string) => void, onlisten?: (() => void) | undefined): ListenRequest;
|
|
718
746
|
|
|
719
|
-
|
|
747
|
+
/** `onerror` receives replication connection errors and errors thrown while a change is decoded; such a change is skipped. */
|
|
748
|
+
subscribe(event: string, cb: (row: Row | null, info: ReplicationEvent) => void, onsubscribe?: (() => void), onerror?: ((error: Error) => void)): Promise<SubscriptionHandle>;
|
|
720
749
|
|
|
721
750
|
largeObject(oid?: number | undefined, /** @default 0x00020000 | 0x00040000 */ mode?: number | undefined): Promise<LargeObject>;
|
|
722
751
|
|
|
@@ -724,6 +753,9 @@ declare namespace postgres {
|
|
|
724
753
|
begin<T>(options: string, cb: (sql: TransactionSql<TTypes>) => T | Promise<T>): Promise<UnwrapPromiseArray<T>>;
|
|
725
754
|
|
|
726
755
|
reserve(): Promise<ReservedSql<TTypes>>
|
|
756
|
+
|
|
757
|
+
/** Returns a snapshot of the pool's connection and query counts */
|
|
758
|
+
stats(): PoolStats;
|
|
727
759
|
}
|
|
728
760
|
|
|
729
761
|
interface TransactionSql<TTypes extends Record<string, unknown> = {}> extends ISql<TTypes> {
|