@rebasepro/server-postgres 0.24.0 → 0.24.1-canary.gcb918fc

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 (172) hide show
  1. package/README.md +16 -0
  2. package/dist/{BranchService-BRt78gfa.js → BranchService-Bsn2wfaj.js} +3 -2
  3. package/dist/{BranchService-BRt78gfa.js.map → BranchService-Bsn2wfaj.js.map} +1 -1
  4. package/dist/PostgresBackendDriver.d.ts +91 -2
  5. package/dist/PostgresBootstrapper.d.ts +101 -2
  6. package/dist/auth/ensure-tables.d.ts +12 -0
  7. package/dist/auth/services.d.ts +6 -0
  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-DW5p9_zv.js → backup-cli-DnpJi6I7.js} +37 -29
  12. package/dist/backup-cli-DnpJi6I7.js.map +1 -0
  13. package/dist/{backup-service-EwcDVG-8.js → backup-service-BTBB7RXn.js} +55 -21
  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-console-Ca6ZlAVU.js +154 -0
  18. package/dist/cli-console-Ca6ZlAVU.js.map +1 -0
  19. package/dist/cli-console.d.ts +39 -0
  20. package/dist/cli-errors-CaI4ntyD.js +1919 -0
  21. package/dist/cli-errors-CaI4ntyD.js.map +1 -0
  22. package/dist/cli-errors.d.ts +51 -5
  23. package/dist/{cli-flags-BglvpjHv.js → cli-flags-JNI-pwSf.js} +6 -3
  24. package/dist/cli-flags-JNI-pwSf.js.map +1 -0
  25. package/dist/cli-helpers.d.ts +40 -0
  26. package/dist/cli-output.d.ts +16 -28
  27. package/dist/cli-push.d.ts +87 -0
  28. package/dist/cli.js +566 -182
  29. package/dist/cli.js.map +1 -1
  30. package/dist/{column-plan-helpers-1-LQD0yI.js → column-plan-helpers-drZIsSNf.js} +10 -4
  31. package/dist/{column-plan-helpers-1-LQD0yI.js.map → column-plan-helpers-drZIsSNf.js.map} +1 -1
  32. package/dist/{databasePoolManager-Bj5FbeAs.js → connection-C9mT8bgf.js} +24 -112
  33. package/dist/connection-C9mT8bgf.js.map +1 -0
  34. package/dist/connection.d.ts +13 -2
  35. package/dist/databasePoolManager-BIuewidb.js +119 -0
  36. package/dist/databasePoolManager-BIuewidb.js.map +1 -0
  37. package/dist/{ensure-collection-tables-kkkHk8oo.js → deferred-index-build-DP1_SNFo.js} +484 -346
  38. package/dist/deferred-index-build-DP1_SNFo.js.map +1 -0
  39. package/dist/{doctor-C-sYWbmt.js → doctor-C4WiftHm.js} +63 -52
  40. package/dist/doctor-C4WiftHm.js.map +1 -0
  41. package/dist/ensure-collection-policies-CXpbeNag.js +541 -0
  42. package/dist/ensure-collection-policies-CXpbeNag.js.map +1 -0
  43. package/dist/ensure-collection-tables-BhsU6qUE.js +532 -0
  44. package/dist/ensure-collection-tables-BhsU6qUE.js.map +1 -0
  45. package/dist/{ensure-tables-BmI_tRxc.js → ensure-tables-CVm_yvzb.js} +173 -77
  46. package/dist/ensure-tables-CVm_yvzb.js.map +1 -0
  47. package/dist/enum-labels-kept-D8fI1MoX.js +56 -0
  48. package/dist/enum-labels-kept-D8fI1MoX.js.map +1 -0
  49. package/dist/generate-drizzle-schema-Bz52jH2h.js +57 -0
  50. package/dist/generate-drizzle-schema-Bz52jH2h.js.map +1 -0
  51. package/dist/{generate-drizzle-schema-logic-sSFDp6LR.js → generate-drizzle-schema-logic-B74WN37S.js} +5 -5
  52. package/dist/generate-drizzle-schema-logic-B74WN37S.js.map +1 -0
  53. package/dist/generate-files-CdEckX6c.js +199 -0
  54. package/dist/generate-files-CdEckX6c.js.map +1 -0
  55. package/dist/{generate-postgres-ddl-logic-BJsLaVNX.js → generate-postgres-ddl-logic-B64O2oZF.js} +3 -3
  56. package/dist/{generate-postgres-ddl-logic-BJsLaVNX.js.map → generate-postgres-ddl-logic-B64O2oZF.js.map} +1 -1
  57. package/dist/generated-sql.d.ts +8 -2
  58. package/dist/history/HistoryService.d.ts +64 -5
  59. package/dist/history/ensure-history-table.d.ts +63 -4
  60. package/dist/index.d.ts +1 -0
  61. package/dist/index.es.js +139 -19236
  62. package/dist/index.es.js.map +1 -1
  63. package/dist/{introspect-db-logic-kCETE8TY.js → introspect-db-logic-C-Di8dA9.js} +44 -6
  64. package/dist/introspect-db-logic-C-Di8dA9.js.map +1 -0
  65. package/dist/{introspect-db-queries-C_Q5VgQw.js → introspect-db-queries-D6kAqReI.js} +23 -1
  66. package/dist/introspect-db-queries-D6kAqReI.js.map +1 -0
  67. package/dist/open-rebase-CbOur0TU.js +20858 -0
  68. package/dist/open-rebase-CbOur0TU.js.map +1 -0
  69. package/dist/open-rebase.d.ts +86 -0
  70. package/dist/pg-error-utils-vJezlXdI.js +388 -0
  71. package/dist/pg-error-utils-vJezlXdI.js.map +1 -0
  72. package/dist/{plan-schema-DU9exq6C.js → plan-schema-B1YF_tYq.js} +151 -41
  73. package/dist/plan-schema-B1YF_tYq.js.map +1 -0
  74. package/dist/{policy-drift-B-J2hhm0.js → policy-drift-Dm8i-s1J.js} +23 -6
  75. package/dist/policy-drift-Dm8i-s1J.js.map +1 -0
  76. package/dist/push-index-prebuild-Db4AQF4i.js +45 -0
  77. package/dist/push-index-prebuild-Db4AQF4i.js.map +1 -0
  78. package/dist/push-legacy-renames-BK6ilQq2.js +34 -0
  79. package/dist/push-legacy-renames-BK6ilQq2.js.map +1 -0
  80. package/dist/{render-ddl-Ds2t_d9V.js → render-ddl-CtC3y9Ol.js} +8 -4
  81. package/dist/render-ddl-CtC3y9Ol.js.map +1 -0
  82. package/dist/{rls-bootstrap-sql-_KNnjanK.js → rls-bootstrap-sql-RQUljnJ1.js} +125 -1270
  83. package/dist/rls-bootstrap-sql-RQUljnJ1.js.map +1 -0
  84. package/dist/{rls-enforcement-CfXOJJaW.js → rls-enforcement-DvzqlaL1.js} +129 -17
  85. package/dist/rls-enforcement-DvzqlaL1.js.map +1 -0
  86. package/dist/schema/auth-schema.d.ts +2 -2
  87. package/dist/schema/boot-ddl.d.ts +47 -0
  88. package/dist/schema/collection-index.d.ts +45 -0
  89. package/dist/schema/column-plan-helpers.d.ts +5 -0
  90. package/dist/schema/deferred-index-build.d.ts +131 -0
  91. package/dist/schema/destructive-sql.d.ts +7 -0
  92. package/dist/schema/doctor-cli.js +110 -6
  93. package/dist/schema/doctor-cli.js.map +1 -1
  94. package/dist/schema/doctor-search-scale.d.ts +45 -0
  95. package/dist/schema/doctor.d.ts +20 -1
  96. package/dist/schema/ensure-collection-policies.d.ts +73 -35
  97. package/dist/schema/ensure-collection-tables.d.ts +56 -3
  98. package/dist/schema/enum-labels-kept.d.ts +26 -0
  99. package/dist/schema/generate-drizzle-schema.js +1 -1
  100. package/dist/schema/generate-files.d.ts +44 -0
  101. package/dist/schema/generate-next-step.d.ts +0 -13
  102. package/dist/schema/generate-postgres-ddl.js +10 -81
  103. package/dist/schema/generate-postgres-ddl.js.map +1 -1
  104. package/dist/schema/introspect-db-logic.d.ts +12 -0
  105. package/dist/schema/introspect-db-queries.d.ts +11 -1
  106. package/dist/schema/introspect-db-structure.d.ts +10 -0
  107. package/dist/schema/introspect-db.js +4 -5
  108. package/dist/schema/introspect-db.js.map +1 -1
  109. package/dist/schema/migration-post-process.d.ts +34 -0
  110. package/dist/schema/plan/diff-plan.d.ts +126 -0
  111. package/dist/schema/plan/types.d.ts +8 -0
  112. package/dist/schema/push-index-prebuild.d.ts +25 -0
  113. package/dist/schema/push-legacy-renames.d.ts +20 -0
  114. package/dist/schema/rls-bootstrap-sql.d.ts +15 -0
  115. package/dist/security/rls-enforcement.d.ts +32 -4
  116. package/dist/services/FetchService.d.ts +58 -8
  117. package/dist/services/PersistService.d.ts +15 -0
  118. package/dist/services/RelationService.d.ts +88 -3
  119. package/dist/services/cdc/CdcListener.d.ts +17 -6
  120. package/dist/services/cdc/trigger-cdc.d.ts +151 -27
  121. package/dist/services/channel-bus/index.d.ts +1 -1
  122. package/dist/services/channel-history.d.ts +15 -4
  123. package/dist/services/collection-helpers.d.ts +15 -0
  124. package/dist/services/field-viewer.d.ts +15 -1
  125. package/dist/services/pg-notify-listener.d.ts +7 -0
  126. package/dist/services/realtimeService.d.ts +314 -14
  127. package/dist/services/row-pipeline.d.ts +16 -9
  128. package/dist/services/socket-liveness.d.ts +42 -2
  129. package/dist/services/sql-script.d.ts +207 -3
  130. package/dist/services/vector-iterative-scan.d.ts +40 -0
  131. package/dist/services/write-depth.d.ts +6 -0
  132. package/dist/services/write-transaction-scope.d.ts +9 -0
  133. package/dist/socket-opt-ins.d.ts +64 -0
  134. package/dist/source-DBDbBTL4.js +811 -0
  135. package/dist/source-DBDbBTL4.js.map +1 -0
  136. package/dist/tsx-loader.d.ts +14 -0
  137. package/dist/utils/drizzle-conditions.d.ts +21 -1
  138. package/dist/utils/error-cause.d.ts +7 -0
  139. package/dist/utils/pg-error-utils.d.ts +2 -4
  140. package/dist/utils/result-rows.d.ts +10 -0
  141. package/dist/websocket-BLGc5yhs.js +1604 -0
  142. package/dist/websocket-BLGc5yhs.js.map +1 -0
  143. package/dist/websocket.d.ts +13 -0
  144. package/package.json +7 -7
  145. package/dist/backup-cli-DW5p9_zv.js.map +0 -1
  146. package/dist/backup-service-EwcDVG-8.js.map +0 -1
  147. package/dist/cli-errors-DsA-K9uP.js +0 -893
  148. package/dist/cli-errors-DsA-K9uP.js.map +0 -1
  149. package/dist/cli-flags-BglvpjHv.js.map +0 -1
  150. package/dist/cli-output-CNdMql-L.js +0 -47
  151. package/dist/cli-output-CNdMql-L.js.map +0 -1
  152. package/dist/collection-index-DdnsxiJ_.js +0 -286
  153. package/dist/collection-index-DdnsxiJ_.js.map +0 -1
  154. package/dist/databasePoolManager-Bj5FbeAs.js.map +0 -1
  155. package/dist/doctor-C-sYWbmt.js.map +0 -1
  156. package/dist/ensure-collection-policies-B1ureSIV.js +0 -160
  157. package/dist/ensure-collection-policies-B1ureSIV.js.map +0 -1
  158. package/dist/ensure-collection-tables-kkkHk8oo.js.map +0 -1
  159. package/dist/ensure-tables-BmI_tRxc.js.map +0 -1
  160. package/dist/generate-drizzle-schema-93M0lUxK.js +0 -123
  161. package/dist/generate-drizzle-schema-93M0lUxK.js.map +0 -1
  162. package/dist/generate-drizzle-schema-logic-sSFDp6LR.js.map +0 -1
  163. package/dist/introspect-db-logic-kCETE8TY.js.map +0 -1
  164. package/dist/introspect-db-queries-C_Q5VgQw.js.map +0 -1
  165. package/dist/plan-schema-DU9exq6C.js.map +0 -1
  166. package/dist/policy-drift-B-J2hhm0.js.map +0 -1
  167. package/dist/render-ddl-Ds2t_d9V.js.map +0 -1
  168. package/dist/rls-bootstrap-sql-_KNnjanK.js.map +0 -1
  169. package/dist/rls-enforcement-CfXOJJaW.js.map +0 -1
  170. package/dist/source-Br7L7GOI.js +0 -410
  171. package/dist/source-Br7L7GOI.js.map +0 -1
  172. package/dist/utils/sql-redaction.d.ts +0 -21
