@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.
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
 
@@ -81,6 +82,7 @@ function Postgres(a, b) {
81
82
  PostgresError,
82
83
  options,
83
84
  reserve,
85
+ stats,
84
86
  listen,
85
87
  begin,
86
88
  close,
@@ -150,6 +152,56 @@ function Postgres(a, b) {
150
152
  }
151
153
  }
152
154
 
155
+ function stats() {
156
+ let idleCount = 0
157
+ , busyCount = 0
158
+ , reservedCount = 0
159
+ , connectingCount = 0
160
+ , closingCount = 0
161
+ , executingCount = 0
162
+ , waitingCount = queries.length
163
+
164
+ for (const c of connections) {
165
+ executingCount += c.active
166
+ waitingCount += c.pipelined + c.acquiring + (c.owner ? c.owner.queue.length : 0)
167
+ c.queue === open
168
+ ? idleCount++
169
+ : c.queue === connecting
170
+ ? connectingCount++
171
+ : c.queue === ended
172
+ ? closingCount++
173
+ : c.queue === reserved
174
+ ? reservedCount++
175
+ : (c.queue === busy || c.queue === full) && (c.owner ? reservedCount++ : busyCount++)
176
+ }
177
+
178
+ const totalCount = idleCount + busyCount + reservedCount + connectingCount
179
+
180
+ return {
181
+ max: options.max,
182
+ totalCount,
183
+ idleCount,
184
+ waitingCount,
185
+ activeCount: busyCount + reservedCount,
186
+ availableCount: idleCount + options.max - totalCount - closingCount,
187
+ busyCount,
188
+ reservedCount,
189
+ connectingCount,
190
+ closingCount,
191
+ executingCount,
192
+ listenCount: pooled(listen.sql),
193
+ subscribeCount: pooled(subscribe.sql)
194
+ }
195
+ }
196
+
197
+ function pooled(sql) {
198
+ if (!sql)
199
+ return 0
200
+
201
+ const s = sql.stats()
202
+ return s.totalCount + s.closingCount
203
+ }
204
+
153
205
  async function listen(name, fn, onlisten) {
154
206
  const listener = { fn, onlisten }
155
207
 
@@ -167,7 +219,7 @@ function Postgres(a, b) {
167
219
  })
168
220
  },
169
221
  onnotify(c, x) {
170
- 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))
171
223
  }
172
224
  }))
173
225
 
