@steadybrew/postgresjs 4.3.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/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: async function() {
31
+ onclose: function() {
25
32
  if (ended)
26
33
  return
27
34
  stream = null
28
35
  state.pid = state.secret = undefined
29
- connected(await init(sql, slot, options.publications))
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 = noop) {
60
+ async function subscribe(event, fn, onsubscribe = noop, onerror) {
52
61
  event = parseEvent(event)
53
62
 
54
- if (!connection)
55
- connection = init(sql, slot, options.publications)
63
+ const subscriber = { fn, onsubscribe, onerror }
56
64
 
57
- const subscriber = { fn, onsubscribe }
58
- const fns = subscribers.has(event)
59
- ? subscribers.get(event).add(subscriber)
60
- : subscribers.set(event, new Set([subscriber])).get(event)
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
- const unsubscribe = () => {
63
- fns.delete(subscriber)
64
- fns.size === 0 && subscribers.delete(event)
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
- return connection.then(x => {
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 connected(x) {
76
- stream = x.stream
77
- state.pid = x.state.pid
78
- state.secret = x.state.secret
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', 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
- parse(x.subarray(25), state, sql.options.parsers, handle, options.transform)
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
- b.relation.keys.length && call('*:' + path + '=' + b.relation.keys.map(x => a[x.name]), a, b)
174
+ key && call('*:' + path + key, a, b)
125
175
  call(b.command, a, b)
126
176
  call(b.command + ':' + path, a, b)
127
- b.relation.keys.length && call(b.command + ':' + path + '=' + b.relation.keys.map(x => a[x.name]), a, b)
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(a, b, x))
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
- while (i < x.length) {
166
- column = r.columns[columnIndex++] = {
167
- key: x[i++],
168
- name: transform.column.from
169
- ? transform.column.from(x.toString('utf8', i, i = x.indexOf(0, i)))
170
- : x.toString('utf8', i, i = x.indexOf(0, i)),
171
- type: x.readUInt32BE(i += 1),
172
- parser: parsers[x.readUInt32BE(i)],
173
- atttypmod: x.readUInt32BE(i += 4)
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
- column.key && r.keys.push(column)
177
- i += 4
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)?:?([^.]+?\.?[^=]+)?=?(.+)?/i) || []
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
- * Called when a notice is received
72
- * @default console.log
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
- subscribe(event: string, cb: (row: Row | null, info: ReplicationEvent) => void, onsubscribe?: (() => void), onerror?: (() => any)): Promise<SubscriptionHandle>;
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