@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.
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() {
@@ -294,9 +295,9 @@ function Postgres(a, b) {
294
295
  function send(l, q, expected = Lease.Active) {
295
296
  const c = l.connection
296
297
  l.state === expected
297
- ? c.queue === full
298
- ? (q.queue = l.queue, l.queue.push(q))
299
- : 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))
300
301
  : q.reject(leaseError(l.state))
301
302
  }
302
303
 
@@ -352,14 +353,16 @@ function Postgres(a, b) {
352
353
  }
353
354
 
354
355
  async function finish(statement) {
356
+ let result
355
357
  l.state = Lease.Settling
356
358
  try {
357
- await statement(Sql(q => send(l, q, Lease.Settling)))
359
+ result = await statement(Sql(q => send(l, q, Lease.Settling)))
358
360
  } catch (e) {
359
361
  settle(l, Lease.Discarded)
360
362
  throw e
361
363
  }
362
364
  settle(l, Lease.Released)
365
+ return result
363
366
  }
364
367
 
365
368
  async function scope(fn, name) {
@@ -374,7 +377,7 @@ function Postgres(a, b) {
374
377
  let uncaughtError
375
378
  , result
376
379
 
377
- name && await sql`savepoint ${ sql(name) }`
380
+ name && await sql.unsafe('savepoint ' + escapeIdentifier(name))
378
381
  try {
379
382
  result = await new Promise((resolve, reject) => {
380
383
  const x = fn(sql)
@@ -385,8 +388,8 @@ function Postgres(a, b) {
385
388
  throw uncaughtError
386
389
  } catch (e) {
387
390
  l.state === Lease.Active && await (name
388
- ? sql`rollback to ${ sql(name) }`
389
- : finish(final => final`rollback`)
391
+ ? sql.unsafe('rollback to ' + escapeIdentifier(name))
392
+ : finish(final => final.unsafe('rollback'))
390
393
  )
391
394
  throw e instanceof PostgresError && e.code === '25P02' && uncaughtError || e
392
395
  }
@@ -394,10 +397,18 @@ function Postgres(a, b) {
394
397
  if (l.state !== Lease.Active)
395
398
  throw l.error || leaseError(l.state)
396
399
 
397
- name || await finish(final => prepare
398
- ? final`prepare transaction ${ final.unsafe(prepare) }`
399
- : final`commit`
400
- )
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
+ }
401
412
 
402
413
  return result
403
414
 
@@ -426,14 +437,6 @@ function Postgres(a, b) {
426
437
  return c
427
438
  }
428
439
 
429
- function callback(fn, ...args) {
430
- try {
431
- fn && fn(...args)
432
- } catch (e) {
433
- queueMicrotask(() => { throw e })
434
- }
435
- }
436
-
437
440
  function json(x) {
438
441
  return new Parameter(x, 3802)
439
442
  }
@@ -456,11 +459,24 @@ function Postgres(a, b) {
456
459
  if (closed.length)
457
460
  return connect(closed.shift(), query)
458
461
 
459
- busy.length
460
- ? go(busy.shift(), query)
462
+ const c = takeBusy()
463
+ c
464
+ ? go(c, query)
461
465
  : queries.push(query)
462
466
  }
463
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
+
464
480
  function takeOpen() {
465
481
  while (open.length) {
466
482
  const c = open.shift()
@@ -545,6 +561,9 @@ function Postgres(a, b) {
545
561
  if (initial && initial.reserve)
546
562
  return initial.reserve(c)
547
563
 
564
+ if (!initial && !c.accepting)
565
+ return move(c, full)
566
+
548
567
  if (!initial && !queries.length)
549
568
  return move(c, open)
550
569
 
@@ -583,7 +602,7 @@ function Postgres(a, b) {
583
602
  function onclose(c, e) {
584
603
  move(c, closed)
585
604
  c.owner && settle(c.owner, Lease.Closed, e)
586
- options.onclose && options.onclose(c.id)
605
+ callback(options.onclose, c.id)
587
606
  if (ending)
588
607
  return closedWhileEnding(c, e)
589
608
 
@@ -638,7 +657,7 @@ function parseOptions(a, b) {
638
657
  connect_timeout : 30,
639
658
  max_lifetime : max_lifetime,
640
659
  max_pipeline : 100,
641
- backoff : backoff,
660
+ backoff : defaultBackoff,
642
661
  keep_alive : 60,
643
662
  prepare : true,
644
663
  debug : false,
@@ -697,10 +716,6 @@ function tsa(o, url, env) {
697
716
  throw new Error('target_session_attrs ' + x + ' is not supported')
698
717
  }
699
718
 
700
- function backoff(retries) {
701
- return (0.5 + Math.random() / 2) * Math.min(3 ** retries / 100, 20)
702
- }
703
-
704
719
  function max_lifetime() {
705
720
  return 60 * (30 + Math.random() * 30)
706
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.3.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",