@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/cjs/src/index.js CHANGED
@@ -8,6 +8,7 @@ const {
8
8
  Parameter,
9
9
  Identifier,
10
10
  Builder,
11
+ escapeIdentifier,
11
12
  toPascal,
12
13
  pascal,
13
14
  toCamel,
@@ -19,12 +20,12 @@ const {
19
20
  fromKebab
20
21
  } = require('./types.js')
21
22
 
22
- const Connection = require('./connection.js')
23
+ const { Connection, defaultBackoff } = require('./connection.js')
23
24
  const { cancelRequest } = require('./transport.js')
24
25
  const clampedTimeout = require('./timeout.js')
25
26
  const { Query, CLOSE } = require('./query.js')
26
27
  const Queue = require('./queue.js')
27
- const { Errors, PostgresError } = require('./errors.js')
28
+ const { Errors, PostgresError, callback } = require('./errors.js')
28
29
  const Subscribe = require('./subscribe.js')
29
30
  const largeObject = require('./large.js')
30
31
 
@@ -218,7 +219,7 @@ function Postgres(a, b) {
218
219
  })
219
220
  },
220
221
  onnotify(c, x) {
221
- c in listen.channels && listen.channels[c].listeners.forEach(l => l.fn(x))
222
+ c in listen.channels && listen.channels[c].listeners.forEach(l => callback(l.fn, x))
222
223
  }
223
224
  }))
224
225
 
