@rebasepro/server-postgres 0.23.0 → 0.24.1-canary.g56af4a4

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 (180) hide show
  1. package/dist/{BranchService-DGPL6G_C.js → BranchService-Bsn2wfaj.js} +36 -18
  2. package/dist/BranchService-Bsn2wfaj.js.map +1 -0
  3. package/dist/PostgresBackendDriver.d.ts +166 -13
  4. package/dist/auth/ensure-tables.d.ts +12 -0
  5. package/dist/auth/services.d.ts +78 -14
  6. package/dist/{auth-users-columns-D2LBFrMH.js → auth-users-columns-C72EMoDJ.js} +17 -1
  7. package/dist/{auth-users-columns-D2LBFrMH.js.map → auth-users-columns-C72EMoDJ.js.map} +1 -1
  8. package/dist/backup/backup-cli.d.ts +14 -0
  9. package/dist/backup/backup-service.d.ts +2 -2
  10. package/dist/backup/pg-binary.d.ts +5 -0
  11. package/dist/{backup-cli-24v4OlSp.js → backup-cli-aUfRv16h.js} +37 -29
  12. package/dist/backup-cli-aUfRv16h.js.map +1 -0
  13. package/dist/{backup-service-HQ9GC4tN.js → backup-service-BTBB7RXn.js} +60 -28
  14. package/dist/backup-service-BTBB7RXn.js.map +1 -0
  15. package/dist/boot-ddl-DVpsFN6r.js +105 -0
  16. package/dist/boot-ddl-DVpsFN6r.js.map +1 -0
  17. package/dist/cli-errors-CaI4ntyD.js +1919 -0
  18. package/dist/cli-errors-CaI4ntyD.js.map +1 -0
  19. package/dist/cli-errors.d.ts +93 -5
  20. package/dist/{cli-flags-BglvpjHv.js → cli-flags-B-Dawocl.js} +5 -2
  21. package/dist/cli-flags-B-Dawocl.js.map +1 -0
  22. package/dist/cli-helpers.d.ts +52 -12
  23. package/dist/cli-output.d.ts +16 -28
  24. package/dist/cli-push.d.ts +87 -0
  25. package/dist/cli.js +625 -190
  26. package/dist/cli.js.map +1 -1
  27. package/dist/{column-plan-helpers-DF-8dTVa.js → column-plan-helpers-drZIsSNf.js} +42 -26
  28. package/dist/column-plan-helpers-drZIsSNf.js.map +1 -0
  29. package/dist/{databasePoolManager-Bj5FbeAs.js → connection-BPoNILnD.js} +19 -110
  30. package/dist/connection-BPoNILnD.js.map +1 -0
  31. package/dist/connection.d.ts +9 -1
  32. package/dist/data-transformer.d.ts +0 -8
  33. package/dist/databasePoolManager-BkEy2w18.js +119 -0
  34. package/dist/databasePoolManager-BkEy2w18.js.map +1 -0
  35. package/dist/deferred-index-build-DP1_SNFo.js +1051 -0
  36. package/dist/deferred-index-build-DP1_SNFo.js.map +1 -0
  37. package/dist/{doctor-Cb2thZ8s.js → doctor-C4WiftHm.js} +279 -87
  38. package/dist/doctor-C4WiftHm.js.map +1 -0
  39. package/dist/ensure-collection-policies-CXpbeNag.js +541 -0
  40. package/dist/ensure-collection-policies-CXpbeNag.js.map +1 -0
  41. package/dist/ensure-collection-tables-BhsU6qUE.js +532 -0
  42. package/dist/ensure-collection-tables-BhsU6qUE.js.map +1 -0
  43. package/dist/{ensure-tables-Dhn9KM3B.js → ensure-tables-CHsgyLqg.js} +178 -75
  44. package/dist/ensure-tables-CHsgyLqg.js.map +1 -0
  45. package/dist/enum-labels-kept-D8fI1MoX.js +56 -0
  46. package/dist/enum-labels-kept-D8fI1MoX.js.map +1 -0
  47. package/dist/generate-drizzle-schema-Bz52jH2h.js +57 -0
  48. package/dist/generate-drizzle-schema-Bz52jH2h.js.map +1 -0
  49. package/dist/{generate-drizzle-schema-logic-BfzK7UQd.js → generate-drizzle-schema-logic-B74WN37S.js} +28 -10
  50. package/dist/generate-drizzle-schema-logic-B74WN37S.js.map +1 -0
  51. package/dist/generate-files-CdEckX6c.js +199 -0
  52. package/dist/generate-files-CdEckX6c.js.map +1 -0
  53. package/dist/generate-postgres-ddl-logic-B64O2oZF.js +152 -0
  54. package/dist/generate-postgres-ddl-logic-B64O2oZF.js.map +1 -0
  55. package/dist/generated-sql.d.ts +34 -0
  56. package/dist/history/HistoryService.d.ts +38 -6
  57. package/dist/history/ensure-history-table.d.ts +42 -4
  58. package/dist/index.es.js +5557 -7612
  59. package/dist/index.es.js.map +1 -1
  60. package/dist/{introspect-db-logic-WMuAfxvw.js → introspect-db-logic-C-Di8dA9.js} +562 -350
  61. package/dist/introspect-db-logic-C-Di8dA9.js.map +1 -0
  62. package/dist/introspect-db-queries-D6kAqReI.js +339 -0
  63. package/dist/introspect-db-queries-D6kAqReI.js.map +1 -0
  64. package/dist/pg-error-utils-vJezlXdI.js +388 -0
  65. package/dist/pg-error-utils-vJezlXdI.js.map +1 -0
  66. package/dist/{plan-schema-C0fxM8dY.js → plan-schema-B1YF_tYq.js} +278 -533
  67. package/dist/plan-schema-B1YF_tYq.js.map +1 -0
  68. package/dist/{policy-drift-DljYdrpW.js → policy-drift-Dm8i-s1J.js} +24 -7
  69. package/dist/policy-drift-Dm8i-s1J.js.map +1 -0
  70. package/dist/push-index-prebuild-Db4AQF4i.js +45 -0
  71. package/dist/push-index-prebuild-Db4AQF4i.js.map +1 -0
  72. package/dist/push-legacy-renames-BK6ilQq2.js +34 -0
  73. package/dist/push-legacy-renames-BK6ilQq2.js.map +1 -0
  74. package/dist/{generate-postgres-ddl-logic-Bt2d2mRH.js → render-ddl-CtC3y9Ol.js} +17 -151
  75. package/dist/render-ddl-CtC3y9Ol.js.map +1 -0
  76. package/dist/{rls-bootstrap-sql-B8EclDyM.js → rls-bootstrap-sql-RQUljnJ1.js} +125 -1046
  77. package/dist/rls-bootstrap-sql-RQUljnJ1.js.map +1 -0
  78. package/dist/{rls-enforcement-C6Xk0lA6.js → rls-enforcement-DvzqlaL1.js} +137 -17
  79. package/dist/rls-enforcement-DvzqlaL1.js.map +1 -0
  80. package/dist/schema/auth-schema.d.ts +172 -2
  81. package/dist/schema/boot-ddl.d.ts +47 -0
  82. package/dist/schema/classify-change.d.ts +29 -1
  83. package/dist/schema/collection-index.d.ts +45 -0
  84. package/dist/schema/column-plan-helpers.d.ts +38 -21
  85. package/dist/schema/deferred-index-build.d.ts +131 -0
  86. package/dist/schema/destructive-sql.d.ts +27 -1
  87. package/dist/schema/doctor-cli.js +110 -6
  88. package/dist/schema/doctor-cli.js.map +1 -1
  89. package/dist/schema/doctor-search-scale.d.ts +45 -0
  90. package/dist/schema/doctor.d.ts +53 -1
  91. package/dist/schema/ensure-collection-policies.d.ts +95 -35
  92. package/dist/schema/ensure-collection-tables.d.ts +56 -3
  93. package/dist/schema/enum-labels-kept.d.ts +26 -0
  94. package/dist/schema/generate-drizzle-schema.js +1 -1
  95. package/dist/schema/generate-files.d.ts +44 -0
  96. package/dist/schema/generate-next-step.d.ts +0 -13
  97. package/dist/schema/generate-postgres-ddl-logic.d.ts +5 -5
  98. package/dist/schema/generate-postgres-ddl.js +10 -81
  99. package/dist/schema/generate-postgres-ddl.js.map +1 -1
  100. package/dist/schema/generate-schema-commit.d.ts +12 -0
  101. package/dist/schema/introspect-db-logic.d.ts +43 -0
  102. package/dist/schema/introspect-db-queries.d.ts +12 -2
  103. package/dist/schema/introspect-db-search.d.ts +22 -0
  104. package/dist/schema/introspect-db-storage.d.ts +83 -0
  105. package/dist/schema/introspect-db-structure.d.ts +10 -0
  106. package/dist/schema/introspect-db.js +17 -6
  107. package/dist/schema/introspect-db.js.map +1 -1
  108. package/dist/schema/migration-post-process.d.ts +34 -0
  109. package/dist/schema/plan/diff-plan.d.ts +127 -1
  110. package/dist/schema/plan/plan-schema.d.ts +16 -8
  111. package/dist/schema/plan/render-ddl.d.ts +6 -0
  112. package/dist/schema/plan/types.d.ts +45 -11
  113. package/dist/schema/push-index-prebuild.d.ts +25 -0
  114. package/dist/schema/push-legacy-renames.d.ts +20 -0
  115. package/dist/schema/rls-bootstrap-sql.d.ts +15 -0
  116. package/dist/search-column-BM-GV6vH.js +442 -0
  117. package/dist/search-column-BM-GV6vH.js.map +1 -0
  118. package/dist/security/policy-drift.d.ts +1 -1
  119. package/dist/security/rls-enforcement.d.ts +39 -4
  120. package/dist/services/BranchService.d.ts +22 -1
  121. package/dist/services/FetchService.d.ts +63 -12
  122. package/dist/services/PersistService.d.ts +15 -0
  123. package/dist/services/RelationService.d.ts +106 -3
  124. package/dist/services/cdc/CdcListener.d.ts +26 -10
  125. package/dist/services/cdc/identity-columns.d.ts +19 -0
  126. package/dist/services/cdc/trigger-cdc.d.ts +180 -18
  127. package/dist/services/channel-bus/PostgresChannelBus.d.ts +12 -7
  128. package/dist/services/channel-history.d.ts +32 -5
  129. package/dist/services/collection-helpers.d.ts +26 -1
  130. package/dist/services/field-viewer.d.ts +15 -1
  131. package/dist/services/pg-notify-listener.d.ts +95 -6
  132. package/dist/services/realtimeService.d.ts +467 -53
  133. package/dist/services/row-pipeline.d.ts +16 -9
  134. package/dist/services/socket-liveness.d.ts +85 -0
  135. package/dist/services/soft-delete.d.ts +12 -0
  136. package/dist/services/sql-script.d.ts +275 -0
  137. package/dist/services/vector-iterative-scan.d.ts +40 -0
  138. package/dist/services/write-depth.d.ts +22 -0
  139. package/dist/services/write-transaction-scope.d.ts +15 -3
  140. package/dist/socket-liveness-D3UD5_BW.js +7570 -0
  141. package/dist/socket-liveness-D3UD5_BW.js.map +1 -0
  142. package/dist/socket-opt-ins.d.ts +64 -0
  143. package/dist/source-DBDbBTL4.js +811 -0
  144. package/dist/source-DBDbBTL4.js.map +1 -0
  145. package/dist/tsx-loader.d.ts +14 -0
  146. package/dist/utils/drizzle-conditions.d.ts +63 -3
  147. package/dist/utils/error-cause.d.ts +7 -0
  148. package/dist/utils/pg-error-utils.d.ts +2 -4
  149. package/dist/utils/result-rows.d.ts +10 -0
  150. package/dist/websocket.d.ts +63 -18
  151. package/package.json +7 -7
  152. package/dist/BranchService-DGPL6G_C.js.map +0 -1
  153. package/dist/backup-cli-24v4OlSp.js.map +0 -1
  154. package/dist/backup-service-HQ9GC4tN.js.map +0 -1
  155. package/dist/cli-errors-B8qHg02P.js +0 -798
  156. package/dist/cli-errors-B8qHg02P.js.map +0 -1
  157. package/dist/cli-flags-BglvpjHv.js.map +0 -1
  158. package/dist/cli-output-CNdMql-L.js +0 -47
  159. package/dist/cli-output-CNdMql-L.js.map +0 -1
  160. package/dist/collection-index-DdnsxiJ_.js +0 -286
  161. package/dist/collection-index-DdnsxiJ_.js.map +0 -1
  162. package/dist/column-plan-helpers-DF-8dTVa.js.map +0 -1
  163. package/dist/databasePoolManager-Bj5FbeAs.js.map +0 -1
  164. package/dist/doctor-Cb2thZ8s.js.map +0 -1
  165. package/dist/ensure-collection-policies-RHUcEp4v.js +0 -105
  166. package/dist/ensure-collection-policies-RHUcEp4v.js.map +0 -1
  167. package/dist/ensure-collection-tables-BEEjn5cn.js +0 -857
  168. package/dist/ensure-collection-tables-BEEjn5cn.js.map +0 -1
  169. package/dist/ensure-tables-Dhn9KM3B.js.map +0 -1
  170. package/dist/generate-drizzle-schema-logic-BfzK7UQd.js.map +0 -1
  171. package/dist/generate-drizzle-schema-yzY_BLhr.js +0 -123
  172. package/dist/generate-drizzle-schema-yzY_BLhr.js.map +0 -1
  173. package/dist/generate-postgres-ddl-logic-Bt2d2mRH.js.map +0 -1
  174. package/dist/introspect-db-logic-WMuAfxvw.js.map +0 -1
  175. package/dist/plan-schema-C0fxM8dY.js.map +0 -1
  176. package/dist/policy-drift-DljYdrpW.js.map +0 -1
  177. package/dist/rls-bootstrap-sql-B8EclDyM.js.map +0 -1
  178. package/dist/rls-enforcement-C6Xk0lA6.js.map +0 -1
  179. package/dist/source-Br7L7GOI.js +0 -410
  180. package/dist/source-Br7L7GOI.js.map +0 -1
