@rebasepro/server-postgres 0.9.1-canary.ff338b5 → 0.10.1-canary.14e53ae

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 (70) hide show
  1. package/README.md +21 -0
  2. package/dist/PostgresBackendDriver.d.ts +18 -0
  3. package/dist/PostgresBootstrapper.d.ts +11 -1
  4. package/dist/auth/services.d.ts +93 -54
  5. package/dist/backup/backup-logic.d.ts +23 -0
  6. package/dist/backup/backup-service.d.ts +44 -2
  7. package/dist/backup/pg-tools.d.ts +41 -1
  8. package/dist/chunk-DSJWtz9O.js +40 -0
  9. package/dist/cli-helpers.d.ts +33 -1
  10. package/dist/ensure-collection-tables-CNlIONzj.js +304 -0
  11. package/dist/ensure-collection-tables-CNlIONzj.js.map +1 -0
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.es.js +2090 -4741
  14. package/dist/index.es.js.map +1 -1
  15. package/dist/schema/auth-bootstrap-sql.d.ts +1 -1
  16. package/dist/schema/auth-schema.d.ts +194 -24
  17. package/dist/schema/destructive-sql.d.ts +49 -0
  18. package/dist/schema/ensure-collection-tables.d.ts +79 -0
  19. package/dist/schema/generate-postgres-ddl-logic.d.ts +4 -1
  20. package/dist/schema/introspect-db-logic.d.ts +0 -5
  21. package/dist/schema/introspect-db-naming.d.ts +10 -0
  22. package/dist/security/policy-drift.d.ts +24 -0
  23. package/dist/security/rls-enforcement.d.ts +2 -2
  24. package/dist/services/cdc/CdcListener.d.ts +7 -14
  25. package/dist/services/channel-bus/ChannelBus.d.ts +29 -0
  26. package/dist/services/channel-bus/PostgresChannelBus.d.ts +111 -0
  27. package/dist/services/channel-bus/index.d.ts +55 -0
  28. package/dist/services/channel-history.d.ts +129 -0
  29. package/dist/services/channel-presence.d.ts +66 -0
  30. package/dist/services/pg-notify-listener.d.ts +47 -0
  31. package/dist/services/realtimeService.d.ts +183 -8
  32. package/dist/src-B0v4IKaI.js +329 -0
  33. package/dist/src-B0v4IKaI.js.map +1 -0
  34. package/dist/src-DmsRg8MR.js +4056 -0
  35. package/dist/src-DmsRg8MR.js.map +1 -0
  36. package/package.json +7 -31
  37. package/src/PostgresBackendDriver.ts +56 -5
  38. package/src/PostgresBootstrapper.ts +87 -1
  39. package/src/auth/ensure-tables.ts +187 -19
  40. package/src/auth/services.ts +309 -170
  41. package/src/backup/backup-cli.ts +60 -1
  42. package/src/backup/backup-cron.ts +24 -1
  43. package/src/backup/backup-logic.ts +62 -0
  44. package/src/backup/backup-service.ts +132 -13
  45. package/src/backup/pg-tools.ts +70 -2
  46. package/src/cli-helpers.ts +82 -27
  47. package/src/cli.ts +152 -6
  48. package/src/index.ts +4 -0
  49. package/src/schema/auth-bootstrap-sql.ts +7 -1
  50. package/src/schema/auth-schema.ts +53 -15
  51. package/src/schema/destructive-sql.ts +94 -0
  52. package/src/schema/ensure-collection-tables.test.ts +156 -0
  53. package/src/schema/ensure-collection-tables.ts +297 -0
  54. package/src/schema/generate-postgres-ddl-logic.ts +3 -3
  55. package/src/schema/introspect-db-inference.ts +1 -1
  56. package/src/schema/introspect-db-logic.ts +1 -10
  57. package/src/schema/introspect-db-naming.ts +15 -0
  58. package/src/schema/introspect-runtime.ts +1 -1
  59. package/src/security/policy-drift.test.ts +46 -0
  60. package/src/security/policy-drift.ts +70 -4
  61. package/src/security/rls-enforcement.ts +11 -5
  62. package/src/services/cdc/CdcListener.ts +27 -91
  63. package/src/services/channel-bus/ChannelBus.ts +44 -0
  64. package/src/services/channel-bus/PostgresChannelBus.ts +299 -0
  65. package/src/services/channel-bus/index.ts +123 -0
  66. package/src/services/channel-history.ts +378 -0
  67. package/src/services/channel-presence.ts +148 -0
  68. package/src/services/pg-notify-listener.ts +137 -0
  69. package/src/services/realtimeService.ts +581 -24
  70. package/src/websocket.ts +30 -11
