@steadybrew/postgresjs 4.3.0 → 4.4.1
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 +18 -2
- package/cf/src/connection.js +59 -19
- package/cf/src/errors.js +8 -0
- package/cf/src/index.js +61 -34
- package/cf/src/subscribe.js +127 -47
- package/cjs/src/connection.js +59 -19
- package/cjs/src/errors.js +8 -0
- package/cjs/src/index.js +61 -34
- package/cjs/src/subscribe.js +127 -47
- package/package.json +1 -1
- package/src/connection.js +59 -19
- package/src/errors.js +8 -0
- package/src/index.js +61 -34
- package/src/subscribe.js +127 -47
- package/types/index.d.ts +9 -9
package/src/subscribe.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
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' }
|
|
6
|
+
, statusInterval = 1000
|
|
2
7
|
|
|
3
8
|
export default function Subscribe(postgres, options) {
|
|
4
9
|
const subscribers = new Map()
|
|
@@ -7,6 +12,9 @@ export default function Subscribe(postgres, options) {
|
|
|
7
12
|
|
|
8
13
|
let connection
|
|
9
14
|
, stream
|
|
15
|
+
, timer
|
|
16
|
+
, inflight = false
|
|
17
|
+
, retries = 0
|
|
10
18
|
, ended = false
|
|
11
19
|
|
|
12
20
|
const sql = subscribe.sql = postgres({
|
|
@@ -21,13 +29,12 @@ export default function Subscribe(postgres, options) {
|
|
|
21
29
|
...options.connection,
|
|
22
30
|
replication: 'database'
|
|
23
31
|
},
|
|
24
|
-
onclose:
|
|
32
|
+
onclose: function() {
|
|
25
33
|
if (ended)
|
|
26
34
|
return
|
|
27
35
|
stream = null
|
|
28
36
|
state.pid = state.secret = undefined
|
|
29
|
-
|
|
30
|
-
subscribers.forEach(event => event.forEach(({ onsubscribe }) => onsubscribe()))
|
|
37
|
+
inflight || timer || !connection || connect().catch(noop)
|
|
31
38
|
},
|
|
32
39
|
no_subscribe: true
|
|
33
40
|
})
|
|
@@ -37,6 +44,9 @@ export default function Subscribe(postgres, options) {
|
|
|
37
44
|
|
|
38
45
|
sql.end = async() => {
|
|
39
46
|
ended = true
|
|
47
|
+
clearTimeout(timer)
|
|
48
|
+
timer = null
|
|
49
|
+
connection && await connection.catch(noop)
|
|
40
50
|
stream && (await new Promise(r => (stream.once('close', r), stream.end())))
|
|
41
51
|
return end()
|
|
42
52
|
}
|
|
@@ -48,34 +58,74 @@ export default function Subscribe(postgres, options) {
|
|
|
48
58
|
|
|
49
59
|
return subscribe
|
|
50
60
|
|
|
51
|
-
async function subscribe(event, fn, onsubscribe = noop, onerror
|
|
61
|
+
async function subscribe(event, fn, onsubscribe = noop, onerror) {
|
|
52
62
|
event = parseEvent(event)
|
|
53
63
|
|
|
54
|
-
|
|
55
|
-
connection = init(sql, slot, options.publications)
|
|
64
|
+
const subscriber = { fn, onsubscribe, onerror }
|
|
56
65
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
66
|
+
return (connection || connect()).then(() => {
|
|
67
|
+
const fns = subscribers.has(event)
|
|
68
|
+
? subscribers.get(event).add(subscriber)
|
|
69
|
+
: subscribers.set(event, new Set([subscriber])).get(event)
|
|
61
70
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
71
|
+
const unsubscribe = () => {
|
|
72
|
+
fns.delete(subscriber)
|
|
73
|
+
fns.size === 0 && subscribers.delete(event)
|
|
74
|
+
subscribers.size === 0 && (clearTimeout(timer), timer = null)
|
|
75
|
+
}
|
|
66
76
|
|
|
67
|
-
|
|
68
|
-
connected(x)
|
|
69
|
-
onsubscribe()
|
|
70
|
-
stream && stream.on('error', onerror)
|
|
77
|
+
callback(onsubscribe)
|
|
71
78
|
return { unsubscribe, state, sql }
|
|
72
79
|
})
|
|
73
80
|
}
|
|
74
81
|
|
|
75
|
-
function
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
82
|
+
function connect() {
|
|
83
|
+
clearTimeout(timer)
|
|
84
|
+
timer = null
|
|
85
|
+
inflight = true
|
|
86
|
+
const resume = subscribers.size > 0
|
|
87
|
+
const current = connection = init(sql, slot, options.publications).then(x => {
|
|
88
|
+
stream = x.stream
|
|
89
|
+
state.pid = x.state.pid
|
|
90
|
+
state.secret = x.state.secret
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
current.then(
|
|
94
|
+
() => {
|
|
95
|
+
inflight = false
|
|
96
|
+
retries = 0
|
|
97
|
+
resume && !ended && subscribers.forEach(event => event.forEach(({ onsubscribe }) => callback(onsubscribe)))
|
|
98
|
+
},
|
|
99
|
+
error => {
|
|
100
|
+
inflight = false
|
|
101
|
+
connection === current && (connection = null)
|
|
102
|
+
if (ended || subscribers.size === 0)
|
|
103
|
+
return
|
|
104
|
+
reportConnection(error)
|
|
105
|
+
timer = setTimeout(() => connect().catch(noop), retryDelay(retries++))
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
return current
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function retryDelay(n) {
|
|
113
|
+
let seconds = backoffSeconds(sql.options.backoff, n)
|
|
114
|
+
seconds = Number.isFinite(seconds) ? seconds : defaultBackoff(n)
|
|
115
|
+
return Math.min(Math.max(seconds * 1000, 100), 2 ** 31 - 1)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function reportConnection(error) {
|
|
119
|
+
console.error('Unexpected error during logical streaming - reconnecting', error) // eslint-disable-line
|
|
120
|
+
subscribers.forEach(fns => fns.forEach(({ onerror }) => callback(onerror, error)))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function report(error, match) {
|
|
124
|
+
let uncaught = false
|
|
125
|
+
subscribers.forEach((fns, event) => match(event) && fns.forEach(({ onerror }) =>
|
|
126
|
+
onerror ? callback(onerror, error) : uncaught = true
|
|
127
|
+
))
|
|
128
|
+
uncaught && callback(() => { throw error })
|
|
79
129
|
}
|
|
80
130
|
|
|
81
131
|
async function init(sql, slot, publications) {
|
|
@@ -99,18 +149,22 @@ export default function Subscribe(postgres, options) {
|
|
|
99
149
|
}
|
|
100
150
|
|
|
101
151
|
stream.on('data', data)
|
|
102
|
-
stream.on('error',
|
|
152
|
+
stream.on('error', reportConnection)
|
|
103
153
|
stream.on('close', sql.close)
|
|
104
154
|
|
|
105
|
-
|
|
155
|
+
// A walsender can miss the wakeup for a commit and sleep until its timeout; a status update wakes it.
|
|
156
|
+
const status = setInterval(() => stream.writable && pong(), statusInterval)
|
|
157
|
+
stream.on('close', () => clearInterval(status))
|
|
106
158
|
|
|
107
|
-
|
|
108
|
-
console.error('Unexpected error during logical streaming - reconnecting', e) // eslint-disable-line
|
|
109
|
-
}
|
|
159
|
+
return { stream, state: xs.state }
|
|
110
160
|
|
|
111
161
|
function data(x) {
|
|
112
162
|
if (x[0] === 0x77) {
|
|
113
|
-
|
|
163
|
+
try {
|
|
164
|
+
parse(x.subarray(25), state, sql.options.parsers, handle, options.transform)
|
|
165
|
+
} catch (error) {
|
|
166
|
+
skip(x.subarray(25), error)
|
|
167
|
+
}
|
|
114
168
|
} else if (x[0] === 0x6b && x[17]) {
|
|
115
169
|
state.lsn = x.subarray(1, 9)
|
|
116
170
|
pong()
|
|
@@ -119,12 +173,26 @@ export default function Subscribe(postgres, options) {
|
|
|
119
173
|
|
|
120
174
|
function handle(a, b) {
|
|
121
175
|
const path = b.relation.schema + '.' + b.relation.table
|
|
176
|
+
, key = b.relation.keys.length && '=' + b.relation.keys.map(x => a[x.name])
|
|
122
177
|
call('*', a, b)
|
|
123
178
|
call('*:' + path, a, b)
|
|
124
|
-
|
|
179
|
+
key && call('*:' + path + key, a, b)
|
|
125
180
|
call(b.command, a, b)
|
|
126
181
|
call(b.command + ':' + path, a, b)
|
|
127
|
-
|
|
182
|
+
key && call(b.command + ':' + path + key, a, b)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function skip(x, error) {
|
|
186
|
+
const command = commands[x[0]]
|
|
187
|
+
, relation = command && state[x.readUInt32BE(1)]
|
|
188
|
+
|
|
189
|
+
if (!relation)
|
|
190
|
+
throw error
|
|
191
|
+
|
|
192
|
+
const path = relation.schema + '.' + relation.table
|
|
193
|
+
, events = ['*', '*:' + path, command, command + ':' + path]
|
|
194
|
+
|
|
195
|
+
report(error, event => events.includes(event.split('=')[0]))
|
|
128
196
|
}
|
|
129
197
|
|
|
130
198
|
function pong() {
|
|
@@ -137,7 +205,7 @@ export default function Subscribe(postgres, options) {
|
|
|
137
205
|
}
|
|
138
206
|
|
|
139
207
|
function call(x, a, b) {
|
|
140
|
-
subscribers.has(x) && subscribers.get(x).forEach(({ fn }) => fn
|
|
208
|
+
subscribers.has(x) && subscribers.get(x).forEach(({ fn }) => callback(fn, a, b, x))
|
|
141
209
|
}
|
|
142
210
|
}
|
|
143
211
|
|
|
@@ -162,19 +230,24 @@ function parse(x, state, parsers, handle, transform) {
|
|
|
162
230
|
let columnIndex = 0
|
|
163
231
|
, column
|
|
164
232
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
233
|
+
try {
|
|
234
|
+
while (i < x.length) {
|
|
235
|
+
column = r.columns[columnIndex++] = {
|
|
236
|
+
key: x[i++],
|
|
237
|
+
name: transform.column.from
|
|
238
|
+
? transform.column.from(x.toString('utf8', i, i = x.indexOf(0, i)))
|
|
239
|
+
: x.toString('utf8', i, i = x.indexOf(0, i)),
|
|
240
|
+
type: x.readUInt32BE(i += 1),
|
|
241
|
+
parser: parsers[x.readUInt32BE(i)],
|
|
242
|
+
atttypmod: x.readUInt32BE(i += 4)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
column.key && r.keys.push(column)
|
|
246
|
+
i += 4
|
|
174
247
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
248
|
+
} catch (error) {
|
|
249
|
+
r.error = error
|
|
250
|
+
r.failed = true
|
|
178
251
|
}
|
|
179
252
|
},
|
|
180
253
|
Y: () => { /* noop */ }, // Type
|
|
@@ -185,7 +258,7 @@ function parse(x, state, parsers, handle, transform) {
|
|
|
185
258
|
},
|
|
186
259
|
I: x => { // Insert
|
|
187
260
|
let i = 1
|
|
188
|
-
const relation = state[x.readUInt32BE(i)]
|
|
261
|
+
const relation = decodable(state[x.readUInt32BE(i)])
|
|
189
262
|
const { row } = tuples(x, relation.columns, i += 7, transform)
|
|
190
263
|
|
|
191
264
|
handle(row, {
|
|
@@ -195,7 +268,7 @@ function parse(x, state, parsers, handle, transform) {
|
|
|
195
268
|
},
|
|
196
269
|
D: x => { // Delete
|
|
197
270
|
let i = 1
|
|
198
|
-
const relation = state[x.readUInt32BE(i)]
|
|
271
|
+
const relation = decodable(state[x.readUInt32BE(i)])
|
|
199
272
|
i += 4
|
|
200
273
|
const key = x[i] === 75
|
|
201
274
|
handle(key || x[i] === 79
|
|
@@ -209,7 +282,7 @@ function parse(x, state, parsers, handle, transform) {
|
|
|
209
282
|
},
|
|
210
283
|
U: x => { // Update
|
|
211
284
|
let i = 1
|
|
212
|
-
const relation = state[x.readUInt32BE(i)]
|
|
285
|
+
const relation = decodable(state[x.readUInt32BE(i)])
|
|
213
286
|
i += 4
|
|
214
287
|
const key = x[i] === 75
|
|
215
288
|
const xs = key || x[i] === 79
|
|
@@ -232,6 +305,13 @@ function parse(x, state, parsers, handle, transform) {
|
|
|
232
305
|
}).reduce(char, {})[x[0]](x)
|
|
233
306
|
}
|
|
234
307
|
|
|
308
|
+
function decodable(relation) {
|
|
309
|
+
if (relation.failed)
|
|
310
|
+
throw relation.error
|
|
311
|
+
|
|
312
|
+
return relation
|
|
313
|
+
}
|
|
314
|
+
|
|
235
315
|
function tuples(x, columns, xi, transform) {
|
|
236
316
|
let type
|
|
237
317
|
, column
|
|
@@ -265,7 +345,7 @@ function tuples(x, columns, xi, transform) {
|
|
|
265
345
|
}
|
|
266
346
|
|
|
267
347
|
function parseEvent(x) {
|
|
268
|
-
const xs = x.match(/^(\*|insert|update|delete)?:?([
|
|
348
|
+
const xs = x.match(/^(\*|insert|update|delete)?:?([^.=]+(?:\.[^=]+)?)?=?(.+)?/i) || []
|
|
269
349
|
|
|
270
350
|
if (!xs)
|
|
271
351
|
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
|
|
|
@@ -745,7 +744,8 @@ declare namespace postgres {
|
|
|
745
744
|
|
|
746
745
|
listen(channel: string, onnotify: (value: string) => void, onlisten?: (() => void) | undefined): ListenRequest;
|
|
747
746
|
|
|
748
|
-
|
|
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>;
|
|
749
749
|
|
|
750
750
|
largeObject(oid?: number | undefined, /** @default 0x00020000 | 0x00040000 */ mode?: number | undefined): Promise<LargeObject>;
|
|
751
751
|
|