@@ -5,7 +5,7 @@ import { NodePgDatabase } from "drizzle-orm/node-postgres";
5
5
  import { RealtimeProvider, CollectionSubscriptionConfig, SingleSubscriptionConfig } from "../interfaces.js";
6
6
  import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry.js";
7
7
  import { ChannelBus } from "./channel-bus/index.js";
8
- import type { ChannelRetentionRule } from "@rebasepro/types";
8
+ import type { ChannelRetentionRule, RealtimeListenerHealth } from "@rebasepro/types";
9
9
  /**
10
10
  * Auth context stored per-subscription so real-time refetches respect RLS.
11
11
  * Mirrors the session variables set by PostgresBackendDriver.withAuth().
@@ -95,6 +95,13 @@ type StoredCollectionRequest = {
95
95
  fields?: string[];
96
96
  /** `SELECT DISTINCT` over the projection. */
97
97
  distinct?: boolean;
98
+ /**
99
+ * A subscription to the count alone (`listenCount`): no rows are read,
100
+ * and each frame carries `rows: []` and the `meta` with the total. A
101
+ * server that predates it reads the rows it was asked for (the SDK asks
102
+ * for one) and counts them as usual, so the same frame answers it.
103
+ */
104
+ countOnly?: boolean;
98
105
  };