@@ -228,7 +229,7 @@ function Postgres(a, b) {
228
229
  if (exists) {
229
230
  channels[name].listeners.push(listener)
230
231
  const result = await channels[name].result
231
- listener.onlisten && listener.onlisten()
232
+ callback(listener.onlisten)
232
233
  return { state: result.state, unlisten }
233
234
  }
234
235
 
@@ -236,7 +237,7 @@ function Postgres(a, b) {
236
237
  sql.unsafe('"' + name.replace(/"/g, '""') + '"')
237
238
  }`, listeners: [listener] }
238
239
  const result = await channels[name].result
239
- listener.onlisten && listener.onlisten()
240
+ callback(listener.onlisten)
240
241
  return { state: result.state, unlisten }
241
242
 
242
243
  async function unlisten() {
@@ -272,7 +273,9 @@ function Postgres(a, b) {
272
273
  const query = { reserve: c => resolve(own(c, l)), reject }
273
274
  closed.length
274
275
  ? connect(closed.shift(), query)
275
- : queries.push(query)
276
+ : allHeldByCopy()
277
+ ? reject(copyInProgress())
278
+ : queries.push(query)
276
279
  })
277
280
  return l
278
281
  }
@@ -294,9 +297,9 @@ function Postgres(a, b) {
294
297
  function send(l, q, expected = Lease.Active) {
295
298
  const c = l.connection
296
299
  l.state === expected
297
- ? c.queue === full
298
- ? (q.queue = l.queue, l.queue.push(q))
299
- : c.execute(q) || move(c, full)
300
+ ? c.copying || (c.queue !== full && c.accepting)
301
+ ? c.execute(q) || move(c, full)
302
+ : (move(c, full), q.queue = l.queue, l.queue.push(q))
300
303
  : q.reject(leaseError(l.state))
301
304
  }
302
305
 
@@ -352,14 +355,16 @@ function Postgres(a, b) {
352
355
  }
353
356
 
354
357
  async function finish(statement) {
358
+ let result
355
359
  l.state = Lease.Settling
356
360
  try {
357
- await statement(Sql(q => send(l, q, Lease.Settling)))
361
+ result = await statement(Sql(q => send(l, q, Lease.Settling)))
358
362
  } catch (e) {
359
363
  settle(l, Lease.Discarded)
360
364
  throw e
361
365
  }
362
366
  settle(l, Lease.Released)
367
+ return result
363
368
  }
364
369
 
365
370
  async function scope(fn, name) {
@@ -374,7 +379,7 @@ function Postgres(a, b) {
374
379
  let uncaughtError
375
380
  , result
376
381
 
377
- name && await sql`savepoint ${ sql(name) }`
382
+ name && await sql.unsafe('savepoint ' + escapeIdentifier(name))
378
383
  try {
379
384
  result = await new Promise((resolve, reject) => {
380
385
  const x = fn(sql)
@@ -385,8 +390,8 @@ function Postgres(a, b) {
385
390
  throw uncaughtError
386
391
  } catch (e) {
387
392
  l.state === Lease.Active && await (name
388
- ? sql`rollback to ${ sql(name) }`
389
- : finish(final => final`rollback`)
393
+ ? sql.unsafe('rollback to ' + escapeIdentifier(name))
394
+ : finish(final => final.unsafe('rollback'))
390
395
  )
391
396
  throw e instanceof PostgresError && e.code === '25P02' && uncaughtError || e
392
397
  }
@@ -394,10 +399,18 @@ function Postgres(a, b) {
394
399
  if (l.state !== Lease.Active)
395
400
  throw l.error || leaseError(l.state)
396
401
 
397
- name || await finish(final => prepare
398
- ? final`prepare transaction ${ final.unsafe(prepare) }`
399
- : final`commit`
400
- )
402
+ if (!name) {
403
+ const done = await finish(final => prepare
404
+ ? final.unsafe('prepare transaction ' + prepare)
405
+ : final.unsafe('commit')
406
+ )
407
+ if (done.command === 'ROLLBACK') {
408
+ throw Object.assign(
409
+ Errors.generic('TRANSACTION_ROLLED_BACK', 'The server rolled the transaction back instead of committing it'),
410
+ uncaughtError && { cause: uncaughtError }
411
+ )
412
+ }
413
+ }
401
414
 
402
415
  return result
403
416
 
@@ -426,14 +439,6 @@ function Postgres(a, b) {
426
439
  return c
427
440
  }
428
441
 
429
- function callback(fn, ...args) {
430
- try {
431
- fn && fn(...args)
432
- } catch (e) {
433
- queueMicrotask(() => { throw e })
434
- }
435
- }
436
-
437
442
  function json(x) {
438
443
  return new Parameter(x, 3802)
439
444
  }
@@ -456,9 +461,32 @@ function Postgres(a, b) {
456
461
  if (closed.length)
457
462
  return connect(closed.shift(), query)
458
463
 
459
- busy.length
460
- ? go(busy.shift(), query)
461
- : queries.push(query)
464
+ const c = takeBusy()
465
+ c
466
+ ? go(c, query)
467
+ : allHeldByCopy()
468
+ ? query.reject(copyInProgress())
469
+ : queries.push(query)
470
+ }
471
+
472
+ function allHeldByCopy() {
473
+ return connections.every(c => c.copyHeld)
474
+ }
475
+
476
+ function copyInProgress() {
477
+ return Errors.generic('COPY_IN_PROGRESS', 'You cannot execute queries during copy')
478
+ }
479
+
480
+ function takeBusy() {
481
+ while (busy.length) {
482
+ const c = busy.shift()
483
+ if (c.accepting)
484
+ return c
485
+
486
+ move(c, full)
487
+ }
488
+
489
+ return null
462
490
  }
463
491
 
464
492
  function takeOpen() {
@@ -545,6 +573,9 @@ function Postgres(a, b) {
545
573
  if (initial && initial.reserve)
546
574
  return initial.reserve(c)
547
575
 
576
+ if (!initial && !c.accepting)
577
+ return move(c, full)
578
+
548
579
  if (!initial && !queries.length)
549
580
  return move(c, open)
550
581
 
@@ -583,7 +614,7 @@ function Postgres(a, b) {
583
614
  function onclose(c, e) {
584
615
  move(c, closed)
585
616
  c.owner && settle(c.owner, Lease.Closed, e)
586
- options.onclose && options.onclose(c.id)
617
+ callback(options.onclose, c.id)
587
618
  if (ending)
588
619
  return closedWhileEnding(c, e)
589
620
 
@@ -638,7 +669,7 @@ function parseOptions(a, b) {
638
669
  connect_timeout : 30,
639
670
  max_lifetime : max_lifetime,
640
671
  max_pipeline : 100,
641
- backoff : backoff,
672
+ backoff : defaultBackoff,
642
673
  keep_alive : 60,
643
674
  prepare : true,
644
675
  debug : false,
@@ -697,10 +728,6 @@ function tsa(o, url, env) {
697
728
  throw new Error('target_session_attrs ' + x + ' is not supported')
698
729
  }
699
730
 
700
- function backoff(retries) {
701
- return (0.5 + Math.random() / 2) * Math.min(3 ** retries / 100, 20)
702
- }
703
-
704
731
  function max_lifetime() {
705
732
  return 60 * (30 + Math.random() * 30)
706
733
  }
@@ -1,4 +1,9 @@
1
+ const { callback } = require('./errors.js')
2
+ const { defaultBackoff, backoffSeconds } = require('./connection.js')
3
+
1
4
  const noop = () => { /* noop */ }
5
+ , commands = { 68: 'delete', 73: 'insert', 85: 'update' }
6
+ , statusInterval = 1000
2
7
 
3
8
  module.exports = Subscribe;function Subscribe(postgres, options) {
4
9
  const subscribers = new Map()
@@ -7,6 +12,9 @@ module.exports = Subscribe;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 @@ module.exports = Subscribe;function Subscribe(postgres, options) {
21
29
  ...options.connection,
22
30
  replication: 'database'
23
31
  },
24
- onclose: async function() {
32
+ onclose: function() {
25
33
  if (ended)
26
34
  return
27
35
  stream = null
28
36
  state.pid = state.secret = undefined
29
- connected(await init(sql, slot, options.publications))
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 @@ module.exports = Subscribe;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 @@ module.exports = Subscribe;function Subscribe(postgres, options) {
48
58
 
49
59
  return subscribe
50
60
 
51
- async function subscribe(event, fn, onsubscribe = noop, onerror = noop) {
61
+ async function subscribe(event, fn, onsubscribe = noop, onerror) {
52
62
  event = parseEvent(event)
53
63
 
54
- if (!connection)
55
- connection = init(sql, slot, options.publications)
64
+ const subscriber = { fn, onsubscribe, onerror }
56
65
 
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)
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
- const unsubscribe = () => {
63
- fns.delete(subscriber)
64
- fns.size === 0 && subscribers.delete(event)
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
- return connection.then(x => {
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 connected(x) {
76
- stream = x.stream
77
- state.pid = x.state.pid
78
- 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 })
79
129
  }
80
130
 
81
131
  async function init(sql, slot, publications) {
@@ -99,18 +149,22 @@ module.exports = Subscribe;function Subscribe(postgres, options) {
99
149
  }
100
150
 
101
151
  stream.on('data', data)
102
- stream.on('error', error)
152
+ stream.on('error', reportConnection)
103
153
  stream.on('close', sql.close)
104
154
 
105
- return { stream, state: xs.state }
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
- function error(e) {
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
- parse(x.subarray(25), state, sql.options.parsers, handle, options.transform)
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 @@ module.exports = Subscribe;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
- b.relation.keys.length && call('*:' + path + '=' + b.relation.keys.map(x => a[x.name]), a, b)
179
+ key && call('*:' + path + key, a, b)
125
180
  call(b.command, a, b)
126
181
  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)
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 @@ module.exports = Subscribe;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(a, b, x))
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
- 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)
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
- column.key && r.keys.push(column)
177
- i += 4
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)?:?([^.]+?\.?[^=]+)?=?(.+)?/i) || []
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steadybrew/postgresjs",
3
- "version": "4.3.0",
3
+ "version": "4.4.1",
4
4
  "description": "PostgreSQL client for Node.js and Deno, independently maintained by Steady Brew",
5
5
  "type": "module",
6
6
  "module": "src/index.js",