@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.
@@ -1,5 +1,9 @@
1
1
  import { Buffer } from 'node:buffer'
2
+ import { callback } from './errors.js'
3
+ import { defaultBackoff, backoffSeconds } from './connection.js'
4
+
2
5
  const noop = () => { /* noop */ }
6
+ , commands = { 68: 'delete', 73: 'insert', 85: 'update' }
3
7
 
4
8
  export default function Subscribe(postgres, options) {
5
9
  const subscribers = new Map()
@@ -8,6 +12,9 @@ export default function Subscribe(postgres, options) {
8
12
 
9
13
  let connection
10
14
  , stream
15
+ , timer
16
+ , inflight = false
17
+ , retries = 0
11
18
  , ended = false
12
19
 
13
20
  const sql = subscribe.sql = postgres({
@@ -22,13 +29,12 @@ export default function Subscribe(postgres, options) {
22
29
  ...options.connection,
23
30
  replication: 'database'
24
31
  },
25
- onclose: async function() {
32
+ onclose: function() {
26
33
  if (ended)
27
34
  return
28
35
  stream = null
29
36
  state.pid = state.secret = undefined
30
- connected(await init(sql, slot, options.publications))
31
- subscribers.forEach(event => event.forEach(({ onsubscribe }) => onsubscribe()))
37
+ inflight || timer || !connection || connect().catch(noop)
32
38
  },
33
39
  no_subscribe: true
34
40
  })
@@ -38,6 +44,9 @@ export default function Subscribe(postgres, options) {
38
44
 
39
45
  sql.end = async() => {
40
46
  ended = true
47
+ clearTimeout(timer)
48
+ timer = null
49
+ connection && await connection.catch(noop)
41
50
  stream && (await new Promise(r => (stream.once('close', r), stream.end())))
42
51
  return end()
43
52
  }
@@ -49,34 +58,74 @@ export default function Subscribe(postgres, options) {
49
58
 
50
59
  return subscribe
51
60
 
52
- async function subscribe(event, fn, onsubscribe = noop, onerror = noop) {
61
+ async function subscribe(event, fn, onsubscribe = noop, onerror) {
53
62
  event = parseEvent(event)
54
63
 
55
- if (!connection)
56
- connection = init(sql, slot, options.publications)
64
+ const subscriber = { fn, onsubscribe, onerror }
57
65
 
58
- const subscriber = { fn, onsubscribe }
59
- const fns = subscribers.has(event)
60
- ? subscribers.get(event).add(subscriber)
61
- : subscribers.set(event, new Set([subscriber])).get(event)
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)
62
70
 
63
- const unsubscribe = () => {
64
- fns.delete(subscriber)
65
- fns.size === 0 && subscribers.delete(event)
66
- }
71
+ const unsubscribe = () => {
72
+ fns.delete(subscriber)
73
+ fns.size === 0 && subscribers.delete(event)
74
+ subscribers.size === 0 && (clearTimeout(timer), timer = null)
75
+ }
67
76
 
68
- return connection.then(x => {
69
- connected(x)
70
- onsubscribe()
71
- stream && stream.on('error', onerror)
77
+ callback(onsubscribe)
72
78
  return { unsubscribe, state, sql }
73
79
  })
74
80
  }
75
81
 
76
- function connected(x) {
77
- stream = x.stream
78
- state.pid = x.state.pid
79
- state.secret = x.state.secret
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 })
80
129
  }
81
130
 
82
131
  async function init(sql, slot, publications) {
@@ -100,18 +149,18 @@ export default function Subscribe(postgres, options) {
100
149
  }
101
150
 
102
151
  stream.on('data', data)
103
- stream.on('error', error)
152
+ stream.on('error', reportConnection)
104
153
  stream.on('close', sql.close)
105
154
 
106
155
  return { stream, state: xs.state }
107
156
 
108
- function error(e) {
109
- console.error('Unexpected error during logical streaming - reconnecting', e) // eslint-disable-line
110
- }
111
-
112
157
  function data(x) {
113
158
  if (x[0] === 0x77) {
114
- parse(x.subarray(25), state, sql.options.parsers, handle, options.transform)
159
+ try {
160
+ parse(x.subarray(25), state, sql.options.parsers, handle, options.transform)
161
+ } catch (error) {
162
+ skip(x.subarray(25), error)
163
+ }
115
164
  } else if (x[0] === 0x6b && x[17]) {
116
165
  state.lsn = x.subarray(1, 9)
117
166
  pong()
@@ -120,12 +169,26 @@ export default function Subscribe(postgres, options) {
120
169
 
121
170
  function handle(a, b) {
122
171
  const path = b.relation.schema + '.' + b.relation.table
172
+ , key = b.relation.keys.length && '=' + b.relation.keys.map(x => a[x.name])
123
173
  call('*', a, b)
124
174
  call('*:' + path, a, b)
125
- b.relation.keys.length && call('*:' + path + '=' + b.relation.keys.map(x => a[x.name]), a, b)
175
+ key && call('*:' + path + key, a, b)
126
176
  call(b.command, a, b)
127
177
  call(b.command + ':' + path, a, b)
128
- b.relation.keys.length && call(b.command + ':' + path + '=' + b.relation.keys.map(x => a[x.name]), a, b)
178
+ key && call(b.command + ':' + path + key, a, b)
179
+ }
180
+
181
+ function skip(x, error) {
182
+ const command = commands[x[0]]
183
+ , relation = command && state[x.readUInt32BE(1)]
184
+
185
+ if (!relation)
186
+ throw error
187
+
188
+ const path = relation.schema + '.' + relation.table
189
+ , events = ['*', '*:' + path, command, command + ':' + path]
190
+
191
+ report(error, event => events.includes(event.split('=')[0]))
129
192
  }
130
193
 
131
194
  function pong() {
@@ -138,7 +201,7 @@ export default function Subscribe(postgres, options) {
138
201
  }
139
202
 
140
203
  function call(x, a, b) {
141
- subscribers.has(x) && subscribers.get(x).forEach(({ fn }) => fn(a, b, x))
204
+ subscribers.has(x) && subscribers.get(x).forEach(({ fn }) => callback(fn, a, b, x))
142
205
  }
143
206
  }
144
207
 
@@ -163,19 +226,24 @@ function parse(x, state, parsers, handle, transform) {
163
226
  let columnIndex = 0
164
227
  , column
165
228
 
166
- while (i < x.length) {
167
- column = r.columns[columnIndex++] = {
168
- key: x[i++],
169
- name: transform.column.from
170
- ? transform.column.from(x.toString('utf8', i, i = x.indexOf(0, i)))
171
- : x.toString('utf8', i, i = x.indexOf(0, i)),
172
- type: x.readUInt32BE(i += 1),
173
- parser: parsers[x.readUInt32BE(i)],
174
- atttypmod: x.readUInt32BE(i += 4)
229
+ try {
230
+ while (i < x.length) {
231
+ column = r.columns[columnIndex++] = {
232
+ key: x[i++],
233
+ name: transform.column.from
234
+ ? transform.column.from(x.toString('utf8', i, i = x.indexOf(0, i)))
235
+ : x.toString('utf8', i, i = x.indexOf(0, i)),
236
+ type: x.readUInt32BE(i += 1),
237
+ parser: parsers[x.readUInt32BE(i)],
238
+ atttypmod: x.readUInt32BE(i += 4)
239
+ }
240
+
241
+ column.key && r.keys.push(column)
242
+ i += 4
175
243
  }
176
-
177
- column.key && r.keys.push(column)
178
- i += 4
244
+ } catch (error) {
245
+ r.error = error
246
+ r.failed = true
179
247
  }
180
248
  },
181
249
  Y: () => { /* noop */ }, // Type
@@ -186,7 +254,7 @@ function parse(x, state, parsers, handle, transform) {
186
254
  },
187
255
  I: x => { // Insert
188
256
  let i = 1
189
- const relation = state[x.readUInt32BE(i)]
257
+ const relation = decodable(state[x.readUInt32BE(i)])
190
258
  const { row } = tuples(x, relation.columns, i += 7, transform)
191
259
 
192
260
  handle(row, {
@@ -196,7 +264,7 @@ function parse(x, state, parsers, handle, transform) {
196
264
  },
197
265
  D: x => { // Delete
198
266
  let i = 1
199
- const relation = state[x.readUInt32BE(i)]
267
+ const relation = decodable(state[x.readUInt32BE(i)])
200
268
  i += 4
201
269
  const key = x[i] === 75
202
270
  handle(key || x[i] === 79
@@ -210,7 +278,7 @@ function parse(x, state, parsers, handle, transform) {
210
278
  },
211
279
  U: x => { // Update
212
280
  let i = 1
213
- const relation = state[x.readUInt32BE(i)]
281
+ const relation = decodable(state[x.readUInt32BE(i)])
214
282
  i += 4
215
283
  const key = x[i] === 75
216
284
  const xs = key || x[i] === 79
@@ -233,6 +301,13 @@ function parse(x, state, parsers, handle, transform) {
233
301
  }).reduce(char, {})[x[0]](x)
234
302
  }
235
303
 
304
+ function decodable(relation) {
305
+ if (relation.failed)
306
+ throw relation.error
307
+
308
+ return relation
309
+ }
310
+
236
311
  function tuples(x, columns, xi, transform) {
237
312
  let type
238
313
  , column
@@ -266,7 +341,7 @@ function tuples(x, columns, xi, transform) {
266
341
  }
267
342
 
268
343
  function parseEvent(x) {
269
- const xs = x.match(/^(\*|insert|update|delete)?:?([^.]+?\.?[^=]+)?=?(.+)?/i) || []
344
+ const xs = x.match(/^(\*|insert|update|delete)?:?([^.=]+(?:\.[^=]+)?)?=?(.+)?/i) || []
270
345
 
271
346
  if (!xs)
272
347
  throw new Error('Malformed subscribe pattern: ' + x)
@@ -5,7 +5,7 @@ const Stream = require('stream')
5
5
  const { performance } = require('perf_hooks')
6
6
 
7
7
  const { stringify, handleValue, addArrayType } = require('./types.js')
8
- const { Errors } = require('./errors.js')
8
+ const { Errors, callback } = require('./errors.js')
9
9
  const Result = require('./result.js')
10
10
  const Queue = require('./queue.js')
11
11
  const clampedTimeout = require('./timeout.js')
@@ -13,7 +13,7 @@ const { SSLRequest, tlsConfig, cancelRequest } = require('./transport.js')
13
13
  const { Query, CLOSE } = require('./query.js')
14
14
  const b = require('./bytes.js')
15
15
 
16
- module.exports = Connection
16
+ module.exports = { Connection }
17
17
 
18
18
  let uid = 1
19
19
 
@@ -26,6 +26,8 @@ const Sync = b().S().end()
26
26
  const Phase = { Closed: 0, Backoff: 1, Connecting: 2, Initializing: 3, Ready: 4, Draining: 5, Closing: 6 }
27
27
  const phaseNames = Object.keys(Phase)
28
28
 
29
+ const endsAbortedState = /^\s*(rollback|abort)\b/i
30
+
29
31
  const retryRoutines = new Set([
30
32
  'FetchPreparedStatement',
31
33
  'RevalidateCachedQuery',
@@ -110,6 +112,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
110
112
  , chunk = null
111
113
  , nonce = null
112
114
  , query = null
115
+ , barrier = null
113
116
  , final = null
114
117
 
115
118
  const check = globalThis[Symbol.for('postgres.js:check')]
@@ -224,6 +227,8 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
224
227
  owner: null,
225
228
  get active() { return query ? 1 : 0 },
226
229
  get pipelined() { return sent.length },
230
+ get accepting() { return accepting() },
231
+ get copying() { return stream !== null },
227
232
  get acquiring() { return acquisition && !acquisition.owner.cancelled ? 1 : 0 },
228
233
  count: 0,
229
234
  id
@@ -268,8 +273,12 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
268
273
  return !query && sent.length === 0
269
274
  }
270
275
 
276
+ function accepting() {
277
+ return !query || (!barrier && sent.length < max_pipeline)
278
+ }
279
+
271
280
  function backoffMs() {
272
- return (typeof backoff === 'function' ? backoff(options.shared.retries) : backoff) * 1000
281
+ return backoffSeconds(backoff, options.shared.retries) * 1000
273
282
  }
274
283
 
275
284
  function standbyPass() {
@@ -446,7 +455,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
446
455
  queryError(pending, err)
447
456
  pending.cancelled && pending.cancelled.resolve()
448
457
  }
449
- query = results = errorResponse = chunk = nextWriteTimer = null
458
+ query = results = errorResponse = chunk = nextWriteTimer = barrier = null
450
459
  result = new Result()
451
460
  rows = 0
452
461
  nonce = serverSignature = null
@@ -510,8 +519,10 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
510
519
  }
511
520
 
512
521
  function submit(q) {
513
- if (stream)
514
- return queryError(q, Errors.generic('COPY_IN_PROGRESS', 'You cannot execute queries during copy'))
522
+ if (stream) {
523
+ queryError(q, Errors.generic('COPY_IN_PROGRESS', 'You cannot execute queries during copy'))
524
+ return true
525
+ }
515
526
 
516
527
  if (q.cancelled)
517
528
  return true
@@ -525,17 +536,21 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
525
536
  : (query = q, query.active = true)
526
537
 
527
538
  build(q)
528
- return write(toBuffer(q))
529
- && !q.describeFirst
530
- && !q.cursorFn
531
- && sent.length < max_pipeline
539
+ const written = write(toBuffer(q))
540
+ if (q.describeFirst || q.cursorFn || mayCopy(q))
541
+ barrier = q
542
+ return written && accepting()
532
543
  } catch (error) {
533
544
  sent.remove(q) || write(Sync)
534
545
  queryError(q, error)
535
- return sent.length < max_pipeline
546
+ return accepting()
536
547
  }
537
548
  }
538
549
 
550
+ function mayCopy(q) {
551
+ return q.options.simple && /\bcopy\b/i.test(q.string)
552
+ }
553
+
539
554
  function toBuffer(q) {
540
555
  if (q.parameters.length >= 65534)
541
556
  throw Errors.generic('MAX_PARAMETERS_EXCEEDED', 'Max number of parameters (65534) exceeded')
@@ -594,7 +609,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
594
609
  ? statements[q.signature]
595
610
  : { string, types, name: q.prepare ? statementId + statementCount++ : '' }
596
611
 
597
- typeof options.debug === 'function' && options.debug(id, string, parameters, types)
612
+ typeof options.debug === 'function' && callback(options.debug, id, string, parameters, types)
598
613
  }
599
614
 
600
615
  function write(x, fn) {
@@ -765,18 +780,21 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
765
780
  backendParameters[k] = v
766
781
  if (options.parameters[k] !== v) {
767
782
  options.parameters[k] = v
768
- onparameter && onparameter(k, v)
783
+ callback(onparameter, k, v)
769
784
  }
770
785
  }
771
786
 
772
- function ReadyForQuery() {
787
+ function ReadyForQuery(x) {
788
+ query === barrier && (barrier = null)
773
789
  if (query) {
774
790
  if (errorResponse) {
775
791
  if (query.initialization)
776
792
  return dispatch('initFailed', errorResponse)
793
+ query.prepared && retryRoutines.has(errorResponse.routine) && delete statements[query.signature]
777
794
  query.retried
778
795
  ? errored(query.retried)
779
- : query.prepared && retryRoutines.has(errorResponse.routine) && query.owner === connection.owner
796
+ : query.prepared && retryRoutines.has(errorResponse.routine) && query.owner === connection.owner && !barrier
797
+ && (x[5] !== 69 || endsAbortedState.test(query.string))
780
798
  ? retry(query, errorResponse)
781
799
  : errored(errorResponse)
782
800
  } else {
@@ -1059,7 +1077,6 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1059
1077
  }
1060
1078
 
1061
1079
  function retry(q, error) {
1062
- delete statements[q.signature]
1063
1080
  q.retried = error
1064
1081
  execute(q)
1065
1082
  }
@@ -1070,7 +1087,8 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1070
1087
 
1071
1088
  let index = 9
1072
1089
  while (x[index++] !== 0);
1073
- onnotify(
1090
+ callback(
1091
+ onnotify,
1074
1092
  x.toString('utf8', 9, index - 1),
1075
1093
  x.toString('utf8', index, x.length - 1)
1076
1094
  )
@@ -1116,6 +1134,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1116
1134
  }
1117
1135
  })
1118
1136
  query.resolve(stream)
1137
+ connection.owner && connection.owner.next()
1119
1138
  }
1120
1139
 
1121
1140
  function CopyOutResponse() {
@@ -1123,6 +1142,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1123
1142
  read() { socket.resume() }
1124
1143
  })
1125
1144
  query.resolve(stream)
1145
+ connection.owner && connection.owner.next()
1126
1146
  }
1127
1147
 
1128
1148
  /* c8 ignore next 3 */
@@ -1145,6 +1165,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1145
1165
  }
1146
1166
  })
1147
1167
  query.resolve(stream)
1168
+ connection.owner && connection.owner.next()
1148
1169
  }
1149
1170
 
1150
1171
  function CopyData(x) {
@@ -1157,10 +1178,12 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1157
1178
  }
1158
1179
 
1159
1180
  function NoticeResponse(x) {
1181
+ if (onnotice === false)
1182
+ return
1183
+
1160
1184
  onnotice
1161
- ? onnotice(parseError(x))
1185
+ ? callback(onnotice, parseError(x))
1162
1186
  : console.log(parseError(x)) // eslint-disable-line
1163
-
1164
1187
  }
1165
1188
 
1166
1189
  /* c8 ignore next 3 */
@@ -1311,3 +1334,19 @@ function timer(fn, seconds) {
1311
1334
  fn.apply(null, args)
1312
1335
  }
1313
1336
  }
1337
+
1338
+ module.exports.defaultBackoff = defaultBackoff;function defaultBackoff(retries) {
1339
+ return (0.5 + Math.random() / 2) * Math.min(3 ** retries / 100, 20)
1340
+ }
1341
+
1342
+ module.exports.backoffSeconds = backoffSeconds;function backoffSeconds(backoff, retries) {
1343
+ if (typeof backoff !== 'function')
1344
+ return backoff
1345
+
1346
+ try {
1347
+ return backoff(retries)
1348
+ } catch (e) {
1349
+ callback(() => { throw e })
1350
+ return defaultBackoff(retries)
1351
+ }
1352
+ }
package/cjs/src/errors.js CHANGED
@@ -6,6 +6,14 @@ const PostgresError = module.exports.PostgresError = class PostgresError extends
6
6
  }
7
7
  }
8
8
 
9
+ module.exports.callback = callback;function callback(fn, ...args) {
10
+ try {
11
+ fn && fn(...args)
12
+ } catch (e) {
13
+ queueMicrotask(() => { throw e })
14
+ }
15
+ }
16
+
9
17
  const Errors = module.exports.Errors = {
10
18
  connection,
11
19
  postgres,