99
106
  /**
100
107
  * What an in-process listener asks for: the read, and who it reads as.
@@ -111,6 +118,7 @@ export type DataDriverSubscriptionRequest = {
111
118
  } | {
112
119
  type: "single";
113
120
  id: string | number;
121
+ include?: IncludeSpec;
114
122
  });
115
123
  /**
116
124
  * A registered subscription, plus the two counters that order its deliveries.
@@ -133,11 +141,22 @@ export type DataDriverSubscriptionRequest = {
133
141
  * last delivery *started* the last one *delivered*.
134
142
  */
135
143
  type Subscription = {
144
+ /** The id the client named it by — what every frame for it carries. */
145
+ subscriptionId: string;
136
146
  clientId: string;
147
+ /** The {@link SubscriptionGroup} whose refetch answers it. */
148
+ groupKey: string;
137
149
  type: "collection" | "single";
138
150
  path: string;
139
151
  id?: string | number;
140
152
  collectionRequest?: StoredCollectionRequest;
153
+ /**
154
+ * The relations a single-row subscription loads into every frame — the
155
+ * `include` a `findById` takes. A list keeps its own in `collectionRequest`.
156
+ */
157
+ include?: IncludeSpec;
158
+ /** A single-row subscriber that asked for the row's `ETag` on every frame. */
159
+ etag?: boolean;
141
160
  authContext?: SubscriptionAuthContext;
142
161
  /**
143
162
  * An in-process listener's error callback, called when a refetch fails.
@@ -149,7 +168,65 @@ type Subscription = {
149
168
  started: number;
150
169
  /** The highest started-sequence that has already reached the subscriber. */
151
170
  delivered: number;
171
+ /**
172
+ * A socket subscriber's last data frame, as {@link frameDigest} of what
173
+ * follows its subscription id; unset before the first and after an error
174
+ * frame. A refetch that reads the same rows sends it nothing — see
175
+ * {@link sendDataFrame}.
176
+ */
177
+ lastFrame?: string;
152
178
  };
179
+ /**
180
+ * How many group refetches may run at once: a quarter of the connection pool
181
+ * the service shares with every REST request, and never fewer than two.
182
+ *
183
+ * One write to a list watched by many different principals arms a refetch
184
+ * per principal, and they all came due in the same tick. Started together,
185
+ * 200 of them were 200 transactions queued on a 20-connection pool, and an
186
+ * unrelated request arriving meanwhile waited behind every one (measured: a
187
+ * `SELECT 1` took 521 ms). Capped, the refetches queue here instead, and
188
+ * three quarters of the pool stays free for the requests. A pool this cannot
189
+ * read (a test double, a client that is not a `pg` pool) is taken to be the
190
+ * default 20-connection one.
191
+ */
192
+ export declare function refetchConcurrencyFor(db: object): number;
193
+ /**
194
+ * The default ceiling on subscriptions one socket may hold.
195
+ *
196
+ * Each collection subscription is a refetch on every write to its collection;
197
+ * without a ceiling one socket could open tens of thousands and turn every
198
+ * write anyone made into as many transactions. The SDK shares identical
199
+ * subscriptions on a socket, so a real page holds one per distinct list or
200
+ * record on screen — the admin panel's tables, forms and reference previews
201
+ * stay well under this. See `REALTIME_MAX_SUBSCRIPTIONS_PER_SOCKET`.
202
+ */
203
+ export declare const DEFAULT_MAX_SUBSCRIPTIONS_PER_SOCKET = 1000;
204
+ /**
205
+ * Read a subscription ceiling, refusing anything that is not a positive whole
206
+ * number. A typo here does not fall back to a default: the ceiling is a limit
207
+ * someone chose, and silently running with another one is how it stops
208
+ * meaning anything.
209
+ */
210
+ export declare function parseMaxSubscriptionsPerSocket(value: unknown, source: string): number;
211
+ /**
212
+ * The smallest last total at which a group's count is reused under
213
+ * `REALTIME_RECOUNT_INTERVAL_MS`, unless `REALTIME_RECOUNT_MIN_ROWS` says
214
+ * otherwise. Below it a count is cheap, and stays exact.
215
+ */
216
+ export declare const DEFAULT_RECOUNT_MIN_ROWS = 100000;
217
+ /**
218
+ * Read one of the realtime tuning numbers (`REALTIME_REFETCH_MAX_WAIT_MS`,
219
+ * `REALTIME_RECOUNT_INTERVAL_MS`, `REALTIME_RECOUNT_MIN_ROWS` and their
220
+ * `realtime.*` options). A value someone set and mistyped stops the boot
221
+ * rather than quietly becoming the default.
222
+ */
223
+ export declare function parseRealtimeTuning(value: unknown, source: string, unit: string): number;
224
+ /**
225
+ * Read `REALTIME_MAX_SOCKETS_PER_USER` / `realtime.maxSocketsPerUser`. Like the
226
+ * subscription ceiling, a value someone set and mistyped stops the boot rather
227
+ * than quietly becoming no ceiling.
228
+ */
229
+ export declare function parseMaxSocketsPerUser(value: unknown, source: string): number;
153
230
  /**
154
231
  * PostgreSQL-specific realtime service.
155
232
  * Handles WebSocket connections and subscriptions for real-time row updates.
@@ -166,8 +243,19 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
166
243
  */
