@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.
@@ -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')]
@@ -222,6 +225,11 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
222
225
  },
223
226
  expired: () => idleTimer.expired() || lifeTimer.expired(),
224
227
  owner: null,
228
+ get active() { return query ? 1 : 0 },
229
+ get pipelined() { return sent.length },
230
+ get accepting() { return accepting() },
231
+ get copying() { return stream !== null },
232
+ get acquiring() { return acquisition && !acquisition.owner.cancelled ? 1 : 0 },
225
233
  count: 0,
226
234
  id
227
235
  }
@@ -265,8 +273,12 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
265
273
  return !query && sent.length === 0
266
274
  }
267
275
 
276
+ function accepting() {
277
+ return !query || (!barrier && sent.length < max_pipeline)
278
+ }
279
+
268
280
  function backoffMs() {
269
- return (typeof backoff === 'function' ? backoff(options.shared.retries) : backoff) * 1000
281
+ return backoffSeconds(backoff, options.shared.retries) * 1000
270
282
  }
271
283
 
272
284
  function standbyPass() {
@@ -443,7 +455,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
443
455
  queryError(pending, err)
444
456
  pending.cancelled && pending.cancelled.resolve()
445
457
  }
446
- query = results = errorResponse = chunk = nextWriteTimer = null
458
+ query = results = errorResponse = chunk = nextWriteTimer = barrier = null
447
459
  result = new Result()
448
460
  rows = 0
449
461
  nonce = serverSignature = null
@@ -507,8 +519,10 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
507
519
  }
508
520
 
509
521
  function submit(q) {
510
- if (stream)
511
- 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
+ }
512
526
 
513
527
  if (q.cancelled)
514
528
  return true
@@ -522,17 +536,21 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
522
536
  : (query = q, query.active = true)
523
537
 
524
538
  build(q)
525
- return write(toBuffer(q))
526
- && !q.describeFirst
527
- && !q.cursorFn
528
- && 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()
529
543
  } catch (error) {
530
- sent.length === 0 && write(Sync)
531
- errored(error)
532
- return true
544
+ sent.remove(q) || write(Sync)
545
+ queryError(q, error)
546
+ return accepting()
533
547
  }
534
548
  }
535
549
 
550
+ function mayCopy(q) {
551
+ return q.options.simple && /\bcopy\b/i.test(q.string)
552
+ }
553
+
536
554
  function toBuffer(q) {
537
555
  if (q.parameters.length >= 65534)
538
556
  throw Errors.generic('MAX_PARAMETERS_EXCEEDED', 'Max number of parameters (65534) exceeded')
@@ -591,7 +609,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
591
609
  ? statements[q.signature]
592
610
  : { string, types, name: q.prepare ? statementId + statementCount++ : '' }
593
611
 
594
- typeof options.debug === 'function' && options.debug(id, string, parameters, types)
612
+ typeof options.debug === 'function' && callback(options.debug, id, string, parameters, types)
595
613
  }
596
614
 
597
615
  function write(x, fn) {
@@ -762,18 +780,21 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
762
780
  backendParameters[k] = v
763
781
  if (options.parameters[k] !== v) {
764
782
  options.parameters[k] = v
765
- onparameter && onparameter(k, v)
783
+ callback(onparameter, k, v)
766
784
  }
767
785
  }
768
786
 
769
- function ReadyForQuery() {
787
+ function ReadyForQuery(x) {
788
+ query === barrier && (barrier = null)
770
789
  if (query) {
771
790
  if (errorResponse) {
772
791
  if (query.initialization)
773
792
  return dispatch('initFailed', errorResponse)
793
+ query.prepared && retryRoutines.has(errorResponse.routine) && delete statements[query.signature]
774
794
  query.retried
775
795
  ? errored(query.retried)
776
- : 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))
777
798
  ? retry(query, errorResponse)
778
799
  : errored(errorResponse)
779
800
  } else {
@@ -1056,7 +1077,6 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1056
1077
  }
1057
1078
 
1058
1079
  function retry(q, error) {
1059
- delete statements[q.signature]
1060
1080
  q.retried = error
1061
1081
  execute(q)
1062
1082
  }
@@ -1067,7 +1087,8 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1067
1087
 
1068
1088
  let index = 9
1069
1089
  while (x[index++] !== 0);
1070
- onnotify(
1090
+ callback(
1091
+ onnotify,
1071
1092
  x.toString('utf8', 9, index - 1),
1072
1093
  x.toString('utf8', index, x.length - 1)
1073
1094
  )
@@ -1113,6 +1134,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1113
1134
  }
1114
1135
  })
1115
1136
  query.resolve(stream)
1137
+ connection.owner && connection.owner.next()
1116
1138
  }
1117
1139
 
1118
1140
  function CopyOutResponse() {
@@ -1120,6 +1142,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1120
1142
  read() { socket.resume() }
1121
1143
  })
1122
1144
  query.resolve(stream)
1145
+ connection.owner && connection.owner.next()
1123
1146
  }
1124
1147
 
1125
1148
  /* c8 ignore next 3 */
@@ -1142,6 +1165,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1142
1165
  }
1143
1166
  })
1144
1167
  query.resolve(stream)
1168
+ connection.owner && connection.owner.next()
1145
1169
  }
1146
1170
 
1147
1171
  function CopyData(x) {
@@ -1154,10 +1178,12 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1154
1178
  }
1155
1179
 
1156
1180
  function NoticeResponse(x) {
1181
+ if (onnotice === false)
1182
+ return
1183
+
1157
1184
  onnotice
1158
- ? onnotice(parseError(x))
1185
+ ? callback(onnotice, parseError(x))
1159
1186
  : console.log(parseError(x)) // eslint-disable-line
1160
-
1161
1187
  }
1162
1188
 
1163
1189
  /* c8 ignore next 3 */
@@ -1308,3 +1334,19 @@ function timer(fn, seconds) {
1308
1334
  fn.apply(null, args)
1309
1335
  }
1310
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,