@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/README.md CHANGED
@@ -592,6 +592,8 @@ for await (const chunk of readableStream) {
592
592
 
593
593
  > **NOTE** This is a low-level API which does not provide any type safety. To make this work, you must match your [`copy query` parameters](https://www.postgresql.org/docs/14/sql-copy.html) correctly to your [Node.js stream read or write](https://nodejs.org/api/stream.html) code. Ensure [Node.js stream backpressure](https://nodejs.org/en/learn/modules/backpressuring-in-streams) is handled correctly to avoid memory exhaustion.
594
594
 
595
+ A `COPY` holds its connection until it finishes. Postgres.js cannot tell that a simple-protocol query is a `COPY` until the server answers, so it sends nothing behind a simple query whose text contains the word `copy` (`sql.unsafe()` or `sql.file()` without parameters, `.simple()`, `.readable()`, `.writable()`). Queries sent through `sql` meanwhile run on another connection or wait for one. When every connection is running a `COPY` that has not been ended, for example with `max: 1`, queries, `sql.reserve()` and `sql.begin()` through `sql` reject with `COPY_IN_PROGRESS` instead, because only that `COPY` finishing could free a connection. Statements on a `sql.reserve()` or `sql.begin()` handle reject with `COPY_IN_PROGRESS` while its `COPY` runs.
596
+
595
597
  ### Canceling Queries in Progress
596
598
 
597
599
  Postgres.js supports, [canceling queries in progress](https://www.postgresql.org/docs/7.1/protocol-protocol.html#AEN39000). It works by opening a new connection with a protocol level startup message to cancel the current query running on a specific connection. That means there is no guarantee that the query will be canceled, and due to the possible race conditions it might even result in canceling another query. This is fine for long running queries, but in the case of high load and fast queries it might be better to simply ignore results instead of canceling.
@@ -869,7 +871,7 @@ const sql = postgres({
869
871
  When you call `.listen`, a dedicated connection will be created to ensure that you receive notifications instantly. This connection will be used for any further calls to `.listen`. The connection will automatically reconnect according to a backoff reconnection pattern to not overload the database server.
870
872
 
871
873
  ### Listen `await sql.listen(channel, onnotify, [onlisten]) -> { state }`
872
- `.listen` takes the channel name, a function to handle each notify, and an optional function to run every time listen is registered and ready (happens on initial connect and reconnects). It returns a promise which resolves once the `LISTEN` query to Postgres completes, or if there is already a listener active.
874
+ `.listen` takes the channel name, a function to handle each notify, and an optional function to run every time listen is registered and ready (happens on initial connect and reconnects). It returns a promise which resolves once the `LISTEN` query to Postgres completes, or if there is already a listener active. If either function throws, the error is rethrown as an uncaught exception, and the other listeners and the returned promise are not affected.
873
875
 
874
876
  ```js
875
877
  await sql.listen('news', payload => {
@@ -922,10 +924,17 @@ const { unsubscribe } = await sql.subscribe(
922
924
  },
923
925
  () => {
924
926
  // Callback on initial connect and potential reconnects
927
+ },
928
+ (error) => {
929
+ // Callback on connection errors and changes that could not be decoded
925
930
  }
926
931
  )
927
932
  ```
928
933
 
934
+ If any of these callbacks throws, the error is rethrown as an uncaught exception, and other subscribers and the replication stream are not affected.
935
+
936
+ `onerror` is called with errors of the replication connection, after which the subscription reconnects on a new replication slot and calls `onsubscribe` again (these are also logged with `console.error`, and changes committed while it was disconnected are not delivered; if the reconnect itself fails, it is retried with backoff, its error is logged with `console.error` and passed to `onerror`, and it never ends the process), and with errors thrown while a change is decoded by a [transform](#data-transformation) or a custom type parser. Such a change is skipped, and the replication stream and later changes carry on. A decode error goes only to the subscriptions that would have received the change, matched by operation and table; a subscription filtered by key gets every skipped change of its table, as the key is read from the decoded row, even if the table has no primary key. If a column name transform throws, each change of that table is skipped and reported until the server describes the table again, for example after `ALTER TABLE`. If one of the matching subscriptions has no `onerror`, the error is rethrown once as an uncaught exception, which ends the process under Node's default handling. `onerror` stays registered across reconnects until `unsubscribe()`.
937
+
929
938
  ### Subscribe pattern
930
939
 
931
940
  You can subscribe to specific operations, tables, or even rows with primary keys.
@@ -1051,6 +1060,8 @@ const sql = postgres('postgres://username:password@host:port/database', {
1051
1060
  })
1052
1061
  ```
1053
1062
 
1063
+ The `onnotice`, `onparameter`, `onnotify`, `onclose`, `onidle` and `debug` callbacks passed in the options are called synchronously from inside the driver. If one throws, the error is rethrown as an uncaught exception and the connection and its queries are not affected; under Node's default handling that ends the process.
1064
+
1054
1065
  Note that `max_lifetime = 60 * (30 + Math.random() * 30)` by default. This resolves to an interval between 30 and 60 minutes to optimize for the benefits of prepared statements **and** working nicely with Linux's OOM killer.
1055
1066
 
1056
1067
  ### Dynamic passwords
@@ -1457,7 +1468,12 @@ If the only attempt times out, the error is `CONNECT_TIMEOUT`. Once earlier atte
1457
1468
  ##### COPY_IN_PROGRESS
1458
1469
  > You cannot execute queries during copy
1459
1470
 
1460
- This error is thrown if trying to run a query during a copy operation (writable / readable).
1471
+ This error is thrown for a statement on a `sql.reserve()` or `sql.begin()` handle while its copy operation (writable / readable) runs, including statements sent before the server started it. A query, `sql.reserve()` or `sql.begin()` through `sql` gets it when every connection is running a copy operation whose writable has not been ended; otherwise it waits for a connection.
1472
+
1473
+ ##### TRANSACTION_ROLLED_BACK
1474
+ > The server rolled the transaction back instead of committing it
1475
+
1476
+ This error is thrown by `sql.begin()` when the server answers the final `COMMIT` (or `PREPARE TRANSACTION`) with `ROLLBACK`, because the transaction was aborted by a statement whose error the callback did not surface, such as a failing query that was not awaited. Nothing was committed. When such a query's error was already recorded, it is the error's `cause`.
1461
1477
 
1462
1478
  ## TypeScript support
1463
1479
 
@@ -7,7 +7,7 @@ import Stream from 'node:stream'
7
7
  import { performance } from '../polyfills.js'
8
8
 
9
9
  import { stringify, handleValue, addArrayType } from './types.js'
10
- import { Errors } from './errors.js'
10
+ import { Errors, callback } from './errors.js'
11
11
  import Result from './result.js'
12
12
  import Queue from './queue.js'
13
13
  import clampedTimeout from './timeout.js'
@@ -15,7 +15,7 @@ import { SSLRequest, tlsConfig, cancelRequest } from './transport.js'
15
15
  import { Query, CLOSE } from './query.js'
16
16
  import b from './bytes.js'
17
17
 
18
- export default Connection
18
+ export { Connection }
19
19
 
20
20
  let uid = 1
21
21
 
@@ -28,6 +28,8 @@ const Sync = b().S().end()
28
28
  const Phase = { Closed: 0, Backoff: 1, Connecting: 2, Initializing: 3, Ready: 4, Draining: 5, Closing: 6 }
29
29
  const phaseNames = Object.keys(Phase)
30
30
 
31
+ const endsAbortedState = /^\s*(rollback|abort)\b/i
32
+
31
33
  const retryRoutines = new Set([
32
34
  'FetchPreparedStatement',
33
35
  'RevalidateCachedQuery',
@@ -112,6 +114,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
112
114
  , chunk = null
113
115
  , nonce = null
114
116
  , query = null
117
+ , barrier = null
115
118
  , final = null
116
119
 
117
120
  const check = globalThis[Symbol.for('postgres.js:check')]
@@ -226,6 +229,9 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
226
229
  owner: null,
227
230
  get active() { return query ? 1 : 0 },
228
231
  get pipelined() { return sent.length },
232
+ get accepting() { return accepting() },
233
+ get copying() { return stream !== null },
234
+ get copyHeld() { return stream !== null && !stream.writableEnded },
229
235
  get acquiring() { return acquisition && !acquisition.owner.cancelled ? 1 : 0 },
230
236
  count: 0,
231
237
  id
@@ -270,8 +276,12 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
270
276
  return !query && sent.length === 0
271
277
  }
272
278
 
279
+ function accepting() {
280
+ return !query || (!barrier && sent.length < max_pipeline)
281
+ }
282
+
273
283
  function backoffMs() {
274
- return (typeof backoff === 'function' ? backoff(options.shared.retries) : backoff) * 1000
284
+ return backoffSeconds(backoff, options.shared.retries) * 1000
275
285
  }
276
286
 
277
287
  function standbyPass() {
@@ -448,7 +458,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
448
458
  queryError(pending, err)
449
459
  pending.cancelled && pending.cancelled.resolve()
450
460
  }
451
- query = results = errorResponse = chunk = nextWriteTimer = null
461
+ query = results = errorResponse = chunk = nextWriteTimer = barrier = null
452
462
  result = new Result()
453
463
  rows = 0
454
464
  nonce = serverSignature = null
@@ -512,8 +522,10 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
512
522
  }
513
523
 
514
524
  function submit(q) {
515
- if (stream)
516
- return queryError(q, Errors.generic('COPY_IN_PROGRESS', 'You cannot execute queries during copy'))
525
+ if (stream) {
526
+ queryError(q, Errors.generic('COPY_IN_PROGRESS', 'You cannot execute queries during copy'))
527
+ return true
528
+ }
517
529
 
518
530
  if (q.cancelled)
519
531
  return true
@@ -527,17 +539,21 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
527
539
  : (query = q, query.active = true)
528
540
 
529
541
  build(q)
530
- return write(toBuffer(q))
531
- && !q.describeFirst
532
- && !q.cursorFn
533
- && sent.length < max_pipeline
542
+ const written = write(toBuffer(q))
543
+ if (q.describeFirst || q.cursorFn || mayCopy(q))
544
+ barrier = q
545
+ return written && accepting()
534
546
  } catch (error) {
535
547
  sent.remove(q) || write(Sync)
536
548
  queryError(q, error)
537
- return sent.length < max_pipeline
549
+ return accepting()
538
550
  }
539
551
  }
540
552
 
553
+ function mayCopy(q) {
554
+ return q.options.simple && /\bcopy\b/i.test(q.string)
555
+ }
556
+
541
557
  function toBuffer(q) {
542
558
  if (q.parameters.length >= 65534)
543
559
  throw Errors.generic('MAX_PARAMETERS_EXCEEDED', 'Max number of parameters (65534) exceeded')
@@ -596,7 +612,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
596
612
  ? statements[q.signature]
597
613
  : { string, types, name: q.prepare ? statementId + statementCount++ : '' }
598
614
 
599
- typeof options.debug === 'function' && options.debug(id, string, parameters, types)
615
+ typeof options.debug === 'function' && callback(options.debug, id, string, parameters, types)
600
616
  }
601
617
 
602
618
  function write(x, fn) {
@@ -767,18 +783,21 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
767
783
  backendParameters[k] = v
768
784
  if (options.parameters[k] !== v) {
769
785
  options.parameters[k] = v
770
- onparameter && onparameter(k, v)
786
+ callback(onparameter, k, v)
771
787
  }
772
788
  }
773
789
 
774
- function ReadyForQuery() {
790
+ function ReadyForQuery(x) {
791
+ query === barrier && (barrier = null)
775
792
  if (query) {
776
793
  if (errorResponse) {
777
794
  if (query.initialization)
778
795
  return dispatch('initFailed', errorResponse)
796
+ query.prepared && retryRoutines.has(errorResponse.routine) && delete statements[query.signature]
779
797
  query.retried
780
798
  ? errored(query.retried)
781
- : query.prepared && retryRoutines.has(errorResponse.routine) && query.owner === connection.owner
799
+ : query.prepared && retryRoutines.has(errorResponse.routine) && query.owner === connection.owner && !barrier
800
+ && (x[5] !== 69 || endsAbortedState.test(query.string))
782
801
  ? retry(query, errorResponse)
783
802
  : errored(errorResponse)
784
803
  } else {
@@ -1061,7 +1080,6 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1061
1080
  }
1062
1081
 
1063
1082
  function retry(q, error) {
1064
- delete statements[q.signature]
1065
1083
  q.retried = error
1066
1084
  execute(q)
1067
1085
  }
@@ -1072,7 +1090,8 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1072
1090
 
1073
1091
  let index = 9
1074
1092
  while (x[index++] !== 0);
1075
- onnotify(
1093
+ callback(
1094
+ onnotify,
1076
1095
  x.toString('utf8', 9, index - 1),
1077
1096
  x.toString('utf8', index, x.length - 1)
1078
1097
  )
@@ -1118,6 +1137,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1118
1137
  }
1119
1138
  })
1120
1139
  query.resolve(stream)
1140
+ connection.owner && connection.owner.next()
1121
1141
  }
1122
1142
 
1123
1143
  function CopyOutResponse() {
@@ -1125,6 +1145,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1125
1145
  read() { socket.resume() }
1126
1146
  })
1127
1147
  query.resolve(stream)
1148
+ connection.owner && connection.owner.next()
1128
1149
  }
1129
1150
 
1130
1151
  /* c8 ignore next 3 */
@@ -1147,6 +1168,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1147
1168
  }
1148
1169
  })
1149
1170
  query.resolve(stream)
1171
+ connection.owner && connection.owner.next()
1150
1172
  }
1151
1173
 
1152
1174
  function CopyData(x) {
@@ -1159,10 +1181,12 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1159
1181
  }
1160
1182
 
1161
1183
  function NoticeResponse(x) {
1184
+ if (onnotice === false)
1185
+ return
1186
+
1162
1187
  onnotice
1163
- ? onnotice(parseError(x))
1188
+ ? callback(onnotice, parseError(x))
1164
1189
  : console.log(parseError(x)) // eslint-disable-line
1165
-
1166
1190
  }
1167
1191
 
1168
1192
  /* c8 ignore next 3 */
@@ -1313,3 +1337,19 @@ function timer(fn, seconds) {
1313
1337
  fn.apply(null, args)
1314
1338
  }
1315
1339
  }
1340
+
1341
+ export function defaultBackoff(retries) {
1342
+ return (0.5 + Math.random() / 2) * Math.min(3 ** retries / 100, 20)
1343
+ }
1344
+
1345
+ export function backoffSeconds(backoff, retries) {
1346
+ if (typeof backoff !== 'function')
1347
+ return backoff
1348
+
1349
+ try {
1350
+ return backoff(retries)
1351
+ } catch (e) {
1352
+ callback(() => { throw e })
1353
+ return defaultBackoff(retries)
1354
+ }
1355
+ }
package/cf/src/errors.js CHANGED
@@ -6,6 +6,14 @@ export class PostgresError extends Error {
6
6
  }
7
7
  }
8
8
 
9
+ export function callback(fn, ...args) {
10
+ try {
11
+ fn && fn(...args)
12
+ } catch (e) {
13
+ queueMicrotask(() => { throw e })
14
+ }
15
+ }
16
+
9
17
  export const Errors = {
10
18
  connection,
11
19
  postgres,
package/cf/src/index.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  Parameter,
10
10
  Identifier,
11
11
  Builder,
12
+ escapeIdentifier,
12
13
  toPascal,
13
14
  pascal,
14
15
  toCamel,
@@ -20,12 +21,12 @@ import {
20
21
  fromKebab
21
22
  } from './types.js'
22
23
 
23
- import Connection from './connection.js'
24
+ import { Connection, defaultBackoff } from './connection.js'
24
25
  import { cancelRequest } from './transport.js'
25
26
  import clampedTimeout from './timeout.js'
26
27
  import { Query, CLOSE } from './query.js'
27
28
  import Queue from './queue.js'
28
- import { Errors, PostgresError } from './errors.js'
29
+ import { Errors, PostgresError, callback } from './errors.js'
29
30
  import Subscribe from './subscribe.js'
30
31
  import largeObject from './large.js'
31
32
 
@@ -219,7 +220,7 @@ function Postgres(a, b) {
219
220
  })
220
221
  },
221
222
  onnotify(c, x) {
222
- c in listen.channels && listen.channels[c].listeners.forEach(l => l.fn(x))
223
+ c in listen.channels && listen.channels[c].listeners.forEach(l => callback(l.fn, x))
223
224
  }
224
225
  }))
225
226
 
@@ -229,7 +230,7 @@ function Postgres(a, b) {
229
230
  if (exists) {
230
231
  channels[name].listeners.push(listener)
231
232
  const result = await channels[name].result
232
- listener.onlisten && listener.onlisten()
233
+ callback(listener.onlisten)
233
234
  return { state: result.state, unlisten }
234
235
  }
235
236
 
@@ -237,7 +238,7 @@ function Postgres(a, b) {
237
238
  sql.unsafe('"' + name.replace(/"/g, '""') + '"')
238
239
  }`, listeners: [listener] }
239
240
  const result = await channels[name].result
240
- listener.onlisten && listener.onlisten()
241
+ callback(listener.onlisten)
241
242
  return { state: result.state, unlisten }
242
243
 
243
244
  async function unlisten() {
@@ -273,7 +274,9 @@ function Postgres(a, b) {
273
274
  const query = { reserve: c => resolve(own(c, l)), reject }
274
275
  closed.length
275
276
  ? connect(closed.shift(), query)
276
- : queries.push(query)
277
+ : allHeldByCopy()
278
+ ? reject(copyInProgress())
279
+ : queries.push(query)
277
280
  })
278
281
  return l
279
282
  }
@@ -295,9 +298,9 @@ function Postgres(a, b) {
295
298
  function send(l, q, expected = Lease.Active) {
296
299
  const c = l.connection
297
300
  l.state === expected
298
- ? c.queue === full
299
- ? (q.queue = l.queue, l.queue.push(q))
300
- : c.execute(q) || move(c, full)
301
+ ? c.copying || (c.queue !== full && c.accepting)
302
+ ? c.execute(q) || move(c, full)
303
+ : (move(c, full), q.queue = l.queue, l.queue.push(q))
301
304
  : q.reject(leaseError(l.state))
302
305
  }
303
306
 
@@ -353,14 +356,16 @@ function Postgres(a, b) {
353
356
  }
354
357
 
355
358
  async function finish(statement) {
359
+ let result
356
360
  l.state = Lease.Settling
357
361
  try {
358
- await statement(Sql(q => send(l, q, Lease.Settling)))
362
+ result = await statement(Sql(q => send(l, q, Lease.Settling)))
359
363
  } catch (e) {
360
364
  settle(l, Lease.Discarded)
361
365
  throw e
362
366
  }
363
367
  settle(l, Lease.Released)
368
+ return result
364
369
  }
365
370
 
366
371
  async function scope(fn, name) {
@@ -375,7 +380,7 @@ function Postgres(a, b) {
375
380
  let uncaughtError
376
381
  , result
377
382
 
378
- name && await sql`savepoint ${ sql(name) }`
383
+ name && await sql.unsafe('savepoint ' + escapeIdentifier(name))
379
384
  try {
380
385
  result = await new Promise((resolve, reject) => {
381
386
  const x = fn(sql)
@@ -386,8 +391,8 @@ function Postgres(a, b) {
386
391
  throw uncaughtError
387
392
  } catch (e) {
388
393
  l.state === Lease.Active && await (name
389
- ? sql`rollback to ${ sql(name) }`
390
- : finish(final => final`rollback`)
394
+ ? sql.unsafe('rollback to ' + escapeIdentifier(name))
395
+ : finish(final => final.unsafe('rollback'))
391
396
  )
392
397
  throw e instanceof PostgresError && e.code === '25P02' && uncaughtError || e
393
398
  }
@@ -395,10 +400,18 @@ function Postgres(a, b) {
395
400
  if (l.state !== Lease.Active)
396
401
  throw l.error || leaseError(l.state)
397
402
 
398
- name || await finish(final => prepare
399
- ? final`prepare transaction ${ final.unsafe(prepare) }`
400
- : final`commit`
401
- )
403
+ if (!name) {
404
+ const done = await finish(final => prepare
405
+ ? final.unsafe('prepare transaction ' + prepare)
406
+ : final.unsafe('commit')
407
+ )
408
+ if (done.command === 'ROLLBACK') {
409
+ throw Object.assign(
410
+ Errors.generic('TRANSACTION_ROLLED_BACK', 'The server rolled the transaction back instead of committing it'),
411
+ uncaughtError && { cause: uncaughtError }
412
+ )
413
+ }
414
+ }
402
415
 
403
416
  return result
404
417
 
@@ -427,14 +440,6 @@ function Postgres(a, b) {
427
440
  return c
428
441
  }
429
442
 
430
- function callback(fn, ...args) {
431
- try {
432
- fn && fn(...args)
433
- } catch (e) {
434
- queueMicrotask(() => { throw e })
435
- }
436
- }
437
-
438
443
  function json(x) {
439
444
  return new Parameter(x, 3802)
440
445
  }
@@ -457,9 +462,32 @@ function Postgres(a, b) {
457
462
  if (closed.length)
458
463
  return connect(closed.shift(), query)
459
464
 
460
- busy.length
461
- ? go(busy.shift(), query)
462
- : queries.push(query)
465
+ const c = takeBusy()
466
+ c
467
+ ? go(c, query)
468
+ : allHeldByCopy()
469
+ ? query.reject(copyInProgress())
470
+ : queries.push(query)
471
+ }
472
+
473
+ function allHeldByCopy() {
474
+ return connections.every(c => c.copyHeld)
475
+ }
476
+
477
+ function copyInProgress() {
478
+ return Errors.generic('COPY_IN_PROGRESS', 'You cannot execute queries during copy')
479
+ }
480
+
481
+ function takeBusy() {
482
+ while (busy.length) {
483
+ const c = busy.shift()
484
+ if (c.accepting)
485
+ return c
486
+
487
+ move(c, full)
488
+ }
489
+
490
+ return null
463
491
  }
464
492
 
465
493
  function takeOpen() {
@@ -546,6 +574,9 @@ function Postgres(a, b) {
546
574
  if (initial && initial.reserve)
547
575
  return initial.reserve(c)
548
576
 
577
+ if (!initial && !c.accepting)
578
+ return move(c, full)
579
+
549
580
  if (!initial && !queries.length)
550
581
  return move(c, open)
551
582
 
@@ -584,7 +615,7 @@ function Postgres(a, b) {
584
615
  function onclose(c, e) {
585
616
  move(c, closed)
586
617
  c.owner && settle(c.owner, Lease.Closed, e)
587
- options.onclose && options.onclose(c.id)
618
+ callback(options.onclose, c.id)
588
619
  if (ending)
589
620
  return closedWhileEnding(c, e)
590
621
 
@@ -639,7 +670,7 @@ function parseOptions(a, b) {
639
670
  connect_timeout : 30,
640
671
  max_lifetime : max_lifetime,
641
672
  max_pipeline : 100,
642
- backoff : backoff,
673
+ backoff : defaultBackoff,
643
674
  keep_alive : 60,
644
675
  prepare : true,
645
676
  debug : false,
@@ -698,10 +729,6 @@ function tsa(o, url, env) {
698
729
  throw new Error('target_session_attrs ' + x + ' is not supported')
699
730
  }
700
731
 
701
- function backoff(retries) {
702
- return (0.5 + Math.random() / 2) * Math.min(3 ** retries / 100, 20)
703
- }
704
-
705
732
  function max_lifetime() {
706
733
  return 60 * (30 + Math.random() * 30)
707
734
  }