167
244
  readonly supportsChannels = true;
168
245
  private clients;
246
+ /** What each socket opted into when it opened — see `socket-opt-ins.ts`. */
247
+ private clientOptIns;
169
248
  private channels;
170
249
  private presence;
250
+ /**
251
+ * The reverse of {@link channels} and {@link presence}: the channels each
252
+ * client is a member of, and those it has a presence entry in. Kept in
253
+ * step wherever either map gains or loses a client, so a disconnect visits
254
+ * what the client was in. It used to walk every channel on the instance:
255
+ * with a channel per document, 5.4 ms per disconnect at 50k channels.
256
+ */
257
+ private channelsByClient;
258
+ private presenceByClient;
171
259
  /**
172
260
  * Ordered, replayable history for channels that opt into it.
173
261
  *
@@ -188,6 +276,8 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
188
276
  * wait on each other.
189
277
  */
190
278
  private channelSendQueues;
279
+ /** The receiving half of the same ordering: retained bus frames per channel, one at a time. */
280
+ private channelReceiveQueues;
191
281
  /**
192
282
  * Cross-instance transport for channel frames and presence.
193
283
  *
@@ -206,9 +296,17 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
206
296
  private presenceStore?;
207
297
  /** Sweeps roster rows left behind by instances that stopped heartbeating. */
208
298
  private presenceSweepInterval?;
299
+ /**
300
+ * The clients that have tracked presence through this instance — the only
301
+ * ones with roster rows to delete when their socket closes. Most sockets
302
+ * never track presence, and each of their closes was a `DELETE` too.
303
+ */
304
+ private presenceTrackers;
209
305
  /**
210
306
  * Channels whose oversized ephemeral broadcasts have already been reported,
211
307
  * so a hot channel logs the problem once rather than once per message.
308
+ * Forgotten with the channel, so it holds live channels only — not every
309
+ * name a client ever broadcast on.
212
310
  */
213
311
  private oversizedBroadcastWarned;
214
312
  /**
@@ -228,34 +326,135 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
228
326
  private foreignInstanceSeen;
229
327
  /** So the multi-pod memory-bus warning is emitted once, not once per join. */
230
328
  private memoryBusWarned;
329
+ /** When the database was last asked whether another instance listens — see {@link warnIfMemoryBusOnMultiplePods}. */
330
+ private otherInstancesCheckedAt?;
331
+ /** The check in flight, so a burst of joins asks once. Awaited by tests. */
332
+ private otherInstancesCheck?;
333
+ private static readonly OTHER_INSTANCES_CHECK_INTERVAL_MS;
231
334
  private presenceInterval?;
232
335
  private static readonly PRESENCE_TIMEOUT_MS;
233
336
  /** How often stale roster rows from other instances are reaped. */
234
337
  private static readonly PRESENCE_SWEEP_INTERVAL_MS;
235
338
  private dataService;
339
+ /**
340
+ * Every subscription, by {@link subscriptionKey} — the client's own id
341
+ * qualified by the client that chose it.
342
+ *
343
+ * Keyed by the bare id, this was one namespace for every socket: two raw
344
+ * clients that both counted from `"sub-1"` replaced each other's
345
+ * subscription without a word, and either one's `unsubscribe` ended the
346
+ * other's.
347
+ */
236
348
  private _subscriptions;
349
+ /** In-process listeners' callbacks, by subscription key. */
237
350
  private subscriptionCallbacks;
351
+ /** Each client's subscriptions, by key — for its ceiling and its disconnect. */
352
+ private subscriptionsByClient;
353
+ /**
354
+ * The in-process subscriptions — started by `startDataDriverSubscription`,
355
+ * `subscribeToCollection` or `subscribeToOne` — by the id their caller
356
+ * gave them, for {@link unsubscribe}, which has nothing else to go on. It
357
+ * used to walk every subscription on the server, sockets' included.
358
+ */
359
+ private inProcessKeysById;
360
+ /** Subscriptions one refetch answers — see {@link SubscriptionGroup}. */
361
+ private groups;
362
+ /**
363
+ * Groups whose refetch is due and waiting for a slot, in the order they
364
+ * came due. A group is in it at most once; changes that arrive while it
365
+ * waits join the read it is waiting for, which starts after them.
366
+ */
367
+ private refetchQueue;
368
+ /** Group refetches running now — at most {@link maxConcurrentRefetches}. */
369
+ private refetchesRunning;
370
+ /** See {@link refetchConcurrencyFor}. */
371
+ maxConcurrentRefetches: number;
372
+ /**
373
+ * The collection groups on each path, and the single-row groups at each
374
+ * `path` + id: what a change looks up, instead of walking every
375
+ * subscription on the server once per changed row.
376
+ */
377
+ private collectionGroupsByPath;
378
+ private singleGroupsByAddress;
379
+ /** The single-row groups on each path, for a change that names no row ({@link ANY_ROW}). */
380
+ private singleGroupsByPath;
381
+ /**
382
+ * When something on each path a list group reads last changed: the
383
+ * debounce clock those groups share, moved once per changed row instead
384
+ * of once per row per group. See {@link notifyPathUpdate}.
385
+ */
386
+ private listPathChangedAt;
387
+ /**
388
+ * The list groups on each path that a change still has to schedule: new
389
+ * ones, and those whose last refetch has started. The rest already have a
390
+ * refetch for every member pending, which the next change only has to
391
+ * keep waiting — the clock above does that.
392
+ */
393
+ private unscheduledListGroups;
394
+ /**
395
+ * Every path a subscription holds, with the collection it lands on and how
396
+ * many hold it — the candidates `aliasPaths` considers, resolved once.
397
+ */
398
+ private subscribedPaths;
399
+ /**
400
+ * How many subscriptions one socket may hold; set from config at boot. See
401
+ * {@link DEFAULT_MAX_SUBSCRIPTIONS_PER_SOCKET}.
402
+ */
403
+ maxSubscriptionsPerSocket: number;
404
+ /**
405
+ * The longest a change waits for its refetch, in ms; set from config at
406
+ * boot (`REALTIME_REFETCH_MAX_WAIT_MS`). See {@link REFETCH_MAX_WAIT_MS},
407
+ * the default.
408
+ */
409
+ refetchMaxWaitMs: number;
410
+ /**
411
+ * How long a list's total may be reused before it is counted again, in ms
412
+ * (`REALTIME_RECOUNT_INTERVAL_MS`). Unset, the default, counts with every
413
+ * frame. Only for a group whose last total was at least
414
+ * {@link recountMinRows}: a count is cheap on a small set, and exact there.
415
+ */
416
+ recountIntervalMs: number | undefined;
417
+ /** See {@link recountIntervalMs} (`REALTIME_RECOUNT_MIN_ROWS`). */
418
+ recountMinRows: number;
238
419
  private driver?;