@@ -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.
@@ -142,6 +150,13 @@ type Subscription = {
142
150
  path: string;
143
151
  id?: string | number;
144
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;
145
160
  authContext?: SubscriptionAuthContext;
146
161
  /**
147
162
  * An in-process listener's error callback, called when a refetch fails.
@@ -153,7 +168,28 @@ type Subscription = {
153
168
  started: number;
154
169
  /** The highest started-sequence that has already reached the subscriber. */
155
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;
156
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;
157
193
  /**
158
194
  * The default ceiling on subscriptions one socket may hold.
159
195
  *
@@ -172,6 +208,25 @@ export declare const DEFAULT_MAX_SUBSCRIPTIONS_PER_SOCKET = 1000;
172
208
  * meaning anything.
173
209
  */
174
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;
175
230
  /**
176
231
  * PostgreSQL-specific realtime service.
177
232
  * Handles WebSocket connections and subscriptions for real-time row updates.
@@ -188,8 +243,19 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
188
243
  */
189
244
  readonly supportsChannels = true;
190
245
  private clients;
246
+ /** What each socket opted into when it opened — see `socket-opt-ins.ts`. */
247
+ private clientOptIns;
191
248
  private channels;
192
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;
193
259
  /**
194
260
  * Ordered, replayable history for channels that opt into it.
195
261
  *
@@ -230,9 +296,17 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
230
296
  private presenceStore?;
231
297
  /** Sweeps roster rows left behind by instances that stopped heartbeating. */
232
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;
233
305
  /**
234
306
  * Channels whose oversized ephemeral broadcasts have already been reported,
235
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.
236
310
  */
237
311
  private oversizedBroadcastWarned;
238
312
  /**
@@ -252,6 +326,11 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
252
326
  private foreignInstanceSeen;
253
327
  /** So the multi-pod memory-bus warning is emitted once, not once per join. */
254
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;
255
334
  private presenceInterval?;
256
335
  private static readonly PRESENCE_TIMEOUT_MS;
257
336
  /** How often stale roster rows from other instances are reaped. */
@@ -271,8 +350,25 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
271
350
  private subscriptionCallbacks;
272
351
  /** Each client's subscriptions, by key — for its ceiling and its disconnect. */
273
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;
274
360
  /** Subscriptions one refetch answers — see {@link SubscriptionGroup}. */
275
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;
276
372
  /**
277
373
  * The collection groups on each path, and the single-row groups at each
278
374
  * `path` + id: what a change looks up, instead of walking every
@@ -280,6 +376,21 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
280
376
  */
281
377
  private collectionGroupsByPath;
282
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;
283
394
  /**
284
395
  * Every path a subscription holds, with the collection it lands on and how
285
396
  * many hold it — the candidates `aliasPaths` considers, resolved once.
@@ -290,21 +401,60 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
290
401
  * {@link DEFAULT_MAX_SUBSCRIPTIONS_PER_SOCKET}.
291
402
  */
292
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;
293
419
  private driver?;
294
420
  /** Unique identifier for this process instance, used to skip own notifications. */
295
421
  private readonly instanceId;
422
+ /** What this instance's LISTEN connections report in `pg_stat_activity`. */
423
+ private readonly listenerApplicationName;
296
424
  /** The LISTEN connection on {@link PG_NOTIFY_CHANNEL}, outside the Drizzle pool. */
297
425
  private crossInstanceListener?;
298
426
  /** Whether cross-instance broadcasting is active. */
299
427
  private broadcasting;
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?;
300
434
  /** Debounce window (ms) for coalescing rapid row updates into a single correctness refetch. */
301
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;
302
446
  /** Dedicated LISTEN client for DB-level change events (undefined unless CDC is enabled). */
303
447
  private cdcListener?;
304
448
  /** Whether database-level CDC is the active cross-instance change source. */
305
449
  private cdcActive;
306
450
  /** Junction table → the child lists its rows belong to, built when CDC starts. */
307
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;
308
458
  /** Reverse lookup: `schema.table` (and bare `table`) → collection, built when CDC starts. */
309
459
  private cdcTableMap?;
310
460
  /**
@@ -349,6 +499,18 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
349
499
  * redaction lives.
350
500
  */
351
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;
352
514
  setDataDriver(driver: DataDriver): void;
353
515
  /**
354
516
  * The live subscriptions by the id their client gave them — for tests and
@@ -368,13 +530,18 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
368
530
  * every index that finds it.
369
531
  */
370
532
  private registerSubscription;
533
+ /** Remember an in-process subscription by its caller's id — see {@link inProcessKeysById}. */
534
+ private indexInProcess;
371
535
  /** Remove the subscription under `key` from everything that holds it. */
372
536
  private dropSubscription;
373
537
  /** Take `subscription` out of its group and path count; a group left empty goes, timer and all. */
374
538
  private leaveGroup;
539
+ /** See {@link unscheduledListGroups}. */
540
+ private markListGroupUnscheduled;
375
541
  /**
376
542
  * Ask for a refetch of `group` once changes have been quiet for the
377
- * debounce window — for every member, or only for `member`.
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.
378
545
  *
379
546
  * The window is measured from the last change, as a debounce is, but the
380
547
  * timer is not re-armed per change: a 10k-row statement is 10k changes,
@@ -384,6 +551,13 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
384
551
  */
385
552
  private scheduleGroupRefetch;
386
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;
387
561
  /**
388
562
  * One read for the group, delivered to each member it is for.
389
563
  *
@@ -456,7 +630,12 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
456
630
  unsubscribe(subscriptionId: string): void;
457
631
  addClient(clientId: string, ws: WebSocket): void;
458
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;
459
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;
460
639
  private handleMessage;
461
640
  /**
462
641
  * Refuse a new subscription past the socket's ceiling, and say so.
@@ -569,13 +748,20 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
569
748
  */
570
749
  private reportDriverFetchFailure;
571
750
  /**
572
- * 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
+ *
573
754
  * When authContext is provided, the fetch runs inside a transaction
574
- * 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}.
575
759
  */
576
760
  private fetchCollectionWithAuth;
577
761
  /**
578
- * 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.
579
765
  *
580
766
  * Frames used to carry rows and primary keys and nothing else, so the
581
767
  * client issued a `GET /count` **per push** to fill in a total it needed to
@@ -583,20 +769,61 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
583
769
  * write, per subscriber, forever. The refetch already knows the query; it
584
770
  * counts once, here, under the same RLS transaction that read the rows, so
585
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.
586
778
  */
587
- private collectionMetaWithAuth;
779
+ private collectionMeta;
780
+ /**
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.
792
+ */
793
+ private fetchEntityFrame;
588
794
  /**
589
795
  * Fetch a single row with optional RLS auth context.
590
796
  */
591
797
  private fetchEntityWithAuth;
592
- private sendCollectionUpdate;
593
798
  /**
594
799
  * Everything in a `collection_update` frame after its subscription id,
595
- * serialised once for every member of a group: the same rows, keys and
596
- * meta as {@link sendCollectionUpdate} sends, in the same order.
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.
597
804
  */
598
805
  private collectionFrameTail;
599
- private sendSingleUpdate;
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;
600
827
  /**
601
828
  * Send a lightweight row-level patch to a collection subscriber.
602
829
  * The client can merge this into its cached data for instant feedback.
@@ -709,6 +936,21 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
709
936
  * fraction of the room. The evidence is already in the process, so use it.
710
937
  */
711
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;
712
954
  /** Leave a broadcast channel */
713
955
  leaveChannel(clientId: string, channel: string): void;
714
956
  /**
@@ -926,6 +1168,19 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
926
1168
  * therefore only ever receives rows its RLS policies permit — filtering is per
927
1169
  * subscriber, never per publisher.
928
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;
929
1184
  private handleCdcEvent;
930
1185
  /**
931
1186
  * Deliver a change on a many-to-many junction table as a change to the child
@@ -944,7 +1199,11 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
944
1199
  * Returns whether the table was recognised as a junction.
945
1200
  */
946
1201
  private handleJunctionCdcEvent;
947
- /** 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
+ */
948
1207
  private extractIdFromCdcRow;
949
1208
  /**
950
1209
  * A captured row with its key under the key's *field* names.
@@ -957,8 +1216,19 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
957
1216
  */
958
1217
  private cdcRowByKeyFields;
959
1218
  private dedupKey;
960
- /** 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
+ */
961
1229
  private markAppEmit;
1230
+ /** When {@link markAppEmit} may next purge expired entries. */
1231
+ private nextAppEmitPurgeAt;
962
1232
  /** Consume a matching app-emit record if present and unexpired; true ⇒ suppress the CDC echo. */
963
1233
  private consumeAppEmit;
964
1234
  /**
@@ -979,18 +1249,48 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
979
1249
  * @param connectionString Raw Postgres connection string for the LISTEN client.
980
1250
  */
981
1251
  startListening(connectionString: string): Promise<void>;
1252
+ /**
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.
1261
+ */
1262
+ startPublishing(): void;
982
1263
  /**
983
1264
  * Stop listening and clean up the dedicated LISTEN connection.
984
1265
  */
985
1266
  stopListening(): Promise<void>;
986
1267
  /**
987
- * Broadcast a change notification to other instances via pg_notify.
988
- * Uses the main Drizzle connection (pooled) — NOT the LISTEN client.
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.
1272
+ *
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.
989
1279
  */
990
1280
  private broadcastChange;
991
- /** One change another instance published on {@link PG_NOTIFY_CHANNEL}. */
1281
+ private flushBroadcasts;
1282
+ /** One notification another instance published on {@link PG_NOTIFY_CHANNEL}: one change, or a batch. */
992
1283
  private handleCrossInstanceNotification;
993
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[];
994
1294
  /**
995
1295
  * Alias for RealtimeService for consistent naming with other database implementations.
996
1296
  * This allows code to use PostgresRealtimeProvider alongside future MongoRealtimeProvider, etc.
@@ -29,15 +29,6 @@ export type RelationStyle = "ref" | "inline";
29
29
  * to unwrap that same level back out.
30
30
  */
31
31
  export declare function isJunctionRelation(relation: ResolvedRelation): boolean;
32
- /**
33
- * Apply {@link toRestScalar} across a row, leaving undeclared columns alone.
34
- *
35
- * Exported because the include loader attaches related rows itself, and a
36
- * target rendered differently from its parent is the shape bug this whole file
37
- * exists to prevent — a date was a string at the top level and a
38
- * `{ __type: "date" }` envelope one level down, in the same response.
39
- */
40
- export declare function toRestValues(row: Record<string, unknown>, collection: CollectionConfig): Record<string, unknown>;
41
32
  /** Render one target row in the requested style. */
42
33
  /**
43
34
  * Drop every column this caller may not read.
@@ -69,6 +60,22 @@ export declare function toRestValues(row: Record<string, unknown>, collection: C
69
60
  * though the array itself is theirs to see.
70
61
  */
71
62
  export declare function stripUnreadable(row: Record<string, unknown>, collection: CollectionConfig): Record<string, unknown>;
63
+ /**
64
+ * May the current caller be handed the rows of this relation of `collection`?
65
+ *
66
+ * A relation property's value *is* its related rows, so a property the caller
67
+ * may not read withholds the relation whole: an `include` neither loads it nor
68
+ * counts it. {@link stripUnreadable} cannot be the only guard here, because the
69
+ * REST rows are stripped before the include loader runs (`toRestRow`, then
70
+ * `loadIncludes`) — the loader wrote the rows back onto the key the strip had
71
+ * just deleted, and `_count` beside them.
72
+ *
73
+ * Judged by every `type: "relation"` property that names this relation: by its
74
+ * own key (the include key of an inline relation), or by the `relationName` it
75
+ * addresses in the collection's `relations` array. A relation declared only in
76
+ * `relations` has no property, so no field rule, and stays readable.
77
+ */
78
+ export declare function canReadRelation(collection: CollectionConfig, key: string, relation: ResolvedRelation): boolean;
72
79
  /**
73
80
  * The address a relation ref points at.
74
81
  *
@@ -11,8 +11,10 @@
11
11
  * Two checks catch it. The socket must answer a ping: one that has not answered
12
12
  * by the next is terminated. And it must read what it is sent: one whose unsent
13
13
  * backlog passes {@link MAX_SOCKET_BUFFERED_BYTES} is terminated at the next
14
- * frame for it. A terminated socket closes like any other, so the realtime
15
- * service drops its subscriptions, channels and presence on its `close`.
14
+ * frame for it, and when all of the process's sockets together hold more than
15
+ * {@link MAX_PROCESS_BUFFERED_BYTES}, the ones with the largest backlogs go
16
+ * first. A terminated socket closes like any other, so the realtime service
17
+ * drops its subscriptions, channels and presence on its `close`.
16
18
  */
17
19
  import { WebSocket, type WebSocketServer } from "ws";
18
20
  /**
@@ -33,6 +35,40 @@ export declare const SOCKET_PING_INTERVAL_MS = 30000;
33
35
  * subscription instead of the backlog.
34
36
  */
35
37
  export declare const MAX_SOCKET_BUFFERED_BYTES: number;
38
+ /**
39
+ * The unsent bytes all of this process's sockets may hold between them before
40
+ * the ones with the largest backlogs are terminated.
41
+ *
42
+ * The per-socket ceiling alone bounds nothing for the process: a hundred slow
43
+ * phones during a bulk-update storm could each sit just under it, 1.6 GB of
44
+ * frames between them, before any one was let go. A healthy socket's backlog
45
+ * is close to nothing — the kernel takes a frame as it is written — so a
46
+ * process holding this much unread is already in a storm. 256 MiB is sixteen
47
+ * sockets at their own ceiling, and an eighth of a 2 GiB container; under a
48
+ * smaller memory limit it is a quarter of that limit (see
49
+ * {@link processSendBudget}).
50
+ */
51
+ export declare const MAX_PROCESS_BUFFERED_BYTES: number;
52
+ /**
53
+ * The process-wide budget: {@link MAX_PROCESS_BUFFERED_BYTES}, or a quarter of
54
+ * the memory limit the process runs under when that is smaller — never less
55
+ * than one socket's own ceiling. `constrained` is the limit in bytes, 0 for
56
+ * none, as `process.constrainedMemory()` reports it.
57
+ */
58
+ export declare function processSendBudget(constrained?: number): number;
59
+ /**
60
+ * Count the sockets of `wss` in the process-wide send budget. Returns the
61
+ * function that stops counting them, for when the server closes.
62
+ */
63
+ export declare function budgetSendBuffers(wss: WebSocketServer): () => void;
64
+ /**
65
+ * Terminate sockets, largest backlog first, until what this process's sockets
66
+ * hold unread between them is back within `budget`. Returns the sockets it
67
+ * terminated. A terminated socket closes like any other, and its client
68
+ * reconnects and re-subscribes: one fresh frame per subscription, instead of
69
+ * the backlog.
70
+ */
71
+ export declare function enforceProcessSendBudget(budget?: number): WebSocket[];
36
72
  /**
37
73
  * Ping every socket of `wss` each `intervalMs`, and terminate the ones that did
38
74
  * not answer the previous ping. Returns the function that stops it.
@@ -41,5 +77,9 @@ export declare function reapSilentSockets(wss: WebSocketServer, intervalMs?: num
41
77
  /**
42
78
  * Terminate `ws` if its unsent backlog has passed {@link MAX_SOCKET_BUFFERED_BYTES}.
43
79
  * Returns whether it did; the caller then sends nothing.
80
+ *
81
+ * Every frame in and out of a socket comes through here, so this is also where
82
+ * the process-wide budget is kept, summed at most every
83
+ * {@link PROCESS_BUDGET_CHECK_MS} — see {@link enforceProcessSendBudget}.
44
84
  */
45
85
  export declare function terminateIfBacklogged(ws: WebSocket, clientId: string): boolean;