@@ -0,0 +1,137 @@
1
+ /**
2
+ * A dedicated, self-healing Postgres `LISTEN` connection.
3
+ *
4
+ * Every cross-instance feature in the backend needs the same thing: one
5
+ * connection *outside* the Drizzle pool that stays open, holds a `LISTEN`, and
6
+ * comes back on its own after the database or the network drops it. CDC needed
7
+ * it first; the channel bus needs it too. This is that connection, with the one
8
+ * behaviour that matters to callers preserved: the **first** connect is
9
+ * validated and rethrown, so a caller can fall back to a different strategy,
10
+ * while every later drop is repaired quietly in the background.
11
+ *
12
+ * `LISTEN` is session state, so this connection must not go through a
13
+ * transaction-mode pooler (PgBouncer): give it the direct database URL.
14
+ */
15
+
16
+ import { Client as PgClient } from "pg";
17
+ import { logger } from "@rebasepro/server";
18
+
19
+ export interface PgNotifyListenerOptions {
20
+ /** Direct Postgres connection string (must bypass a transaction-mode pooler). */
21
+ connectionString: string;
22
+ /** NOTIFY channel to LISTEN on. Must be a plain identifier — it is interpolated. */
23
+ channel: string;
24
+ /** Called for every notification payload received. */
25
+ onPayload: (payload: string) => void | Promise<void>;
26
+ /** Prefix for log lines, e.g. `"[CDC]"`. */
27
+ logLabel: string;
28
+ /** Delay before a reconnect attempt. */
29
+ reconnectDelayMs?: number;
30
+ }
31
+
32
+ const DEFAULT_RECONNECT_DELAY_MS = 3000;
33
+ /** Guards the identifier interpolated into `LISTEN`. */
34
+ const SAFE_CHANNEL = /^[A-Za-z_][A-Za-z0-9_]*$/;
35
+
36
+ export class PgNotifyListener {
37
+ private client?: PgClient;
38
+ private running = false;
39
+ private reconnectTimer?: ReturnType<typeof setTimeout>;
40
+
41
+ constructor(private readonly options: PgNotifyListenerOptions) {
42
+ if (!SAFE_CHANNEL.test(options.channel)) {
43
+ throw new Error(`Unsafe NOTIFY channel name "${options.channel}" — expected a plain SQL identifier.`);
44
+ }
45
+ }
46
+
47
+ /** Whether the listener is meant to be connected right now. */
48
+ get active(): boolean {
49
+ return this.running;
50
+ }
51
+
52
+ /**
53
+ * Connect and begin listening. Idempotent.
54
+ *
55
+ * Rejects if the *initial* connection or `LISTEN` fails, leaving the
56
+ * listener stopped — callers use that to degrade deliberately instead of
57
+ * running blind against a channel nothing is delivering.
58
+ */
59
+ async start(): Promise<void> {
60
+ if (this.running) return;
61
+ this.running = true;
62
+ try {
63
+ await this.connect({ initial: true });
64
+ } catch (err) {
65
+ this.running = false;
66
+ throw err;
67
+ }
68
+ }
69
+
70
+ /** Stop listening and release the connection. Idempotent. */
71
+ async stop(): Promise<void> {
72
+ this.running = false;
73
+ if (this.reconnectTimer) {
74
+ clearTimeout(this.reconnectTimer);
75
+ this.reconnectTimer = undefined;
76
+ }
77
+ if (this.client) {
78
+ try {
79
+ await this.client.end();
80
+ } catch { /* ignore close errors */ }
81
+ this.client = undefined;
82
+ }
83
+ }
84
+
85
+ private async connect({ initial = false }: { initial?: boolean } = {}): Promise<void> {
86
+ const { connectionString, channel, onPayload, logLabel } = this.options;
87
+ try {
88
+ const client = new PgClient({ connectionString });
89
+
90
+ client.on("error", (err) => {
91
+ logger.error(`❌ ${logLabel} LISTEN client error`, { detail: err.message });
92
+ this.scheduleReconnect();
93
+ });
94
+
95
+ client.on("end", () => {
96
+ if (this.running) {
97
+ logger.warn(`⚠️ ${logLabel} LISTEN client disconnected unexpectedly.`);
98
+ this.scheduleReconnect();
99
+ }
100
+ });
101
+
102
+ client.on("notification", (msg) => {
103
+ if (!msg.payload) return;
104
+ // A handler rejection must never surface as an unhandled
105
+ // rejection inside the pg client's event emitter.
106
+ Promise.resolve(onPayload(msg.payload)).catch((err) =>
107
+ logger.error(`❌ ${logLabel} Error handling notification`, { error: err })
108
+ );
109
+ });
110
+
111
+ await client.connect();
112
+ await client.query(`LISTEN ${channel}`);
113
+ this.client = client;
114
+ logger.info(`📡 ${logLabel} Listening on channel "${channel}".`);
115
+ } catch (err) {
116
+ // Surface the initial failure so callers can choose to fall back;
117
+ // for reconnects, keep retrying quietly in the background.
118
+ if (initial) throw err;
119
+ logger.error(`❌ ${logLabel} Failed to connect LISTEN client`, { error: err });
120
+ this.scheduleReconnect();
121
+ }
122
+ }
123
+
124
+ private scheduleReconnect(): void {
125
+ if (!this.running || this.reconnectTimer) return;
126
+
127
+ this.reconnectTimer = setTimeout(async () => {
128
+ this.reconnectTimer = undefined;
129
+ if (!this.running) return;
130
+ if (this.client) {
131
+ try { await this.client.end(); } catch { /* ignore */ }
132
+ this.client = undefined;
133
+ }
134
+ await this.connect();
135
+ }, this.options.reconnectDelayMs ?? DEFAULT_RECONNECT_DELAY_MS);
136
+ }
137
+ }