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