turbine-orm 0.79.1 → 0.80.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.
Files changed (71) hide show
  1. package/README.md +4 -4
  2. package/dist/checkout.d.ts +53 -0
  3. package/dist/checkout.js +78 -0
  4. package/dist/cjs/checkout.d.ts +53 -0
  5. package/dist/cjs/checkout.js +82 -0
  6. package/dist/cjs/cli/index.js +4 -0
  7. package/dist/cjs/cli/mcp.js +4 -0
  8. package/dist/cjs/cli/migrate.js +6 -0
  9. package/dist/cjs/cli/observe.js +8 -0
  10. package/dist/cjs/cli/studio.js +13 -1
  11. package/dist/cjs/client.d.ts +12 -2
  12. package/dist/cjs/client.js +61 -75
  13. package/dist/cjs/connection-guard.d.ts +120 -0
  14. package/dist/cjs/connection-guard.js +191 -0
  15. package/dist/cjs/errors.d.ts +26 -0
  16. package/dist/cjs/errors.js +85 -1
  17. package/dist/cjs/index.d.ts +1 -1
  18. package/dist/cjs/nested-write.d.ts +12 -2
  19. package/dist/cjs/nested-write.js +4 -10
  20. package/dist/cjs/pipeline.js +12 -7
  21. package/dist/cjs/plan-flip-probe.js +4 -0
  22. package/dist/cjs/powdb-shared.d.ts +22 -2
  23. package/dist/cjs/powdb-shared.js +27 -2
  24. package/dist/cjs/powdb.js +36 -37
  25. package/dist/cjs/powql.d.ts +51 -6
  26. package/dist/cjs/powql.js +199 -45
  27. package/dist/cjs/prisma-compat.js +28 -4
  28. package/dist/cjs/query/builder.d.ts +44 -24
  29. package/dist/cjs/query/builder.js +125 -66
  30. package/dist/cjs/query/deferred.d.ts +9 -0
  31. package/dist/cjs/query/option-surface.js +12 -0
  32. package/dist/cjs/query/types.d.ts +68 -4
  33. package/dist/cjs/query/writes.d.ts +39 -9
  34. package/dist/cjs/query/writes.js +72 -34
  35. package/dist/cjs/realtime.d.ts +46 -2
  36. package/dist/cjs/realtime.js +125 -20
  37. package/dist/cjs/schema-sql.js +6 -0
  38. package/dist/cli/index.js +4 -0
  39. package/dist/cli/mcp.js +4 -0
  40. package/dist/cli/migrate.js +6 -0
  41. package/dist/cli/observe.js +8 -0
  42. package/dist/cli/studio.js +13 -1
  43. package/dist/client.d.ts +12 -2
  44. package/dist/client.js +62 -76
  45. package/dist/connection-guard.d.ts +120 -0
  46. package/dist/connection-guard.js +183 -0
  47. package/dist/errors.d.ts +26 -0
  48. package/dist/errors.js +83 -1
  49. package/dist/index.d.ts +1 -1
  50. package/dist/index.js +1 -1
  51. package/dist/nested-write.d.ts +12 -2
  52. package/dist/nested-write.js +4 -10
  53. package/dist/pipeline.js +13 -8
  54. package/dist/plan-flip-probe.js +4 -0
  55. package/dist/powdb-shared.d.ts +22 -2
  56. package/dist/powdb-shared.js +25 -2
  57. package/dist/powdb.js +23 -24
  58. package/dist/powql.d.ts +51 -6
  59. package/dist/powql.js +200 -46
  60. package/dist/prisma-compat.js +28 -4
  61. package/dist/query/builder.d.ts +44 -24
  62. package/dist/query/builder.js +126 -67
  63. package/dist/query/deferred.d.ts +9 -0
  64. package/dist/query/option-surface.js +12 -0
  65. package/dist/query/types.d.ts +68 -4
  66. package/dist/query/writes.d.ts +39 -9
  67. package/dist/query/writes.js +71 -34
  68. package/dist/realtime.d.ts +46 -2
  69. package/dist/realtime.js +125 -20
  70. package/dist/schema-sql.js +6 -0
  71. package/package.json +5 -3
@@ -25,10 +25,23 @@
25
25
  * (Neon HTTP, Vercel Postgres over fetch) cannot hold such a connection, so
26
26
  * `$listen` will surface a clear error rather than hang. `$notify` works