239
420
  /** Unique identifier for this process instance, used to skip own notifications. */
240
421
  private readonly instanceId;
241
- /** Dedicated pg.Client for LISTEN (outside the Drizzle pool). */
242
- private listenClient?;
243
- /** Connection string used for reconnecting the LISTEN client. */
244
- private listenConnectionString?;
422
+ /** What this instance's LISTEN connections report in `pg_stat_activity`. */
423
+ private readonly listenerApplicationName;
424
+ /** The LISTEN connection on {@link PG_NOTIFY_CHANNEL}, outside the Drizzle pool. */
425
+ private crossInstanceListener?;
245
426
  /** Whether cross-instance broadcasting is active. */
246
427
  private broadcasting;
247
- /** Reconnection timer handle. */
248
- private reconnectTimer?;
249
- /** Debounce timers for collection refetches to prevent refetch storms. */
250
- private refetchTimers;
428
+ /**
429
+ * Changes waiting to be announced on {@link PG_NOTIFY_CHANNEL}, and the
430
+ * send that will announce them — see {@link broadcastChange}.
431
+ */
432
+ private pendingBroadcasts;
433
+ private broadcastFlush?;
251
434
  /** Debounce window (ms) for coalescing rapid row updates into a single correctness refetch. */
252
435
  private static readonly REFETCH_DEBOUNCE_MS;
436
+ /**
437
+ * The longest a change waits for its refetch, quiet window or not.
438
+ *
439
+ * A pure debounce waits for 300 ms without a change, and a collection
440
+ * written more often than that — an orders, events or chat table at a few
441
+ * writes a second — never went quiet: its subscribers received nothing for
442
+ * as long as the writes continued. Under a steady stream a group now
443
+ * refetches once per this interval, which is the whole cost.
444
+ */
445
+ private static readonly REFETCH_MAX_WAIT_MS;
253
446
  /** Dedicated LISTEN client for DB-level change events (undefined unless CDC is enabled). */
254
447
  private cdcListener?;
255
448
  /** Whether database-level CDC is the active cross-instance change source. */
256
449
  private cdcActive;
257
450
  /** Junction table → the child lists its rows belong to, built when CDC starts. */
258
451
  private junctionLinkMap?;
452
+ /** Changes waiting for a turn of the event loop — see {@link handleCdcEvents}. */
453
+ private cdcBacklog;
454
+ /** Changes handled in the current turn. */
455
+ private cdcTurnRows;
456
+ /** How many changed rows one turn of the event loop routes: ~5–10 ms of work. */
457
+ private static readonly CDC_ROWS_PER_TURN;
259
458
  /** Reverse lookup: `schema.table` (and bare `table`) → collection, built when CDC starts. */
260
459
  private cdcTableMap?;
261
460
  /**
@@ -300,8 +499,75 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
300
499
  * redaction lives.
301
500
  */
302
501
  private debugLog;
502
+ /**
503
+ * {@link rawQueryLoggingEnabled}, read at most once a second.
504
+ *
505
+ * It reads two environment variables, and `process.env` is a getenv per
506
+ * read: called three or four times per changed row, it was the largest
507
+ * cost of a change notification here — 1.4 µs of every ~3.5 µs per row,
508
+ * so ~1.4 s of event loop for a 1M-row statement. A switch flipped at run
509
+ * time is honoured within a second.
510
+ */
511
+ private debugEnabled;
512
+ private debugFlag;
513
+ private debugCheckedAt;
303
514
  setDataDriver(driver: DataDriver): void;
304
- get subscriptions(): Map<string, Subscription>;
515
+ /**
516
+ * The live subscriptions by the id their client gave them — for tests and
517
+ * diagnostics. Two clients may use one id; this view shows the last.
518
+ */
519
+ get subscriptions(): ReadonlyMap<string, Subscription>;
520
+ /** The internal key of `subscriptionId` as `clientId` named it. */
521
+ private subscriptionKey;
522
+ /**
523
+ * The group a subscription's refetch belongs to. Socket subscriptions that
524
+ * would run the same read as the same principal share one; an in-process
525
+ * listener is always alone — see {@link SubscriptionGroup}.
526
+ */
527
+ private groupKeyFor;
528
+ /**
529
+ * Register (or replace) a subscription under `key`, in its group and in
530
+ * every index that finds it.
531
+ */
532
+ private registerSubscription;
533
+ /** Remember an in-process subscription by its caller's id — see {@link inProcessKeysById}. */
534
+ private indexInProcess;
535
+ /** Remove the subscription under `key` from everything that holds it. */
536
+ private dropSubscription;
537
+ /** Take `subscription` out of its group and path count; a group left empty goes, timer and all. */
538
+ private leaveGroup;
539
+ /** See {@link unscheduledListGroups}. */
540
+ private markListGroupUnscheduled;
541
+ /**
542
+ * Ask for a refetch of `group` once changes have been quiet for the
543
+ * debounce window — for every member, or only for `member` — and in any
544
+ * case no later than {@link REFETCH_MAX_WAIT_MS} after the first of them.
545
+ *
546
+ * The window is measured from the last change, as a debounce is, but the
547
+ * timer is not re-armed per change: a 10k-row statement is 10k changes,
548
+ * and re-arming a timer per change per group was work in proportion to
549
+ * both. A change only moves the timestamp; the timer, when it fires early,
550
+ * waits out the rest.
551
+ */
552
+ private scheduleGroupRefetch;
553
+ private armGroupTimer;
554
+ /**
555
+ * Start queued group refetches while fewer than
556
+ * {@link maxConcurrentRefetches} are running. Each one that settles starts
557
+ * the next, so the fan-out of one write waits its turn here rather than
558
+ * in the pool, ahead of every REST request.
559
+ */
560
+ private drainRefetchQueue;
561
+ /**
562
+ * One read for the group, delivered to each member it is for.
563
+ *
564
+ * Each member still claims its own delivery slot before the read and
565
+ * checks it after (see {@link beginDelivery}), so a member that left, was
566
+ * replaced or was overtaken while the read ran is skipped exactly as it
567
+ * was when it had a read of its own. A socket frame is serialised once and
568
+ * only its subscription id differs per member.
569
+ */
570
+ private refetchGroup;
305
571
  /**
306
572
  * Claim a delivery slot for a subscription, before doing the work.
307
573
  *
@@ -355,13 +621,32 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
355
621
  */
