@voltro/sql-postgres 0.54.0 → 0.56.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/CHANGELOG.md +639 -2
- package/dist/index.d.ts +104 -1
- package/dist/index.js +198 -87
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ChangeEvent } from '@voltro/database';
|
|
2
2
|
import { ChangeStrategy } from '@voltro/database';
|
|
3
|
+
import { ChangeStreamGap } from '@voltro/database';
|
|
3
4
|
import { ConfigError } from 'effect';
|
|
4
5
|
import { ConnectionConfig } from '@voltro/database';
|
|
5
6
|
import { Context } from 'effect';
|
|
@@ -20,6 +21,51 @@ import { SqlDialect } from '@voltro/database';
|
|
|
20
21
|
import { SqlError } from '@effect/sql';
|
|
21
22
|
import { TransactionConnection } from '@effect/sql/SqlClient';
|
|
22
23
|
|
|
24
|
+
/** Marker payload for the heartbeat, on the same channel as the changes.
|
|
25
|
+
*
|
|
26
|
+
* A separate channel would need a second `LISTEN` — and a second thing that can
|
|
27
|
+
* be registered on a connection whose FIRST registration is the one in doubt.
|
|
28
|
+
* Sharing the channel means the probe exercises exactly the subscription the
|
|
29
|
+
* changes ride on, which is the whole point of probing. */
|
|
30
|
+
export declare const CDC_HEARTBEAT_MARKER = "__voltro_cdc_heartbeat";
|
|
31
|
+
|
|
32
|
+
export declare interface CdcLivenessState {
|
|
33
|
+
/** Wall-clock ms of the last notification of ANY kind on the channel. */
|
|
34
|
+
readonly lastHeardAtMs: number;
|
|
35
|
+
/** When the outstanding probe was sent; `null` when none is outstanding. */
|
|
36
|
+
readonly probeSentAtMs: number | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export declare interface CdcLivenessTunables {
|
|
40
|
+
/** Silence on the channel before a probe is sent. */
|
|
41
|
+
readonly idleMs: number;
|
|
42
|
+
/** How long a sent probe may go unheard before the consumer is declared dead. */
|
|
43
|
+
readonly timeoutMs: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** What the watchdog should do on this tick. */
|
|
47
|
+
export declare type CdcLivenessVerdict =
|
|
48
|
+
/** Traffic is recent (or a probe is still within its deadline) — do nothing. */
|
|
49
|
+
'healthy'
|
|
50
|
+
/** Nothing heard for `idleMs` — send a heartbeat and start the deadline. */
|
|
51
|
+
| 'probe'
|
|
52
|
+
/** A probe went unanswered for `timeoutMs` — the consumer is deaf; reconnect. */
|
|
53
|
+
| 'reconnect';
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Pure verdict for one watchdog tick.
|
|
57
|
+
*
|
|
58
|
+
* The ordering of the two branches is load-bearing: an outstanding probe is
|
|
59
|
+
* resolved FIRST. Checking idleness first would re-arm the probe on every tick
|
|
60
|
+
* of a dead connection and the deadline would never expire — the watchdog would
|
|
61
|
+
* beat forever at a corpse and never once conclude anything.
|
|
62
|
+
*/
|
|
63
|
+
export declare const cdcLivenessVerdict: (state: CdcLivenessState, nowMs: number, tunables?: CdcLivenessTunables) => CdcLivenessVerdict;
|
|
64
|
+
|
|
65
|
+
/** Reconnect backoff: 250ms doubling to a 10s ceiling. Uncapped attempts —
|
|
66
|
+
* a consumer that gives up is the defect this file exists to remove. */
|
|
67
|
+
export declare const cdcReconnectDelayMs: (attempt: number) => number;
|
|
68
|
+
|
|
23
69
|
/**
|
|
24
70
|
* Every number the CDC consumer would otherwise pick on your behalf, with its
|
|
25
71
|
* default. Each is also readable from the environment, so an operator can tune
|
|
@@ -66,6 +112,8 @@ export declare type DatabaseConnectionLayer = ReturnType<typeof makePostgresSqlL
|
|
|
66
112
|
|
|
67
113
|
export declare type DatabaseConnectionLayerInput = Parameters<typeof makePostgresSqlLayer>[0];
|
|
68
114
|
|
|
115
|
+
export declare const DEFAULT_CDC_LIVENESS: CdcLivenessTunables;
|
|
116
|
+
|
|
69
117
|
/** Encode one row as a COPY TEXT line (no trailing newline). */
|
|
70
118
|
export declare const encodeCopyRow: (row: Readonly<Record<string, unknown>>, columns: ReadonlyArray<string>, columnTypes?: Readonly<Record<string, string>>) => string;
|
|
71
119
|
|
|
@@ -242,8 +290,23 @@ export declare class PostgresDataStore implements DataStore {
|
|
|
242
290
|
private readonly changeStrategy;
|
|
243
291
|
private readonly cdcChannel;
|
|
244
292
|
private readonly cdcRehydrate;
|
|
293
|
+
private readonly cdcLiveness;
|
|
245
294
|
private readonly emitter;
|
|
246
295
|
private cdcFiber;
|
|
296
|
+
/** Watchdog state for the LISTEN consumer — see `cdcLiveness.ts` for why a
|
|
297
|
+
* dropped LISTEN connection cannot be detected by waiting for an error. */
|
|
298
|
+
private cdcWatchdog;
|
|
299
|
+
private cdcLastHeardAt;
|
|
300
|
+
/** Notifications received on this channel, ever. A COUNTER and not the
|
|
301
|
+
* timestamp, because "did anything arrive since I asked" is not answerable
|
|
302
|
+
* from a clock two events can share a millisecond of. */
|
|
303
|
+
private cdcHeardCount;
|
|
304
|
+
private cdcProbeSentAt;
|
|
305
|
+
/** Serialises the watchdog's reconnect with any later one, so a slow
|
|
306
|
+
* re-listen cannot be raced into two live consumers on one channel. */
|
|
307
|
+
private cdcReconnecting;
|
|
308
|
+
private cdcStopped;
|
|
309
|
+
private readonly cdcGapListeners;
|
|
247
310
|
/** In-flight count of `transactional()` calls. `close()` waits for
|
|
248
311
|
* this to drop to 0 (or its grace period to expire) before disposing
|
|
249
312
|
* the runtime — preventing connection-pool teardown mid-COMMIT. */
|
|
@@ -255,7 +318,7 @@ export declare class PostgresDataStore implements DataStore {
|
|
|
255
318
|
/** PERF-23 — counts EVERY eager JSON-agg → walker degradation and logs a
|
|
256
319
|
* rate-limited line. Instance-scoped for the same reason as above. */
|
|
257
320
|
private readonly reportEagerFallback;
|
|
258
|
-
constructor(sql: SqlClient.SqlClient, runtime: ManagedRuntime.ManagedRuntime<SqlClient.SqlClient | PgClient.PgClient, never>, changeStrategy: ChangeStrategy, cdcChannel: string, cdcRehydrate?: CdcRehydrateTunables);
|
|
321
|
+
constructor(sql: SqlClient.SqlClient, runtime: ManagedRuntime.ManagedRuntime<SqlClient.SqlClient | PgClient.PgClient, never>, changeStrategy: ChangeStrategy, cdcChannel: string, cdcRehydrate?: CdcRehydrateTunables, cdcLiveness?: CdcLivenessTunables);
|
|
259
322
|
/**
|
|
260
323
|
* The DIALECT half of the shared transaction bracket
|
|
261
324
|
* (`runStoreTransaction` / `runRetryingTransaction` in `@voltro/database`).
|
|
@@ -569,6 +632,41 @@ export declare class PostgresDataStore implements DataStore {
|
|
|
569
632
|
* what re-hydration can and cannot recover.
|
|
570
633
|
*/
|
|
571
634
|
startCdcConsumer(): Promise<void>;
|
|
635
|
+
/**
|
|
636
|
+
* Wait until the consumer can PROVE it receives — by hearing its own
|
|
637
|
+
* heartbeat come back.
|
|
638
|
+
*
|
|
639
|
+
* `runFork` returns before the stream's `LISTEN` has executed, so a consumer
|
|
640
|
+
* that has merely been started is not yet a consumer that hears anything.
|
|
641
|
+
* That window is small and it is real: this test suite caught a change written
|
|
642
|
+
* 20ms after a "reconnected" log line being lost, which is the very failure the
|
|
643
|
+
* reconnect exists to end — reintroduced one level down, and reported as
|
|
644
|
+
* success. An echo is the only evidence that settles it.
|
|
645
|
+
*/
|
|
646
|
+
private proveCdcListening;
|
|
647
|
+
/**
|
|
648
|
+
* Register the gap listener the reactive layer recovers with.
|
|
649
|
+
*
|
|
650
|
+
* Fires AFTER the consumer is listening again, never while it is down: the
|
|
651
|
+
* recovery is "re-run every live query", and re-running them against a
|
|
652
|
+
* connection that is still deaf just produces a second stale answer.
|
|
653
|
+
*/
|
|
654
|
+
onChangeStreamGap(listener: (gap: ChangeStreamGap) => void): () => void;
|
|
655
|
+
private emitCdcGap;
|
|
656
|
+
/** Open (or re-open) the LISTEN stream and fork the consumer onto it. */
|
|
657
|
+
private attachCdcStream;
|
|
658
|
+
/** Probe an idle channel, and reconnect a consumer that stops answering. */
|
|
659
|
+
private startCdcWatchdog;
|
|
660
|
+
private cdcWatchdogTick;
|
|
661
|
+
/**
|
|
662
|
+
* Tear the consumer down and open a new one, retrying until it succeeds.
|
|
663
|
+
*
|
|
664
|
+
* Reports a gap on success. That second half is what makes the recovery
|
|
665
|
+
* correct rather than merely alive: every NOTIFY sent while the connection was
|
|
666
|
+
* down is gone — postgres queues nothing for a listener that is not there — so
|
|
667
|
+
* a consumer that reconnects silently keeps serving whatever it last saw.
|
|
668
|
+
*/
|
|
669
|
+
private reconnectCdc;
|
|
572
670
|
}
|
|
573
671
|
|
|
574
672
|
export declare interface PostgresDataStoreOptions {
|
|
@@ -626,6 +724,11 @@ export declare const postgresReplicationAdapter: () => DialectReplicationAdapter
|
|
|
626
724
|
/** `SqlDialect.retryFilter` implementation for postgres. */
|
|
627
725
|
export declare const postgresRetryFilter: (err: unknown) => RetryDecision;
|
|
628
726
|
|
|
727
|
+
/** Read the tunables from env, ignoring values that would disable the watchdog.
|
|
728
|
+
* A zero or negative `idleMs` would probe on every tick; a zero `timeoutMs`
|
|
729
|
+
* would declare every probe dead before its echo could arrive. */
|
|
730
|
+
export declare const resolveCdcLiveness: (env?: Record<string, string | undefined>, overrides?: Partial<CdcLivenessTunables>) => CdcLivenessTunables;
|
|
731
|
+
|
|
629
732
|
/**
|
|
630
733
|
* Captured inside `transactional()`'s `withTransaction` scope. Holds the
|
|
631
734
|
* `TransactionConnection` context tuple `[Connection, depth]` so we can
|