27
27
  * everywhere, it's a single round-trip `SELECT pg_notify(...)`.
28
+ *
29
+ * Connection loss:
30
+ *
31
+ * A subscription's connection is held indefinitely with no query in flight,
32
+ * so a database restart, failover or `pg_terminate_backend` reaches it only
33
+ * as an `'error'` event. That event used to have no listener, which exits the
34
+ * process. It is now guarded (connection-guard.ts), and the subscription
35
+ * RECONNECTS by default: the dead connection is destroyed, a fresh one is
36
+ * checked out with exponential backoff, and `LISTEN` is re-issued. Postgres
37
+ * does not queue notifications for a listener that is not connected, so
38
+ * anything NOTIFYed during the gap is gone for good; `onReconnect` is the
39
+ * caller's cue to resynchronise from the source of truth.
28
40
  */
29
41
  Object.defineProperty(exports, "__esModule", { value: true });
30
42
  exports.validateChannel = validateChannel;
31
43
  exports.createSubscription = createSubscription;
44
+ const connection_guard_js_1 = require("./connection-guard.js");
32
45
  const errors_js_1 = require("./errors.js");
33
46
  // ---------------------------------------------------------------------------
34
47
  // Identifier validation
@@ -62,17 +75,12 @@ function validateChannel(channel) {
62
75
  '(letters, digits, underscores; cannot start with a digit)');
63
76
  }
64
77
  }
78
+ const DEFAULT_RECONNECT = { initialDelayMs: 100, maxDelayMs: 30_000 };
65
79
  /**
66
- * Acquire a dedicated connection, run `LISTEN "channel"`, and wire the handler.
67
- *
68
- * @param pool the pg-compatible pool to check a long-lived client out of
69
- * @param channel channel name, MUST already be validated by the caller
70
- * @param quotedChannel the channel run through quoteIdent (interpolated into SQL)
71
- * @param handler called with each notification's payload
72
- * @param onClosed invoked when the subscription releases, so the client can
73
- * drop it from its active-subscription registry
80
+ * Check out a dedicated connection, wire the handler and run `LISTEN`. Throws
81
+ * (typed) on failure, having given the connection back.
74
82
  */