356
622
  subscribeToOne(subscriptionId: string, config: SingleSubscriptionConfig, callback?: (row: Record<string, unknown> | null) => void, onError?: (error: unknown) => void): void;
357
623
  /**
358
- * Unsubscribe from a subscription (RealtimeProvider interface)
624
+ * Unsubscribe an in-process subscription (RealtimeProvider interface).
625
+ *
626
+ * By id alone, because the in-process callers hold nothing else — so a
627
+ * socket's subscription under the same id is not this one, and is left
628
+ * alone. A socket ends its own through the `unsubscribe` frame.
359
629
  */
360
630
  unsubscribe(subscriptionId: string): void;
361
631
  addClient(clientId: string, ws: WebSocket): void;
362
632
  handleClientMessage(clientId: string, message: WebSocketMessage, authContext?: SubscriptionAuthContext): Promise<void>;
633
+ /** Record what a socket opted into when it opened. See `socket-opt-ins.ts`. */
634
+ setClientOptIns(clientId: string, optIns: ReadonlySet<string>): void;
363
635
  removeClient(clientId: string): Promise<void>;
636
+ /** Record that `clientId` is in `channel`, in a reverse index — see {@link channelsByClient}. */
637
+ private indexClientChannel;
638
+ private unindexClientChannel;
364
639
  private handleMessage;
640
+ /**
641
+ * Refuse a new subscription past the socket's ceiling, and say so.
642
+ *
643
+ * Every collection subscription is a refetch on every write to its
644
+ * collection, so a socket allowed to open them without limit could turn
645
+ * each write anyone makes into tens of thousands of transactions. Replacing
646
+ * a subscription the socket already holds — the same id again — is not a
647
+ * new one and always passes. Returns whether the subscription may go ahead.
648
+ */
649
+ private admitSubscription;
365
650
  private handleCollectionSubscription;
366
651
  private handleEntitySubscription;
367
652
  /**
@@ -397,6 +682,10 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
397
682
  * again, when one is installed, and left when it refuses.
398
683
  */
399
684
  rescopeClient(clientId: string, authContext: SubscriptionAuthContext): Promise<void>;
685
+ /**
686
+ * End a socket's own subscription. Only its own: the id is the client's,
687
+ * so another socket's subscription under the same id is a different one.
688
+ */
400
689
  private handleUnsubscribe;
401
690
  /**
402
691
  * Enhanced notification method that handles nested relation updates.
@@ -446,15 +735,6 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
446
735
  * whether this subscriber may see the row at all.
447
736
  */
448
737
  private notifyPathUpdate;
449
- /**
450
- * Debounce a collection refetch for a WebSocket subscription.
451
- * Coalesces rapid row mutations into a single database query.
452
- */
453
- private debouncedCollectionRefetch;
454
- /**
455
- * Debounce a collection refetch for a DataDriver callback subscription.
456
- */
457
- private debouncedDriverRefetch;
458
738
  /**
459
739
  * Tell an in-process listener its refetch failed, through the slot the
460
740
  * rows would have used.
@@ -468,13 +748,20 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
468
748
  */
469
749
  private reportDriverFetchFailure;
470
750
  /**
471
- * Fetch a collection with optional RLS auth context.
751
+ * Fetch a collection with optional RLS auth context, and with `meta: true`
752
+ * the `meta` a socket frame carries beside the rows.
753
+ *
472
754
  * When authContext is provided, the fetch runs inside a transaction
473
- * with set_config calls so PostgreSQL RLS policies are enforced.
755
+ * with set_config calls so PostgreSQL RLS policies are enforced. The count
756
+ * behind `meta` runs in that same transaction — one pool acquisition and
757
+ * one `applyAuthContext` per refetch, where it used to take a second
758
+ * transaction of its own — see {@link collectionMeta}.
474
759
  */
475
760
  private fetchCollectionWithAuth;
476
761
  /**
477
- * The `meta` a `collection_update` frame carries beside its rows.
762
+ * The `meta` a `collection_update` frame carries beside its rows, counted
763
+ * through `service` — the subscriber's own transaction, read only, with
764
+ * its auth context and `beforeQuery` narrowing already bound.
478
765
  *
479
766
  * Frames used to carry rows and primary keys and nothing else, so the
480
767
  * client issued a `GET /count` **per push** to fill in a total it needed to
@@ -482,22 +769,61 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
482
769
  * write, per subscriber, forever. The refetch already knows the query; it
483
770
  * counts once, here, under the same RLS transaction that read the rows, so
484
771
  * the total describes the same set they came from.
772
+ *
773
+ * A first page that came back short of its limit *is* the whole set — the
774
+ * same filter, the same narrowing, read as the same principal — so its
775
+ * length is the total and nothing is counted. Not for a later page, a
776
+ * cursor page, a search (whose page and count are not the same query) or
777
+ * a `distinct` read (whose count counts rows, not values): those count.
485
778
  */