@@ -177,7 +229,7 @@ function Postgres(a, b) {
177
229
  if (exists) {
178
230
  channels[name].listeners.push(listener)
179
231
  const result = await channels[name].result
180
- listener.onlisten && listener.onlisten()
232
+ callback(listener.onlisten)
181
233
  return { state: result.state, unlisten }
182
234
  }
183
235
 
@@ -185,7 +237,7 @@ function Postgres(a, b) {
185
237
  sql.unsafe('"' + name.replace(/"/g, '""') + '"')
186
238
  }`, listeners: [listener] }
187
239
  const result = await channels[name].result
188
- listener.onlisten && listener.onlisten()
240
+ callback(listener.onlisten)
189
241
  return { state: result.state, unlisten }
190
242
 
191
243
  async function unlisten() {
@@ -243,9 +295,9 @@ function Postgres(a, b) {
243
295
  function send(l, q, expected = Lease.Active) {
244
296
  const c = l.connection
245
297
  l.state === expected
246
- ? c.queue === full
247
- ? l.queue.push(q)
248
- : c.execute(q) || move(c, full)
298
+ ? c.copying || (c.queue !== full && c.accepting)
299
+ ? c.execute(q) || move(c, full)
300
+ : (move(c, full), q.queue = l.queue, l.queue.push(q))
249
301
  : q.reject(leaseError(l.state))
250
302
  }
251
303
 
@@ -275,6 +327,7 @@ function Postgres(a, b) {
275
327
  const l = await lease(false)
276
328
  const sql = Sql(q => send(l, q))
277
329
  sql.release = () => settle(l, Lease.Released)
330
+ sql.stats = stats
278
331
  return sql
279
332
  }
280
333
 
@@ -300,14 +353,16 @@ function Postgres(a, b) {
300
353
  }
301
354
 
302
355
  async function finish(statement) {
356
+ let result
303
357
  l.state = Lease.Settling
304
358
  try {
305
- await statement(Sql(q => send(l, q, Lease.Settling)))
359
+ result = await statement(Sql(q => send(l, q, Lease.Settling)))
306
360
  } catch (e) {
307
361
  settle(l, Lease.Discarded)
308
362
  throw e
309
363
  }
310
364
  settle(l, Lease.Released)
365
+ return result
311
366
  }
312
367
 
313
368
  async function scope(fn, name) {
@@ -322,7 +377,7 @@ function Postgres(a, b) {
322
377
  let uncaughtError
323
378
  , result
324
379
 
325
- name && await sql`savepoint ${ sql(name) }`
380
+ name && await sql.unsafe('savepoint ' + escapeIdentifier(name))
326
381
  try {
327
382
  result = await new Promise((resolve, reject) => {
328
383
  const x = fn(sql)
@@ -333,8 +388,8 @@ function Postgres(a, b) {
333
388
  throw uncaughtError
334
389
  } catch (e) {
335
390
  l.state === Lease.Active && await (name
336
- ? sql`rollback to ${ sql(name) }`
337
- : finish(final => final`rollback`)
391
+ ? sql.unsafe('rollback to ' + escapeIdentifier(name))
392
+ : finish(final => final.unsafe('rollback'))
338
393
  )
339
394
  throw e instanceof PostgresError && e.code === '25P02' && uncaughtError || e
340
395
  }
@@ -342,10 +397,18 @@ function Postgres(a, b) {
342
397
  if (l.state !== Lease.Active)
343
398
  throw l.error || leaseError(l.state)
344
399
 
345
- name || await finish(final => prepare
346
- ? final`prepare transaction ${ final.unsafe(prepare) }`
347
- : final`commit`
348
- )
400
+ if (!name) {
401
+ const done = await finish(final => prepare
402
+ ? final.unsafe('prepare transaction ' + prepare)
403
+ : final.unsafe('commit')
404
+ )
405
+ if (done.command === 'ROLLBACK') {
406
+ throw Object.assign(
407
+ Errors.generic('TRANSACTION_ROLLED_BACK', 'The server rolled the transaction back instead of committing it'),
408
+ uncaughtError && { cause: uncaughtError }
409
+ )
410
+ }
411
+ }
349
412
 
350
413
  return result
351
414
 
@@ -374,14 +437,6 @@ function Postgres(a, b) {
374
437
  return c
375
438
  }
376
439
 
377
- function callback(fn, ...args) {
378
- try {
379
- fn && fn(...args)
380
- } catch (e) {
381
- queueMicrotask(() => { throw e })
382
- }
383
- }
384
-
385
440
  function json(x) {
386
441
  return new Parameter(x, 3802)
387
442
  }
@@ -404,11 +459,24 @@ function Postgres(a, b) {
404
459
  if (closed.length)
405
460
  return connect(closed.shift(), query)
406
461
 
407
- busy.length
408
- ? go(busy.shift(), query)
462
+ const c = takeBusy()
463
+ c
464
+ ? go(c, query)
409
465
  : queries.push(query)
410
466
  }
411
467
 
468
+ function takeBusy() {
469
+ while (busy.length) {
470
+ const c = busy.shift()
471
+ if (c.accepting)
472
+ return c
473
+
474
+ move(c, full)
475
+ }
476
+
477
+ return null
478
+ }
479
+
412
480
  function takeOpen() {
413
481
  while (open.length) {
414
482
  const c = open.shift()
@@ -435,7 +503,7 @@ function Postgres(a, b) {
435
503
  if (query.state) {
436
504
  query.cancelled = { resolve, reject }
437
505
  } else {
438
- queries.remove(query)
506
+ (query.queue || queries).remove(query)
439
507
  query.cancelled = true
440
508
  query.reject(Errors.generic('57014', 'canceling statement due to user request'))
441
509
  resolve()
@@ -493,6 +561,9 @@ function Postgres(a, b) {
493
561
  if (initial && initial.reserve)
494
562
  return initial.reserve(c)
495
563
 
564
+ if (!initial && !c.accepting)
565
+ return move(c, full)
566
+
496
567
  if (!initial && !queries.length)
497
568
  return move(c, open)
498
569
 
@@ -531,7 +602,7 @@ function Postgres(a, b) {
531
602
  function onclose(c, e) {
532
603
  move(c, closed)
533
604
  c.owner && settle(c.owner, Lease.Closed, e)
534
- options.onclose && options.onclose(c.id)
605
+ callback(options.onclose, c.id)
535
606
  if (ending)
536
607
  return closedWhileEnding(c, e)
537
608
 
@@ -586,7 +657,7 @@ function parseOptions(a, b) {
586
657
  connect_timeout : 30,
587
658
  max_lifetime : max_lifetime,
588
659
  max_pipeline : 100,
589
- backoff : backoff,
660
+ backoff : defaultBackoff,
590
661
  keep_alive : 60,
591
662
  prepare : true,
592
663
  debug : false,
@@ -645,10 +716,6 @@ function tsa(o, url, env) {
645
716
  throw new Error('target_session_attrs ' + x + ' is not supported')
646
717
  }
647
718
 
648
- function backoff(retries) {
649
- return (0.5 + Math.random() / 2) * Math.min(3 ** retries / 100, 20)
650
- }
651
-
652
719
  function max_lifetime() {
653
720
  return 60 * (30 + Math.random() * 30)
654
721
  }
@@ -1,4 +1,8 @@
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' }
2
6
 
3
7
  module.exports = Subscribe;function Subscribe(postgres, options) {
4
8
  const subscribers = new Map()
@@ -7,6 +11,9 @@ module.exports = Subscribe;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 @@ module.exports = Subscribe;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 @@ module.exports = Subscribe;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 @@ module.exports = Subscribe;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 @@ module.exports = Subscribe;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 @@ module.exports = Subscribe;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 @@ module.exports = Subscribe;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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steadybrew/postgresjs",
3
- "version": "4.2.0",
3
+ "version": "4.4.0",
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",