75
- async function createSubscription(pool, channel, quotedChannel, handler, onClosed) {
83
+ async function openListenConnection(pool, channel, quotedChannel, handler, onLost) {
76
84
  let client;
77
85
  try {
78
86
  client = (await pool.connect());
@@ -85,11 +93,12 @@ async function createSubscription(pool, channel, quotedChannel, handler, onClose
85
93
  // forever waiting for messages that can never arrive, so fail loudly now and
86
94
  // give the connection straight back.
87
95
  if (typeof client.on !== 'function') {
88
- client.release?.();
96
+ client.release();
89
97
  throw new errors_js_1.ConnectionError('$listen requires a persistent connection that can push notifications. ' +
90
98
  'The configured pool returned a client with no event support (stateless HTTP drivers ' +
91
99
  'like Neon HTTP / Vercel Postgres cannot LISTEN). Use a TCP pg.Pool for LISTEN/NOTIFY.');
92
100
  }
101
+ const checkout = (0, connection_guard_js_1.guardCheckout)(client, onLost);
93
102
  const onNotification = (msg) => {
94
103
  // pg delivers ALL notifications for the connection to every listener; a
95
104
  // dedicated connection only ever LISTENs on one channel, but guard anyway.
@@ -103,43 +112,139 @@ async function createSubscription(pool, channel, quotedChannel, handler, onClose
103
112
  }
104
113
  catch (err) {
105
114
  client.removeListener?.('notification', onNotification);
106
- client.release?.();
115
+ checkout.release(err instanceof Error ? err : true);
107
116
  throw (0, errors_js_1.wrapPgError)(err);
108
117
  }
118
+ return { client, checkout, onNotification };
119
+ }
120
+ /**
121
+ * Acquire a dedicated connection, run `LISTEN "channel"`, and wire the handler.
122
+ * A failure here is thrown to the `$listen` caller; only a connection lost
123
+ * AFTER the subscription is established is retried.
124
+ *
125
+ * @param pool the pg-compatible pool to check a long-lived client out of
126
+ * @param channel channel name, MUST already be validated by the caller
127
+ * @param quotedChannel the channel run through quoteIdent (interpolated into SQL)
128
+ * @param handler called with each notification's payload
129
+ * @param onClosed invoked when the subscription ends, so the client can
130
+ * drop it from its active-subscription registry
131
+ * @param options reconnect policy and loss/reconnect callbacks
132
+ */
133
+ async function createSubscription(pool, channel, quotedChannel, handler, onClosed, options = {}) {
134
+ const backoff = options.reconnect === false
135
+ ? undefined
136
+ : { ...DEFAULT_RECONNECT, ...(typeof options.reconnect === 'object' ? options.reconnect : {}) };
137
+ const reportError = options.onError ??
138
+ ((err) => {
139
+ console.error(`[turbine] $listen "${channel}": ${err.message}${backoff && !closed ? ', reconnecting' : ''}`);
140
+ });
109
141
  let closed = false;
142
+ let current;
143
+ let timer;
144
+ let delay = backoff?.initialDelayMs ?? 0;
145
+ /** Let go of the held connection. A lost one is destroyed, never pooled. */
146
+ const dropCurrent = (destroy) => {
147
+ const conn = current;
148
+ if (!conn)
149
+ return;
150
+ current = undefined;
151
+ conn.client.removeListener?.('notification', conn.onNotification);
152
+ conn.checkout.release(destroy);
153
+ };
154
+ const scheduleReconnect = () => {
155
+ if (closed || !backoff)
156
+ return;
157
+ timer = setTimeout(() => {
158
+ timer = undefined;
159
+ void attemptReconnect();
160
+ }, delay);
161
+ // A process with nothing else to do should not stay alive just to retry.
162
+ timer.unref?.();
163
+ delay = Math.min(delay * 2, backoff.maxDelayMs);
164
+ };
165
+ const onLost = (err) => {
166
+ if (closed || !current)
167
+ return;
168
+ dropCurrent(err);
169
+ const lost = (0, errors_js_1.wrapPgError)(err);
170
+ reportError(lost instanceof Error ? lost : err);
171
+ if (!backoff) {
172
+ closed = true;
173
+ onClosed(sub);
174
+ return;
175
+ }
176
+ delay = backoff.initialDelayMs;
177
+ scheduleReconnect();
178
+ };
179
+ const attemptReconnect = async () => {
180
+ if (closed)
181
+ return;
182
+ let conn;
183
+ try {
184
+ conn = await openListenConnection(pool, channel, quotedChannel, handler, onLost);
185
+ }
186
+ catch (err) {
187
+ if (closed)
188
+ return;
189
+ reportError(err instanceof Error ? err : new Error(String(err)));
190
+ scheduleReconnect();
191
+ return;
192
+ }
193
+ if (closed) {
194
+ // unsubscribe() or disconnect() ran while this attempt was in flight.
195
+ conn.client.removeListener?.('notification', conn.onNotification);
196
+ conn.checkout.release(true);
197
+ return;
198
+ }
199
+ current = conn;
200
+ if (backoff)
201
+ delay = backoff.initialDelayMs;
202
+ options.onReconnect?.();
203
+ };
204
+ current = await openListenConnection(pool, channel, quotedChannel, handler, onLost);
205
+ const stop = () => {
206
+ closed = true;
207
+ if (timer !== undefined) {
208
+ clearTimeout(timer);
209
+ timer = undefined;
210
+ }
211
+ };
110
212
  const sub = {
111
213
  channel,
112
214
  async unsubscribe() {
113
215
  if (closed)
114
216
  return;
115
- closed = true;
217
+ stop();
218
+ const conn = current;
219
+ if (!conn) {
220
+ // Between connections: nothing is LISTENing, so there is nothing to undo.
221
+ onClosed(sub);
222
+ return;
223
+ }
116
224
  try {
117
- await client.query(`UNLISTEN ${quotedChannel}`);
225
+ await conn.client.query(`UNLISTEN ${quotedChannel}`);
118
226
  }
119
227
  catch (err) {
120
228
  // Best-effort: the connection may already be dead. Still detach +
121
229
  // release below so we don't leak the pool slot.
122
- client.removeListener?.('notification', onNotification);
123
- client.release?.();
230
+ dropCurrent(true);
124
231
  onClosed(sub);
125
232
  throw (0, errors_js_1.wrapPgError)(err);
126
233
  }
127
- client.removeListener?.('notification', onNotification);
128
- client.release?.();
234
+ dropCurrent(undefined);
129
235
  onClosed(sub);
130
236
  },
131
237
  _forceRelease() {
132
238
  if (closed)
133
239
  return;
134
- closed = true;
135
- client.removeListener?.('notification', onNotification);
240
+ stop();
136
241
  // Destroy the connection (release(true)) rather than return it to the pool:
137
242
  // we skip UNLISTEN here (the pool is being torn down), so a recycled
138
243
  // connection would otherwise carry a stale LISTEN registration. Destroying
139
244
  // it guarantees no pooled backend keeps receiving NOTIFY traffic. Matters
140
245
  // most for external/serverless pools, where disconnect() is a no-op and the
141
246
  // pool outlives this client.
142
- client.release?.(true);
247
+ dropCurrent(true);
143
248
  onClosed(sub);
144
249
  },
145
250
  };
@@ -25,6 +25,7 @@ exports.schemaPush = schemaPush;
25
25
  exports.schemaToSQLString = schemaToSQLString;
26
26
  const pg_1 = __importDefault(require("pg"));
27
27
  const destructive_js_1 = require("./cli/destructive.js");
28
+ const connection_guard_js_1 = require("./connection-guard.js");
28
29
  const dialect_js_1 = require("./dialect.js");
29
30
  const errors_js_1 = require("./errors.js");
30
31
  const introspect_js_1 = require("./introspect.js");
@@ -616,6 +617,9 @@ async function schemaDiff(schema, connectionString, options = {}) {
616
617
  // `pgSchema` because `schema` is already the SchemaDef in this function.
617
618
  const pgSchema = options.schema ?? 'public';
618
619
  const client = new pg_1.default.Client({ connectionString });
620
+ // A bare client with no 'error' listener exits the process if the server
621
+ // drops it mid-diff; guarded, the pending query rejects instead.
622
+ (0, connection_guard_js_1.guardConnection)(client);
619
623
  await client.connect();
620
624
  try {
621
625
  // Get existing tables in the target schema
@@ -1636,6 +1640,8 @@ async function schemaPush(schema, connectionString, options = {}) {
1636
1640
  }
1637
1641
  // Execute all statements in a transaction
1638
1642
  const client = new pg_1.default.Client({ connectionString });
1643
+ // Same as schemaDiff: a dropped connection must reject the push, not exit.
1644
+ (0, connection_guard_js_1.guardConnection)(client);
1639
1645
  await client.connect();
1640
1646
  try {
1641
1647
  await client.query('BEGIN');
package/dist/cli/index.js CHANGED
@@ -29,6 +29,7 @@ import { appendFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFi
29
29
  import { tmpdir } from 'node:os';
30
30
  import { basename, dirname, extname, join, relative, resolve, sep } from 'node:path';
31
31
  import { pathToFileURL } from 'node:url';
32
+ import { guardConnection } from '../connection-guard.js';
32
33
  import { detectPooler, parseSearchPathValue, poolerRefusalMessage } from '../connection-url.js';
33
34
  import { ValidationError } from '../errors.js';
34
35
  import { generate, generatePrismaMap, resolveImportExtension } from '../generate.js';
@@ -955,6 +956,7 @@ async function probeDatabase(url, schema) {
955
956
  try {
956
957
  const { default: pg } = await import('pg');
957
958
  const client = new pg.Client({ connectionString: url });
959
+ guardConnection(client);
958
960
  await client.connect();
959
961
  let tableCount = 0;
960
962
  try {
@@ -2978,6 +2980,7 @@ async function seedConnectionString(config) {
2978
2980
  assertPinnableSchema(schema);
2979
2981
  const { default: pg } = await import('pg');
2980
2982
  const probe = new pg.Client({ connectionString: config.url });
2983
+ guardConnection(probe);
2981
2984
  await probe.connect();
2982
2985
  let inherited;
2983
2986
  try {
@@ -3049,6 +3052,7 @@ async function runSeedPlan(plan, config) {
3049
3052
  const url = seedUrl ?? requireUrl(config);
3050
3053
  const { default: pg } = await import('pg');
3051
3054
  const client = new pg.Client({ connectionString: url });
3055
+ guardConnection(client);
3052
3056
  await client.connect();
3053
3057
  try {
3054
3058
  await client.query(readFileSync(plan.file, 'utf-8'));
package/dist/cli/mcp.js CHANGED
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
2
2
  import { existsSync, readFileSync, realpathSync } from 'node:fs';
3
3
  import { dirname, resolve } from 'node:path';
4
4
  import pg from 'pg';
5
+ import { absorbCheckedOutErrors } from '../connection-guard.js';
5
6
  import { findMissingRelationIndexes } from '../index-advisor.js';
6
7
  import { formatBytes } from '../index-stats.js';
7
8
  import { deriveCatalogRelations, indexKeyColumn, parseIndexKeyEntries } from '../introspect.js';
@@ -289,6 +290,9 @@ export function startMcpServer(options, transport = {}) {
289
290
  ctx.pool.on?.('error', (err) => {
290
291
  process.stderr.write(`[turbine] mcp pool error: ${redactUrl(err.message)}\n`);
291
292
  });
293
+ // The same event on a CHECKED-OUT client (a tool call mid-query when the
294
+ // server restarts) has no pool listener to fall back on; see connection-guard.ts.
295
+ absorbCheckedOutErrors(ctx.pool);
292
296
  announcePiiTags(options);
293
297
  let buffer = '';
294
298
  let disposed = false;
@@ -16,6 +16,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from
16
16
  import { join } from 'node:path';
17
17
  import pg from 'pg';
18
18
  import { postgresql } from '../adapters/index.js';
19
+ import { guardConnection } from '../connection-guard.js';
19
20
  import { isPlainSchemaIdentifier, parseSearchPathValue, withSearchPathOption } from '../connection-url.js';
20
21
  import { postgresDialect } from '../dialect.js';
21
22
  import { MigrationError, ValidationError } from '../errors.js';
@@ -841,6 +842,9 @@ async function connectMigrationClient(connectionString, schema) {
841
842
  const inherited = schema === undefined || schema === '' ? [] : await probeConnectionSchemas(connectionString, schema);
842
843
  const pinned = connectionStringForSchema(connectionString, schema, inherited);
843
844
  const client = new pg.Client({ connectionString: pinned });
845
+ // Held for the whole run: a server restart must fail the migration with a
846
+ // message, not exit through an unheard 'error' event (connection-guard.ts).
847
+ guardConnection(client);
844
848
  await client.connect();
845
849
  return { client, connectionString: pinned };
846
850
  }
@@ -853,6 +857,7 @@ async function probeConnectionSchemas(connectionString, schema) {
853
857
  // so in those terms, see assertPinnableSchema.
854
858
  assertPinnableSchema(schema);
855
859
  const probe = new pg.Client({ connectionString });
860
+ guardConnection(probe);
856
861
  await probe.connect();
857
862
  try {
858
863
  await assertSchemaExists(probe, schema);
@@ -866,6 +871,7 @@ async function probeConnectionSchemas(connectionString, schema) {
866
871
  /** Open the second, lock-only connection. Separated so tests can fake it. */
867
872
  async function openLockConnection(connectionString) {
868
873
  const client = new pg.Client({ connectionString });
874
+ guardConnection(client);
869
875
  await client.connect();
870
876
  return client;
871
877
  }
@@ -8,8 +8,10 @@
8
8
  import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
9
9
  import { createServer } from 'node:http';
10
10
  import pg from 'pg';
11
+ import { absorbCheckedOutErrors } from '../connection-guard.js';
11
12
  import { OBSERVE_HTML } from './observe-ui.js';
12
13
  import { callerKey, checkRateLimit } from './rate-limit.js';
14
+ import { redactUrl } from './ui.js';
13
15
  // ---------------------------------------------------------------------------
14
16
  // Main entry point
15
17
  // ---------------------------------------------------------------------------
@@ -19,6 +21,12 @@ export async function startObserve(options) {
19
21
  max: 2,
20
22
  idleTimeoutMillis: 10_000,
21
23
  });
24
+ // Idle-client errors surface on the pool, checked-out ones on the client;
25
+ // with no listener either one exits the dashboard (see connection-guard.ts).
26
+ pool.on('error', (err) => {
27
+ console.error(`[turbine] observe pool error: ${redactUrl(err.message)}`);
28
+ });
29
+ absorbCheckedOutErrors(pool);
22
30
  const probe = await pool.connect();
23
31
  try {
24
32
  await probe.query('SELECT 1');
@@ -43,6 +43,7 @@ import { createServer } from 'node:http';
43
43
  import { platform } from 'node:os';
44
44
  import { dirname, resolve as pathResolve } from 'node:path';
45
45
  import pg from 'pg';
46
+ import { absorbCheckedOutErrors } from '../connection-guard.js';
46
47
  import { ValidationError } from '../errors.js';
47
48
  import { introspect } from '../introspect.js';
48
49
  import { QueryInterface, quoteIdent } from '../query/index.js';
@@ -58,6 +59,7 @@ import { applyPiiTags, loadPiiTags } from './pii-tags.js';
58
59
  import { callerKey, checkRateLimit } from './rate-limit.js';
59
60
  import { createDemoContext } from './studio-demo.js';
60
61
  import { STUDIO_HTML } from './studio-ui.generated.js';
62
+ import { redactUrl } from './ui.js';
61
63
  // ---------------------------------------------------------------------------
62
64
  // Main entry point
63
65
  // ---------------------------------------------------------------------------
@@ -102,11 +104,21 @@ export async function startStudio(options) {
102
104
  registerUtcTemporalParsers();
103
105
  // pg.Pool satisfies the PgCompatPool contract (same as the external-pool
104
106
  // seam in client.ts); the cast keeps one typed pool field for both modes.
105
- pool = new pg.Pool({
107
+ const pgPool = new pg.Pool({
106
108
  connectionString: options.url,
107
109
  max: 4, // small pool, single-user tool
108
110
  idleTimeoutMillis: 10_000,
109
111
  });
112
+ // A connection that dies while idle emits 'error' on the POOL, and one that
113
+ // dies mid-request emits it on the checked-out client. Neither had a
114
+ // listener, so a database restart exited Studio. Now the pending request
115
+ // fails and the server keeps running. The message is redacted because pg
116
+ // can echo the connection string into connection failures.
117
+ pgPool.on('error', (err) => {
118
+ console.error(`[turbine] studio pool error: ${redactUrl(err.message)}`);
119
+ });
120
+ absorbCheckedOutErrors(pgPool);
121
+ pool = pgPool;
110
122
  // Verify connectivity before starting the server, fail fast.
111
123
  const probe = await pool.connect();
112
124
  try {
package/dist/client.d.ts CHANGED
@@ -27,7 +27,7 @@ import { type ObserveConfig, type ObserveHandle } from './observe.js';
27
27
  import type { PgCompatPool, PgCompatPoolClient } from './pg-types.js';
28
28
  import { type PipelineOptions, type PipelineResults } from './pipeline.js';
29
29
  import { type DeferredQuery, type GlobalFilters, type JsonEncoding, type QueryEventListener, QueryInterface, type QueryInterfaceOptions, type RelationLoadStrategy, type TemporalInfinityReading } from './query/index.js';
30
- import { type NotificationHandler, type Subscription } from './realtime.js';
30
+ import { type ListenOptions, type NotificationHandler, type Subscription } from './realtime.js';
31
31
  import type { SchemaMetadata } from './schema.js';
32
32
  import { TypedSqlQuery } from './typed-sql.js';
33
33
  export interface RetryOptions {
@@ -1206,17 +1206,27 @@ export declare class TurbineClient {
1206
1206
  * cannot do this, `$listen` throws a `ConnectionError` rather than hang.
1207
1207
  * `$notify` works on every driver.
1208
1208
  *
1209
+ * **Connection loss:** if the subscription's connection dies (a restart,
1210
+ * failover, compute suspend, `pg_terminate_backend`), it reconnects with
1211
+ * exponential backoff and re-issues `LISTEN`; `options.reconnect: false`
1212
+ * ends it instead. Postgres does not hold notifications for a disconnected
1213
+ * listener, so anything sent during the gap is lost: use
1214
+ * `options.onReconnect` to resynchronise. `options.onError` receives the loss
1215
+ * and each failed attempt (default: one `console.error` line each).
1216
+ *
1209
1217
  * @example
1210
1218
  * ```ts
1211
1219
  * const sub = await db.$listen('order_created', (payload) => {
1212
1220
  * const order = JSON.parse(payload);
1213
1221
  * console.log('new order', order.id);
1222
+ * }, {
1223
+ * onReconnect: () => refreshOrdersFromDatabase(),
1214
1224
  * });
1215
1225
  * // ...later
1216
1226
  * await sub.unsubscribe();
1217
1227
  * ```
1218
1228
  */
1219
- $listen(channel: string, handler: NotificationHandler): Promise<Subscription>;
1229
+ $listen(channel: string, handler: NotificationHandler, options?: ListenOptions): Promise<Subscription>;
1220
1230
  /**
1221
1231
  * Send a Postgres NOTIFY on `channel` with an optional payload string.
1222
1232
  *