486
- private collectionMetaWithAuth;
487
- /**
488
- * Debounce an row refetch for a WebSocket subscription.
489
- */
490
- private debouncedSingleRefetch;
779
+ private collectionMeta;
491
780
  /**
492
- * Debounce an row refetch for a Driver callback subscription.
781
+ * A `single_update` frame's row, and — when the subscriber asked — the
782
+ * `ETag` a `GET /:id` of the same row answers.
783
+ *
784
+ * The tag names the row, not the read: `GET` hashes the row as
785
+ * `restFetchService.fetchOneForRest` serves it with no `include`, and a
786
+ * write's `If-Match` is compared against that same rendering. A frame
787
+ * whose relations are loaded therefore takes its tag from a second read
788
+ * without them, read first — as `GET /:id?include=` does — so a write
789
+ * landing between the two leaves the tag older than the body (a 412 the
790
+ * client recovers from) and never newer, which would let an edit made
791
+ * against the older body pass over a change it never saw.
493
792
  */
494
- private debouncedSingleDriverRefetch;
793
+ private fetchEntityFrame;
495
794
  /**
496
795
  * Fetch a single row with optional RLS auth context.
497
796
  */
498
797
  private fetchEntityWithAuth;
499
- private sendCollectionUpdate;
500
- private sendSingleUpdate;
798
+ /**
799
+ * Everything in a `collection_update` frame after its subscription id,
800
+ * serialised once for every member of a group: the rows, the collection's
801
+ * key columns, and the `meta` beside them (see {@link CollectionUpdateMeta}
802
+ * for the round trip that removes). The order is the one
803
+ * {@link CollectionUpdateMessage} declares.
804
+ */
805
+ private collectionFrameTail;
806
+ /** Everything in a `single_update` frame after its subscription id. */
807
+ private singleFrameTail;
808
+ /** A frame tail with its {@link frameDigest}, computed once for every member it goes to. */
809
+ private dataFrame;
810
+ /**
811
+ * What a subscriber was last sent, in 44 bytes: holding the frames
812
+ * themselves would keep a page of rows per subscriber in memory.
813
+ */
814
+ private frameDigest;
815
+ /**
816
+ * Send a data frame to a socket subscriber — unless it is exactly the one
817
+ * the subscriber was last sent.
818
+ *
819
+ * A write anywhere in a collection refetches every list of it, and the
820
+ * first 50 rows of a 10k-row list come back the same for 99.5% of writes.
821
+ * Each of those frames was sent anyway: ~30 KB per viewer per write,
822
+ * parsed, merged and re-rendered by every client to change nothing. The
823
+ * comparison is per subscriber, because the members of one group need not
824
+ * have been sent the same frame last — one may have joined since.
825
+ */
826
+ private sendDataFrame;
501
827
  /**
502
828
  * Send a lightweight row-level patch to a collection subscriber.
503
829
  * The client can merge this into its cached data for instant feedback.
@@ -524,6 +850,15 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
524
850
  */
525
851
  private sendError;
526
852
  private sendMessage;
853
+ /**
854
+ * Send an already-serialised frame to a client, if it is still connected.
855
+ *
856
+ * Every frame to a socket goes through here, so this is where a client
857
+ * that does not read what it is sent is let go of — see
858
+ * {@link terminateIfBacklogged}. Terminated, it closes like any other
859
+ * socket, and {@link removeClient} drops what it held.
860
+ */
861
+ private sendRaw;
527
862
  /**
528
863
  * Extract parent paths from a nested path like "posts/70/tags"
529
864
  * Returns ["posts", "posts/70"] for the example above
@@ -601,6 +936,21 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
601
936
  * fraction of the room. The evidence is already in the process, so use it.
602
937
  */
603
938
  private warnIfMemoryBusOnMultiplePods;
939
+ /**
940
+ * Ask the database whether another process holds a realtime LISTEN
941
+ * connection on it, at most once per {@link OTHER_INSTANCES_CHECK_INTERVAL_MS}.
942
+ *
943
+ * The legacy entity channel proves a second instance with every foreign
944
+ * `sid`, but under change capture — the default — nothing crosses that
945
+ * channel, so the warning above could never fire on exactly the
946
+ * deployments it was written for. Every LISTEN connection names itself
947
+ * (`application_name`, which `pg_stat_activity` shows to any role), so
948
+ * the count is one cheap query. Only a process that listens is asked: a
949
+ * single process with no LISTEN connection has nothing to compare with.
950
+ */
951
+ private checkForOtherInstances;
952
+ /** Drop a channel its last local member left, and what was kept per channel for it. */
953
+ private forgetChannel;
604
954
  /** Leave a broadcast channel */
605
955
  leaveChannel(clientId: string, channel: string): void;
606
956
  /**
@@ -668,6 +1018,7 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
668
1018
  * its own `sid`.
669
1019
  */
670
1020
  private handleBusFrame;
1021
+ private deliverBusFrame;
671
1022
  /**
672
1023
  * Install retention rules and create the tables they need.
673
1024
  *
@@ -757,8 +1108,16 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
757
1108
  * does not answer the close frame is dropped after a short grace.
758
1109
  */
759
1110
  destroy(): Promise<void>;
760
- /** Whether database-level change capture is currently the active source. */
1111
+ /**
1112
+ * Whether database-level change capture is the source and is listening.
1113
+ *
1114
+ * Configured is not enough: a CDC connection that went half-open used to
1115
+ * leave this `true` while nothing arrived. False while the connection is
1116
+ * down and being replaced.
1117
+ */
761
1118
  isCdcActive(): boolean;
1119
+ /** The LISTEN connections this service depends on — see `RealtimeProvider.health`. */
1120
+ health(): RealtimeListenerHealth[];
762
1121
  /**
763
1122
  * Enable database-level change capture as the realtime source.
764
1123
  *
@@ -802,13 +1161,26 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
802
1161
  /**
803
1162
  * Route a captured database change into the realtime pipeline.
804
1163
  *
805
- * Delivery is RLS-safe by construction: the raw tuple from the WAL/trigger is
806
- * NOT forwarded to subscribers. Instead the change is marked invalidated, so
1164
+ * Delivery is RLS-safe by construction: the event carries the changed row's
1165
+ * key and nothing else, and even that is NOT forwarded to subscribers. Instead the change is marked invalidated, so
807
1166
  * every matching subscription re-reads the row under its own auth context via
808
1167
  * {@link fetchCollectionWithAuth} / {@link fetchEntityWithAuth}. A subscriber
809
1168
  * therefore only ever receives rows its RLS policies permit — filtering is per
810
1169
  * subscriber, never per publisher.
811
1170
  */
1171
+ /**
1172
+ * Route the rows one notification names — a statement's batch, or one row.
1173
+ *
1174
+ * Each is handled as a change on its own (the app-emit de-duplication and
1175
+ * single-row addressing are per row), at most {@link CDC_ROWS_PER_TURN} in
1176
+ * one turn of the event loop; the rest wait in order for the next turns. A
1177
+ * socket read hands the listener up to 2 MB of notifications at once, and
1178
+ * batched that is ~250k rows: handled in one go, the event loop stalled
1179
+ * for ~400 ms. Sliced, a 1M-row statement is ~200 short turns that timers,
1180
+ * sockets and requests run between.
1181
+ */
1182
+ private handleCdcEvents;
1183
+ private drainCdcBacklog;
812
1184
  private handleCdcEvent;
813
1185
  /**
814
1186
  * Deliver a change on a many-to-many junction table as a change to the child
@@ -827,7 +1199,11 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
827
1199
  * Returns whether the table was recognised as a junction.
828
1200
  */
829
1201
  private handleJunctionCdcEvent;
830
- /** Compute the canonical (possibly composite) id string from a captured row. */
1202
+ /**
1203
+ * Compute the canonical (possibly composite) id string from a captured row,
1204
+ * or {@link ANY_ROW} when it names none — a key too large to notify, a
1205
+ * table with no key.
1206
+ */
831
1207
  private extractIdFromCdcRow;
832
1208
  /**
833
1209
  * A captured row with its key under the key's *field* names.
@@ -840,14 +1216,32 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
840
1216
  */
841
1217
  private cdcRowByKeyFields;
842
1218
  private dedupKey;
843
- /** Record that this instance just delivered `key` via the app path. */
1219
+ /**
1220
+ * Record that this instance just delivered `key` via the app path.
1221
+ *
1222
+ * Expired entries are purged so the map cannot grow unbounded under write
1223
+ * load — once it holds over 1,000, and at most once per dedup window. The
1224
+ * purge walks the whole map, and it used to run on every write past 1,000:
1225
+ * inside one window nothing has expired yet, so a burst of writes walked
1226
+ * all of it each time (703 ms of event loop for 20,000 marks). Once a
1227
+ * window, the map holds at most two windows' writes.
1228
+ */
844
1229
  private markAppEmit;
1230
+ /** When {@link markAppEmit} may next purge expired entries. */
1231
+ private nextAppEmitPurgeAt;
845
1232
  /** Consume a matching app-emit record if present and unexpired; true ⇒ suppress the CDC echo. */
846
1233
  private consumeAppEmit;
847
1234
  /**
848
1235
  * Enable cross-instance realtime broadcasting via Postgres LISTEN/NOTIFY.
849
- * Creates a dedicated pg.Client (outside the Drizzle pool) that stays
850
- * connected and listens for change notifications from other instances.
1236
+ * Listens on a dedicated connection (outside the Drizzle pool) for the
1237
+ * changes other instances publish, through a {@link PgNotifyListener}: it
1238
+ * proves itself with a heartbeat, is replaced when it drops or stops
1239
+ * answering, and every subscription is refetched once it is back, since
1240
+ * whatever was published meanwhile reached nobody here.
1241
+ *
1242
+ * A database that cannot be reached at boot does not fail this call: the
1243
+ * listener reports itself down (see {@link health}) and keeps dialling.
1244
+ * There is no other cross-instance path to fall back to.
851
1245
  *
852
1246
  * This is an **optional** feature — if never called, the backend operates
853
1247
  * in single-instance mode (the default, perfectly fine for most setups).
@@ -856,27 +1250,47 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
856
1250
  */
857
1251
  startListening(connectionString: string): Promise<void>;
858
1252
  /**
859
- * Stop listening and clean up the dedicated LISTEN connection.
1253
+ * Announce this process's writes on {@link PG_NOTIFY_CHANNEL} without
1254
+ * listening for anyone else's.
1255
+ *
1256
+ * For a process that serves no websockets (`worker`, `functions`) while
1257
+ * change capture is not in effect. It has no subscriber of its own, so it
1258
+ * holds no LISTEN connection — but without capture the announcement is
1259
+ * the only way the `api` hears that a cron or a function wrote a row.
1260
+ * Sending needs only the pool.
860
1261
  */
861
- stopListening(): Promise<void>;
1262
+ startPublishing(): void;
862
1263
  /**
863
- * Broadcast a change notification to other instances via pg_notify.
864
- * Uses the main Drizzle connection (pooled) — NOT the LISTEN client.
1264
+ * Stop listening and clean up the dedicated LISTEN connection.
865
1265
  */
866
- private broadcastChange;
1266
+ stopListening(): Promise<void>;
867
1267
  /**
868
- * Create and connect the dedicated LISTEN client with auto-reconnect.
1268
+ * Announce a change to the other instances on {@link PG_NOTIFY_CHANNEL},
1269
+ * with the rest of its burst. Resolves once the notification carrying it
1270
+ * has been sent. Uses the main Drizzle connection (pooled), not the LISTEN
1271
+ * client.
869
1272
  *
870
- * @param reconnect Set when the client is coming back from a drop: every
871
- * notification published in the gap is gone, so the subscriptions
872
- * are refetched once it is listening again.
873
- */
874
- private connectListenClient;
875
- /**
876
- * Schedule a reconnection attempt with a fixed 3s delay.
1273
+ * Changes queued in the same turn of the event loop go out together, as
1274
+ * many to a notification as fit under the NOTIFY ceiling: a 1,000-row save
1275
+ * used to be 1,000 round trips. A burst of one keeps the single-change
1276
+ * shape `{ sid, p, eid, db }`, which an older instance in a rolling deploy
1277
+ * still reads; a batch is `{ sid, c: [[p, eid, db], …] }`. Both carry
1278
+ * addresses and nothing else.
877
1279
  */
878
- private scheduleReconnect;
1280
+ private broadcastChange;
1281
+ private flushBroadcasts;
1282
+ /** One notification another instance published on {@link PG_NOTIFY_CHANNEL}: one change, or a batch. */
1283
+ private handleCrossInstanceNotification;
879
1284
  }
1285
+ /**
1286
+ * Pack `changes` into as few payloads as fit under
1287
+ * {@link ENTITY_NOTIFY_MAX_BYTES}. One change keeps the single-change shape.
1288
+ */
1289
+ export declare function packEntityChanges(sid: string, changes: Array<{
1290
+ p: string;
1291
+ eid: string;
1292
+ db: string | null;
1293
+ }>): string[];
880
1294
  /**
881
1295
  * Alias for RealtimeService for consistent naming with other database implementations.
882
1296
  * This allows code to use PostgresRealtimeProvider alongside future MongoRealtimeProvider, etc.