@rebasepro/server-postgres 0.22.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{BranchService-CucnFcSE.js → BranchService-BRt78gfa.js} +35 -18
- package/dist/BranchService-BRt78gfa.js.map +1 -0
- package/dist/PostgresBackendDriver.d.ts +116 -4
- package/dist/auth/services.d.ts +99 -22
- package/dist/{auth-users-columns-D2LBFrMH.js → auth-users-columns-C72EMoDJ.js} +17 -1
- package/dist/{auth-users-columns-D2LBFrMH.js.map → auth-users-columns-C72EMoDJ.js.map} +1 -1
- package/dist/backup/backup-cli.d.ts +22 -0
- package/dist/{backup-cli-DqBakiMO.js → backup-cli-DW5p9_zv.js} +17 -14
- package/dist/backup-cli-DW5p9_zv.js.map +1 -0
- package/dist/{backup-service-CaMOS76G.js → backup-service-EwcDVG-8.js} +7 -9
- package/dist/{backup-service-CaMOS76G.js.map → backup-service-EwcDVG-8.js.map} +1 -1
- package/dist/{cli-errors-Dka89exj.js → cli-errors-DsA-K9uP.js} +101 -1
- package/dist/cli-errors-DsA-K9uP.js.map +1 -0
- package/dist/cli-errors.d.ts +42 -0
- package/dist/cli-flags-BglvpjHv.js +138 -0
- package/dist/cli-flags-BglvpjHv.js.map +1 -0
- package/dist/cli-flags.d.ts +28 -0
- package/dist/cli-helpers.d.ts +18 -12
- package/dist/cli-scratch-database.d.ts +34 -0
- package/dist/cli.js +177 -287
- package/dist/cli.js.map +1 -1
- package/dist/{column-plan-helpers-CpILzHJS.js → column-plan-helpers-1-LQD0yI.js} +44 -38
- package/dist/column-plan-helpers-1-LQD0yI.js.map +1 -0
- package/dist/data-transformer.d.ts +0 -8
- package/dist/{doctor-CU9IogdL.js → doctor-C-sYWbmt.js} +228 -46
- package/dist/doctor-C-sYWbmt.js.map +1 -0
- package/dist/{ensure-collection-policies-Dzd-2S81.js → ensure-collection-policies-B1ureSIV.js} +73 -18
- package/dist/ensure-collection-policies-B1ureSIV.js.map +1 -0
- package/dist/{ensure-collection-tables-CpAgy51F.js → ensure-collection-tables-kkkHk8oo.js} +124 -37
- package/dist/ensure-collection-tables-kkkHk8oo.js.map +1 -0
- package/dist/{ensure-tables-Cr5B4UmH.js → ensure-tables-BmI_tRxc.js} +10 -3
- package/dist/ensure-tables-BmI_tRxc.js.map +1 -0
- package/dist/{generate-drizzle-schema-B537GIvz.js → generate-drizzle-schema-93M0lUxK.js} +2 -2
- package/dist/{generate-drizzle-schema-B537GIvz.js.map → generate-drizzle-schema-93M0lUxK.js.map} +1 -1
- package/dist/{generate-drizzle-schema-logic-BcMl7VSy.js → generate-drizzle-schema-logic-sSFDp6LR.js} +26 -8
- package/dist/generate-drizzle-schema-logic-sSFDp6LR.js.map +1 -0
- package/dist/generate-postgres-ddl-logic-BJsLaVNX.js +152 -0
- package/dist/generate-postgres-ddl-logic-BJsLaVNX.js.map +1 -0
- package/dist/generated-sql.d.ts +28 -0
- package/dist/index.es.js +4768 -1117
- package/dist/index.es.js.map +1 -1
- package/dist/{introspect-db-logic-C6LQdTxj.js → introspect-db-logic-kCETE8TY.js} +532 -39
- package/dist/introspect-db-logic-kCETE8TY.js.map +1 -0
- package/dist/introspect-db-queries-C_Q5VgQw.js +317 -0
- package/dist/introspect-db-queries-C_Q5VgQw.js.map +1 -0
- package/dist/{plan-schema-CboAIwLN.js → plan-schema-DU9exq6C.js} +291 -529
- package/dist/plan-schema-DU9exq6C.js.map +1 -0
- package/dist/{policy-drift-xJfy9xG7.js → policy-drift-B-J2hhm0.js} +3 -3
- package/dist/policy-drift-B-J2hhm0.js.map +1 -0
- package/dist/{generate-postgres-ddl-logic-D7imhYV8.js → render-ddl-Ds2t_d9V.js} +11 -149
- package/dist/render-ddl-Ds2t_d9V.js.map +1 -0
- package/dist/{rls-bootstrap-sql-H3rCFi3F.js → rls-bootstrap-sql-_KNnjanK.js} +562 -35
- package/dist/rls-bootstrap-sql-_KNnjanK.js.map +1 -0
- package/dist/{rls-enforcement-C6Xk0lA6.js → rls-enforcement-CfXOJJaW.js} +10 -2
- package/dist/rls-enforcement-CfXOJJaW.js.map +1 -0
- package/dist/schema/atlas-argv.d.ts +15 -0
- package/dist/schema/auth-schema.d.ts +170 -0
- package/dist/schema/classify-change.d.ts +29 -1
- package/dist/schema/column-plan-helpers.d.ts +51 -20
- package/dist/schema/destructive-sql.d.ts +71 -1
- package/dist/schema/doctor-cli.js +4 -4
- package/dist/schema/doctor.d.ts +34 -1
- package/dist/schema/ensure-collection-policies.d.ts +22 -0
- package/dist/schema/generate-drizzle-schema.js +1 -1
- package/dist/schema/generate-postgres-ddl-logic.d.ts +5 -5
- package/dist/schema/generate-postgres-ddl.js +1 -1
- package/dist/schema/generate-schema-commit.d.ts +12 -0
- package/dist/schema/introspect-db-logic.d.ts +31 -0
- package/dist/schema/introspect-db-queries.d.ts +1 -1
- package/dist/schema/introspect-db-search.d.ts +22 -0
- package/dist/schema/introspect-db-storage.d.ts +83 -0
- package/dist/schema/introspect-db.js +15 -314
- package/dist/schema/introspect-db.js.map +1 -1
- package/dist/schema/plan/diff-plan.d.ts +4 -3
- package/dist/schema/plan/plan-schema.d.ts +26 -11
- package/dist/schema/plan/render-ddl.d.ts +6 -0
- package/dist/schema/plan/types.d.ts +43 -12
- package/dist/search-column-BM-GV6vH.js +442 -0
- package/dist/search-column-BM-GV6vH.js.map +1 -0
- package/dist/security/policy-drift.d.ts +1 -1
- package/dist/security/rls-enforcement.d.ts +7 -0
- package/dist/services/BranchService.d.ts +22 -1
- package/dist/services/FetchService.d.ts +102 -32
- package/dist/services/PersistService.d.ts +41 -5
- package/dist/services/RelationService.d.ts +29 -0
- package/dist/services/RelationWriteService.d.ts +6 -0
- package/dist/services/cdc/CdcListener.d.ts +15 -5
- package/dist/services/cdc/identity-columns.d.ts +19 -0
- package/dist/services/cdc/trigger-cdc.d.ts +45 -7
- package/dist/services/channel-bus/PostgresChannelBus.d.ts +12 -7
- package/dist/services/channel-history.d.ts +17 -1
- package/dist/services/collection-helpers.d.ts +21 -0
- package/dist/services/dataService.d.ts +3 -20
- package/dist/services/field-op-sql.d.ts +71 -0
- package/dist/services/junction-writes.d.ts +19 -3
- package/dist/services/pg-notify-listener.d.ts +95 -6
- package/dist/services/read-field-access.d.ts +19 -0
- package/dist/services/realtimeService.d.ts +232 -44
- package/dist/services/row-pipeline.d.ts +5 -0
- package/dist/services/socket-liveness.d.ts +45 -0
- package/dist/services/soft-delete.d.ts +12 -2
- package/dist/services/sql-script.d.ts +71 -0
- package/dist/services/write-depth.d.ts +16 -0
- package/dist/services/write-transaction-scope.d.ts +42 -0
- package/dist/utils/drizzle-conditions.d.ts +49 -4
- package/dist/utils/sql-redaction.d.ts +21 -0
- package/dist/websocket.d.ts +57 -18
- package/package.json +9 -9
- package/dist/BranchService-CucnFcSE.js.map +0 -1
- package/dist/backup-cli-DqBakiMO.js.map +0 -1
- package/dist/cli-errors-Dka89exj.js.map +0 -1
- package/dist/column-plan-helpers-CpILzHJS.js.map +0 -1
- package/dist/doctor-CU9IogdL.js.map +0 -1
- package/dist/ensure-collection-policies-Dzd-2S81.js.map +0 -1
- package/dist/ensure-collection-tables-CpAgy51F.js.map +0 -1
- package/dist/ensure-tables-Cr5B4UmH.js.map +0 -1
- package/dist/generate-drizzle-schema-logic-BcMl7VSy.js.map +0 -1
- package/dist/generate-postgres-ddl-logic-D7imhYV8.js.map +0 -1
- package/dist/introspect-db-logic-C6LQdTxj.js.map +0 -1
- package/dist/plan-schema-CboAIwLN.js.map +0 -1
- package/dist/policy-drift-xJfy9xG7.js.map +0 -1
- package/dist/rls-bootstrap-sql-H3rCFi3F.js.map +0 -1
- package/dist/rls-enforcement-C6Xk0lA6.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().
|
|
@@ -133,7 +133,11 @@ export type DataDriverSubscriptionRequest = {
|
|
|
133
133
|
* last delivery *started* the last one *delivered*.
|
|
134
134
|
*/
|
|
135
135
|
type Subscription = {
|
|
136
|
+
/** The id the client named it by — what every frame for it carries. */
|
|
137
|
+
subscriptionId: string;
|
|
136
138
|
clientId: string;
|
|
139
|
+
/** The {@link SubscriptionGroup} whose refetch answers it. */
|
|
140
|
+
groupKey: string;
|
|
137
141
|
type: "collection" | "single";
|
|
138
142
|
path: string;
|
|
139
143
|
id?: string | number;
|
|
@@ -150,6 +154,24 @@ type Subscription = {
|
|
|
150
154
|
/** The highest started-sequence that has already reached the subscriber. */
|
|
151
155
|
delivered: number;
|
|
152
156
|
};
|
|
157
|
+
/**
|
|
158
|
+
* The default ceiling on subscriptions one socket may hold.
|
|
159
|
+
*
|
|
160
|
+
* Each collection subscription is a refetch on every write to its collection;
|
|
161
|
+
* without a ceiling one socket could open tens of thousands and turn every
|
|
162
|
+
* write anyone made into as many transactions. The SDK shares identical
|
|
163
|
+
* subscriptions on a socket, so a real page holds one per distinct list or
|
|
164
|
+
* record on screen — the admin panel's tables, forms and reference previews
|
|
165
|
+
* stay well under this. See `REALTIME_MAX_SUBSCRIPTIONS_PER_SOCKET`.
|
|
166
|
+
*/
|
|
167
|
+
export declare const DEFAULT_MAX_SUBSCRIPTIONS_PER_SOCKET = 1000;
|
|
168
|
+
/**
|
|
169
|
+
* Read a subscription ceiling, refusing anything that is not a positive whole
|
|
170
|
+
* number. A typo here does not fall back to a default: the ceiling is a limit
|
|
171
|
+
* someone chose, and silently running with another one is how it stops
|
|
172
|
+
* meaning anything.
|
|
173
|
+
*/
|
|
174
|
+
export declare function parseMaxSubscriptionsPerSocket(value: unknown, source: string): number;
|
|
153
175
|
/**
|
|
154
176
|
* PostgreSQL-specific realtime service.
|
|
155
177
|
* Handles WebSocket connections and subscriptions for real-time row updates.
|
|
@@ -188,6 +210,8 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
188
210
|
* wait on each other.
|
|
189
211
|
*/
|
|
190
212
|
private channelSendQueues;
|
|
213
|
+
/** The receiving half of the same ordering: retained bus frames per channel, one at a time. */
|
|
214
|
+
private channelReceiveQueues;
|
|
191
215
|
/**
|
|
192
216
|
* Cross-instance transport for channel frames and presence.
|
|
193
217
|
*
|
|
@@ -233,21 +257,46 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
233
257
|
/** How often stale roster rows from other instances are reaped. */
|
|
234
258
|
private static readonly PRESENCE_SWEEP_INTERVAL_MS;
|
|
235
259
|
private dataService;
|
|
260
|
+
/**
|
|
261
|
+
* Every subscription, by {@link subscriptionKey} — the client's own id
|
|
262
|
+
* qualified by the client that chose it.
|
|
263
|
+
*
|
|
264
|
+
* Keyed by the bare id, this was one namespace for every socket: two raw
|
|
265
|
+
* clients that both counted from `"sub-1"` replaced each other's
|
|
266
|
+
* subscription without a word, and either one's `unsubscribe` ended the
|
|
267
|
+
* other's.
|
|
268
|
+
*/
|
|
236
269
|
private _subscriptions;
|
|
270
|
+
/** In-process listeners' callbacks, by subscription key. */
|
|
237
271
|
private subscriptionCallbacks;
|
|
272
|
+
/** Each client's subscriptions, by key — for its ceiling and its disconnect. */
|
|
273
|
+
private subscriptionsByClient;
|
|
274
|
+
/** Subscriptions one refetch answers — see {@link SubscriptionGroup}. */
|
|
275
|
+
private groups;
|
|
276
|
+
/**
|
|
277
|
+
* The collection groups on each path, and the single-row groups at each
|
|
278
|
+
* `path` + id: what a change looks up, instead of walking every
|
|
279
|
+
* subscription on the server once per changed row.
|
|
280
|
+
*/
|
|
281
|
+
private collectionGroupsByPath;
|
|
282
|
+
private singleGroupsByAddress;
|
|
283
|
+
/**
|
|
284
|
+
* Every path a subscription holds, with the collection it lands on and how
|
|
285
|
+
* many hold it — the candidates `aliasPaths` considers, resolved once.
|
|
286
|
+
*/
|
|
287
|
+
private subscribedPaths;
|
|
288
|
+
/**
|
|
289
|
+
* How many subscriptions one socket may hold; set from config at boot. See
|
|
290
|
+
* {@link DEFAULT_MAX_SUBSCRIPTIONS_PER_SOCKET}.
|
|
291
|
+
*/
|
|
292
|
+
maxSubscriptionsPerSocket: number;
|
|
238
293
|
private driver?;
|
|
239
294
|
/** Unique identifier for this process instance, used to skip own notifications. */
|
|
240
295
|
private readonly instanceId;
|
|
241
|
-
/**
|
|
242
|
-
private
|
|
243
|
-
/** Connection string used for reconnecting the LISTEN client. */
|
|
244
|
-
private listenConnectionString?;
|
|
296
|
+
/** The LISTEN connection on {@link PG_NOTIFY_CHANNEL}, outside the Drizzle pool. */
|
|
297
|
+
private crossInstanceListener?;
|
|
245
298
|
/** Whether cross-instance broadcasting is active. */
|
|
246
299
|
private broadcasting;
|
|
247
|
-
/** Reconnection timer handle. */
|
|
248
|
-
private reconnectTimer?;
|
|
249
|
-
/** Debounce timers for collection refetches to prevent refetch storms. */
|
|
250
|
-
private refetchTimers;
|
|
251
300
|
/** Debounce window (ms) for coalescing rapid row updates into a single correctness refetch. */
|
|
252
301
|
private static readonly REFETCH_DEBOUNCE_MS;
|
|
253
302
|
/** Dedicated LISTEN client for DB-level change events (undefined unless CDC is enabled). */
|
|
@@ -267,7 +316,19 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
267
316
|
* and always flow through. Keyed → expiry timestamp (ms).
|
|
268
317
|
*/
|
|
269
318
|
private recentAppEmits;
|
|
270
|
-
/**
|
|
319
|
+
/**
|
|
320
|
+
* How long an app-emit key suppresses its own CDC echo: the refetch
|
|
321
|
+
* debounce, and no longer.
|
|
322
|
+
*
|
|
323
|
+
* The mark cannot tell an echo from another writer's change to the same
|
|
324
|
+
* row, and a save that touches no row of the table (a to-many relation
|
|
325
|
+
* only, an empty payload) is announced with no echo ever coming to consume
|
|
326
|
+
* it. Held for seconds, it swallowed the next change psql, a cron or
|
|
327
|
+
* another instance made to that row. Inside the debounce nothing is lost:
|
|
328
|
+
* the refetch the app emit scheduled has not started yet — its timer fires
|
|
329
|
+
* no earlier than this — and it reads after any commit whose NOTIFY has
|
|
330
|
+
* already arrived. An echo later than that costs one more refetch.
|
|
331
|
+
*/
|
|
271
332
|
private static readonly CDC_DEDUP_WINDOW_MS;
|
|
272
333
|
constructor(db: NodePgDatabase<any>, registry: PostgresCollectionRegistry);
|
|
273
334
|
/**
|
|
@@ -289,7 +350,50 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
289
350
|
*/
|
|
290
351
|
private debugLog;
|
|
291
352
|
setDataDriver(driver: DataDriver): void;
|
|
292
|
-
|
|
353
|
+
/**
|
|
354
|
+
* The live subscriptions by the id their client gave them — for tests and
|
|
355
|
+
* diagnostics. Two clients may use one id; this view shows the last.
|
|
356
|
+
*/
|
|
357
|
+
get subscriptions(): ReadonlyMap<string, Subscription>;
|
|
358
|
+
/** The internal key of `subscriptionId` as `clientId` named it. */
|
|
359
|
+
private subscriptionKey;
|
|
360
|
+
/**
|
|
361
|
+
* The group a subscription's refetch belongs to. Socket subscriptions that
|
|
362
|
+
* would run the same read as the same principal share one; an in-process
|
|
363
|
+
* listener is always alone — see {@link SubscriptionGroup}.
|
|
364
|
+
*/
|
|
365
|
+
private groupKeyFor;
|
|
366
|
+
/**
|
|
367
|
+
* Register (or replace) a subscription under `key`, in its group and in
|
|
368
|
+
* every index that finds it.
|
|
369
|
+
*/
|
|
370
|
+
private registerSubscription;
|
|
371
|
+
/** Remove the subscription under `key` from everything that holds it. */
|
|
372
|
+
private dropSubscription;
|
|
373
|
+
/** Take `subscription` out of its group and path count; a group left empty goes, timer and all. */
|
|
374
|
+
private leaveGroup;
|
|
375
|
+
/**
|
|
376
|
+
* Ask for a refetch of `group` once changes have been quiet for the
|
|
377
|
+
* debounce window — for every member, or only for `member`.
|
|
378
|
+
*
|
|
379
|
+
* The window is measured from the last change, as a debounce is, but the
|
|
380
|
+
* timer is not re-armed per change: a 10k-row statement is 10k changes,
|
|
381
|
+
* and re-arming a timer per change per group was work in proportion to
|
|
382
|
+
* both. A change only moves the timestamp; the timer, when it fires early,
|
|
383
|
+
* waits out the rest.
|
|
384
|
+
*/
|
|
385
|
+
private scheduleGroupRefetch;
|
|
386
|
+
private armGroupTimer;
|
|
387
|
+
/**
|
|
388
|
+
* One read for the group, delivered to each member it is for.
|
|
389
|
+
*
|
|
390
|
+
* Each member still claims its own delivery slot before the read and
|
|
391
|
+
* checks it after (see {@link beginDelivery}), so a member that left, was
|
|
392
|
+
* replaced or was overtaken while the read ran is skipped exactly as it
|
|
393
|
+
* was when it had a read of its own. A socket frame is serialised once and
|
|
394
|
+
* only its subscription id differs per member.
|
|
395
|
+
*/
|
|
396
|
+
private refetchGroup;
|
|
293
397
|
/**
|
|
294
398
|
* Claim a delivery slot for a subscription, before doing the work.
|
|
295
399
|
*
|
|
@@ -343,13 +447,27 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
343
447
|
*/
|
|
344
448
|
subscribeToOne(subscriptionId: string, config: SingleSubscriptionConfig, callback?: (row: Record<string, unknown> | null) => void, onError?: (error: unknown) => void): void;
|
|
345
449
|
/**
|
|
346
|
-
* Unsubscribe
|
|
450
|
+
* Unsubscribe an in-process subscription (RealtimeProvider interface).
|
|
451
|
+
*
|
|
452
|
+
* By id alone, because the in-process callers hold nothing else — so a
|
|
453
|
+
* socket's subscription under the same id is not this one, and is left
|
|
454
|
+
* alone. A socket ends its own through the `unsubscribe` frame.
|
|
347
455
|
*/
|
|
348
456
|
unsubscribe(subscriptionId: string): void;
|
|
349
457
|
addClient(clientId: string, ws: WebSocket): void;
|
|
350
458
|
handleClientMessage(clientId: string, message: WebSocketMessage, authContext?: SubscriptionAuthContext): Promise<void>;
|
|
351
459
|
removeClient(clientId: string): Promise<void>;
|
|
352
460
|
private handleMessage;
|
|
461
|
+
/**
|
|
462
|
+
* Refuse a new subscription past the socket's ceiling, and say so.
|
|
463
|
+
*
|
|
464
|
+
* Every collection subscription is a refetch on every write to its
|
|
465
|
+
* collection, so a socket allowed to open them without limit could turn
|
|
466
|
+
* each write anyone makes into tens of thousands of transactions. Replacing
|
|
467
|
+
* a subscription the socket already holds — the same id again — is not a
|
|
468
|
+
* new one and always passes. Returns whether the subscription may go ahead.
|
|
469
|
+
*/
|
|
470
|
+
private admitSubscription;
|
|
353
471
|
private handleCollectionSubscription;
|
|
354
472
|
private handleEntitySubscription;
|
|
355
473
|
/**
|
|
@@ -366,6 +484,29 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
366
484
|
* synchronously, so nothing else can have answered the request yet.
|
|
367
485
|
*/
|
|
368
486
|
private reportSocketFetchFailure;
|
|
487
|
+
/**
|
|
488
|
+
* Re-judge everything a socket holds open as the identity it has now.
|
|
489
|
+
*
|
|
490
|
+
* A socket re-authenticates on every session refresh, and when a role is
|
|
491
|
+
* taken away, a tenant claim removed or a different account signs in, the
|
|
492
|
+
* next token says so. Requests read as the new identity at once; the
|
|
493
|
+
* subscriptions kept refetching as the one they were opened with, so a
|
|
494
|
+
* demoted user went on receiving editor-only or another tenant's rows for
|
|
495
|
+
* as long as the view stayed mounted.
|
|
496
|
+
*
|
|
497
|
+
* Each of this client's subscriptions is replaced by one carrying the new
|
|
498
|
+
* identity — a replacement, so a refetch already running as the old one
|
|
499
|
+
* cannot deliver — and refetched, so the view narrows now rather than at
|
|
500
|
+
* the next write. A subscription whose filter, sort or projection names a
|
|
501
|
+
* field the new roles may not read is ended with the refusal a new
|
|
502
|
+
* subscription would get. Channel memberships are put to the authorizer
|
|
503
|
+
* again, when one is installed, and left when it refuses.
|
|
504
|
+
*/
|
|
505
|
+
rescopeClient(clientId: string, authContext: SubscriptionAuthContext): Promise<void>;
|
|
506
|
+
/**
|
|
507
|
+
* End a socket's own subscription. Only its own: the id is the client's,
|
|
508
|
+
* so another socket's subscription under the same id is a different one.
|
|
509
|
+
*/
|
|
369
510
|
private handleUnsubscribe;
|
|
370
511
|
/**
|
|
371
512
|
* Enhanced notification method that handles nested relation updates.
|
|
@@ -415,15 +556,6 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
415
556
|
* whether this subscriber may see the row at all.
|
|
416
557
|
*/
|
|
417
558
|
private notifyPathUpdate;
|
|
418
|
-
/**
|
|
419
|
-
* Debounce a collection refetch for a WebSocket subscription.
|
|
420
|
-
* Coalesces rapid row mutations into a single database query.
|
|
421
|
-
*/
|
|
422
|
-
private debouncedCollectionRefetch;
|
|
423
|
-
/**
|
|
424
|
-
* Debounce a collection refetch for a DataDriver callback subscription.
|
|
425
|
-
*/
|
|
426
|
-
private debouncedDriverRefetch;
|
|
427
559
|
/**
|
|
428
560
|
* Tell an in-process listener its refetch failed, through the slot the
|
|
429
561
|
* rows would have used.
|
|
@@ -453,19 +585,17 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
453
585
|
* the total describes the same set they came from.
|
|
454
586
|
*/
|
|
455
587
|
private collectionMetaWithAuth;
|
|
456
|
-
/**
|
|
457
|
-
* Debounce an row refetch for a WebSocket subscription.
|
|
458
|
-
*/
|
|
459
|
-
private debouncedSingleRefetch;
|
|
460
|
-
/**
|
|
461
|
-
* Debounce an row refetch for a Driver callback subscription.
|
|
462
|
-
*/
|
|
463
|
-
private debouncedSingleDriverRefetch;
|
|
464
588
|
/**
|
|
465
589
|
* Fetch a single row with optional RLS auth context.
|
|
466
590
|
*/
|
|
467
591
|
private fetchEntityWithAuth;
|
|
468
592
|
private sendCollectionUpdate;
|
|
593
|
+
/**
|
|
594
|
+
* 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.
|
|
597
|
+
*/
|
|
598
|
+
private collectionFrameTail;
|
|
469
599
|
private sendSingleUpdate;
|
|
470
600
|
/**
|
|
471
601
|
* Send a lightweight row-level patch to a collection subscriber.
|
|
@@ -493,10 +623,36 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
493
623
|
*/
|
|
494
624
|
private sendError;
|
|
495
625
|
private sendMessage;
|
|
626
|
+
/**
|
|
627
|
+
* Send an already-serialised frame to a client, if it is still connected.
|
|
628
|
+
*
|
|
629
|
+
* Every frame to a socket goes through here, so this is where a client
|
|
630
|
+
* that does not read what it is sent is let go of — see
|
|
631
|
+
* {@link terminateIfBacklogged}. Terminated, it closes like any other
|
|
632
|
+
* socket, and {@link removeClient} drops what it held.
|
|
633
|
+
*/
|
|
634
|
+
private sendRaw;
|
|
496
635
|
/**
|
|
497
636
|
* Extract parent paths from a nested path like "posts/70/tags"
|
|
498
637
|
* Returns ["posts", "posts/70"] for the example above
|
|
499
638
|
*/
|
|
639
|
+
/**
|
|
640
|
+
* The other paths a subscriber can address the row written at `path` by.
|
|
641
|
+
*
|
|
642
|
+
* `authors/1/posts` and `posts` are two addresses for the same rows, and
|
|
643
|
+
* subscriptions were matched by the written path's exact string. So a post
|
|
644
|
+
* saved through `authors/1/posts` never reached a subscriber of `posts` or
|
|
645
|
+
* of `posts/43`, and one saved through `posts` never reached a subscriber
|
|
646
|
+
* of `authors/1/posts`, whose list it may just have joined or left.
|
|
647
|
+
*
|
|
648
|
+
* The aliases are the target collection's root path, and every nested path
|
|
649
|
+
* a live subscription holds that lands on the same collection. Each still
|
|
650
|
+
* refetches under its own scope and its own parent, so naming a path here
|
|
651
|
+
* decides only who is asked to look again — never what they are shown.
|
|
652
|
+
*/
|
|
653
|
+
private aliasPaths;
|
|
654
|
+
/** The slug of the collection a path lands on, or `undefined` for one that names none. */
|
|
655
|
+
private collectionSlugAt;
|
|
500
656
|
private getParentPaths;
|
|
501
657
|
/**
|
|
502
658
|
* Install a channel authorizer — see {@link ChannelAuthorizer}.
|
|
@@ -620,6 +776,7 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
620
776
|
* its own `sid`.
|
|
621
777
|
*/
|
|
622
778
|
private handleBusFrame;
|
|
779
|
+
private deliverBusFrame;
|
|
623
780
|
/**
|
|
624
781
|
* Install retention rules and create the tables they need.
|
|
625
782
|
*
|
|
@@ -705,12 +862,20 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
705
862
|
* 1. All debounced refetch timers are cancelled (prevents queries after pool closes).
|
|
706
863
|
* 2. All subscription state and callbacks are cleared.
|
|
707
864
|
* 3. The dedicated LISTEN client (outside the pool) is disconnected.
|
|
708
|
-
* 4. All WebSocket clients are
|
|
709
|
-
*
|
|
865
|
+
* 4. All WebSocket clients are closed with 1001 ("going away"); one that
|
|
866
|
+
* does not answer the close frame is dropped after a short grace.
|
|
710
867
|
*/
|
|
711
868
|
destroy(): Promise<void>;
|
|
712
|
-
/**
|
|
869
|
+
/**
|
|
870
|
+
* Whether database-level change capture is the source and is listening.
|
|
871
|
+
*
|
|
872
|
+
* Configured is not enough: a CDC connection that went half-open used to
|
|
873
|
+
* leave this `true` while nothing arrived. False while the connection is
|
|
874
|
+
* down and being replaced.
|
|
875
|
+
*/
|
|
713
876
|
isCdcActive(): boolean;
|
|
877
|
+
/** The LISTEN connections this service depends on — see `RealtimeProvider.health`. */
|
|
878
|
+
health(): RealtimeListenerHealth[];
|
|
714
879
|
/**
|
|
715
880
|
* Enable database-level change capture as the realtime source.
|
|
716
881
|
*
|
|
@@ -729,6 +894,18 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
729
894
|
* (bypass PgBouncer — LISTEN needs a session connection).
|
|
730
895
|
*/
|
|
731
896
|
enableCdc(connectionString: string): Promise<void>;
|
|
897
|
+
/**
|
|
898
|
+
* Refetch every live subscription, as a change to its path would.
|
|
899
|
+
*
|
|
900
|
+
* Called when a LISTEN connection (CDC, or the cross-instance broadcast)
|
|
901
|
+
* is listening again after a drop. Postgres does not queue NOTIFY for a
|
|
902
|
+
* session that is not listening, so every change committed while it was
|
|
903
|
+
* down — by another instance, psql, a cron — reached nobody here, and a
|
|
904
|
+
* subscriber held its pre-gap rows until some later change to the same
|
|
905
|
+
* collection happened to arrive. Each refetch runs under the
|
|
906
|
+
* subscription's own scope, so this decides only who looks again.
|
|
907
|
+
*/
|
|
908
|
+
private resyncSubscriptions;
|
|
732
909
|
/** Stop the CDC listener and clear its state. */
|
|
733
910
|
stopCdc(): Promise<void>;
|
|
734
911
|
/**
|
|
@@ -742,8 +919,8 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
742
919
|
/**
|
|
743
920
|
* Route a captured database change into the realtime pipeline.
|
|
744
921
|
*
|
|
745
|
-
* Delivery is RLS-safe by construction: the
|
|
746
|
-
* NOT forwarded to subscribers. Instead the change is marked invalidated, so
|
|
922
|
+
* Delivery is RLS-safe by construction: the event carries the changed row's
|
|
923
|
+
* key and nothing else, and even that is NOT forwarded to subscribers. Instead the change is marked invalidated, so
|
|
747
924
|
* every matching subscription re-reads the row under its own auth context via
|
|
748
925
|
* {@link fetchCollectionWithAuth} / {@link fetchEntityWithAuth}. A subscriber
|
|
749
926
|
* therefore only ever receives rows its RLS policies permit — filtering is per
|
|
@@ -769,6 +946,16 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
769
946
|
private handleJunctionCdcEvent;
|
|
770
947
|
/** Compute the canonical (possibly composite) id string from a captured row. */
|
|
771
948
|
private extractIdFromCdcRow;
|
|
949
|
+
/**
|
|
950
|
+
* A captured row with its key under the key's *field* names.
|
|
951
|
+
*
|
|
952
|
+
* The trigger captures the tuple by column — `user_id` — and an address is
|
|
953
|
+
* built from the key's property names — `userId`. Read by property name
|
|
954
|
+
* alone, a key declared apart from its column was never on the captured
|
|
955
|
+
* row, the address fell back to `*`, and no single-row subscriber heard
|
|
956
|
+
* about any write made outside this server.
|
|
957
|
+
*/
|
|
958
|
+
private cdcRowByKeyFields;
|
|
772
959
|
private dedupKey;
|
|
773
960
|
/** Record that this instance just delivered `key` via the app path. */
|
|
774
961
|
private markAppEmit;
|
|
@@ -776,8 +963,15 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
776
963
|
private consumeAppEmit;
|
|
777
964
|
/**
|
|
778
965
|
* Enable cross-instance realtime broadcasting via Postgres LISTEN/NOTIFY.
|
|
779
|
-
*
|
|
780
|
-
*
|
|
966
|
+
* Listens on a dedicated connection (outside the Drizzle pool) for the
|
|
967
|
+
* changes other instances publish, through a {@link PgNotifyListener}: it
|
|
968
|
+
* proves itself with a heartbeat, is replaced when it drops or stops
|
|
969
|
+
* answering, and every subscription is refetched once it is back, since
|
|
970
|
+
* whatever was published meanwhile reached nobody here.
|
|
971
|
+
*
|
|
972
|
+
* A database that cannot be reached at boot does not fail this call: the
|
|
973
|
+
* listener reports itself down (see {@link health}) and keeps dialling.
|
|
974
|
+
* There is no other cross-instance path to fall back to.
|
|
781
975
|
*
|
|
782
976
|
* This is an **optional** feature — if never called, the backend operates
|
|
783
977
|
* in single-instance mode (the default, perfectly fine for most setups).
|
|
@@ -794,14 +988,8 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
794
988
|
* Uses the main Drizzle connection (pooled) — NOT the LISTEN client.
|
|
795
989
|
*/
|
|
796
990
|
private broadcastChange;
|
|
797
|
-
/**
|
|
798
|
-
|
|
799
|
-
*/
|
|
800
|
-
private connectListenClient;
|
|
801
|
-
/**
|
|
802
|
-
* Schedule a reconnection attempt with a fixed 3s delay.
|
|
803
|
-
*/
|
|
804
|
-
private scheduleReconnect;
|
|
991
|
+
/** One change another instance published on {@link PG_NOTIFY_CHANNEL}. */
|
|
992
|
+
private handleCrossInstanceNotification;
|
|
805
993
|
}
|
|
806
994
|
/**
|
|
807
995
|
* Alias for RealtimeService for consistent naming with other database implementations.
|
|
@@ -59,6 +59,11 @@ export declare function toRestValues(row: Record<string, unknown>, collection: C
|
|
|
59
59
|
* an `update` that echoes the row back overwrite the real value with the null it
|
|
60
60
|
* was handed.
|
|
61
61
|
*
|
|
62
|
+
* A withheld to-one relation takes its foreign key with it. `bandId: 7` names
|
|
63
|
+
* the salary band exactly as `band: { id: 7 }` does, and the row carries the
|
|
64
|
+
* column beside the relation — so withholding only the relation withheld
|
|
65
|
+
* nothing.
|
|
66
|
+
*
|
|
62
67
|
* `_matches` is filtered rather than deleted: it is the list of fields a text
|
|
63
68
|
* search hit, and a field the caller cannot read must not appear in it even
|
|
64
69
|
* though the array itself is theirs to see.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Letting go of client sockets the server can no longer reach.
|
|
3
|
+
*
|
|
4
|
+
* A phone that loses signal, a laptop that sleeps, a NAT that forgets the
|
|
5
|
+
* connection: the peer is gone, and nothing tells the server. No close frame
|
|
6
|
+
* arrives and no FIN, so the socket stays open until the operating system gives
|
|
7
|
+
* up on the TCP connection — about two hours on Linux. Meanwhile every
|
|
8
|
+
* subscription it holds is refetched on every write to its collection, and
|
|
9
|
+
* every frame for it is serialised into a send buffer nobody drains.
|
|
10
|
+
*
|
|
11
|
+
* Two checks catch it. The socket must answer a ping: one that has not answered
|
|
12
|
+
* by the next is terminated. And it must read what it is sent: one whose unsent
|
|
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`.
|
|
16
|
+
*/
|
|
17
|
+
import { WebSocket, type WebSocketServer } from "ws";
|
|
18
|
+
/**
|
|
19
|
+
* How often every socket is pinged. A socket that has not answered one ping by
|
|
20
|
+
* the next is terminated, so a peer that vanished is let go of within two
|
|
21
|
+
* intervals. Every browser and `ws` client answers a ping by itself.
|
|
22
|
+
*/
|
|
23
|
+
export declare const SOCKET_PING_INTERVAL_MS = 30000;
|
|
24
|
+
/**
|
|
25
|
+
* The unsent bytes a socket may have queued before it is terminated.
|
|
26
|
+
*
|
|
27
|
+
* A healthy client drains its queue as fast as the network allows; one whose
|
|
28
|
+
* backlog keeps growing is not reading, or is so far behind that what it would
|
|
29
|
+
* read is stale. Set well above a single frame — the largest body the data API
|
|
30
|
+
* accepts is 10 MiB, and a list frame can approach it — so a burst of large
|
|
31
|
+
* frames to a client that is keeping up does not trip it. A terminated client
|
|
32
|
+
* reconnects and resubscribes, which brings it up to date with one frame per
|
|
33
|
+
* subscription instead of the backlog.
|
|
34
|
+
*/
|
|
35
|
+
export declare const MAX_SOCKET_BUFFERED_BYTES: number;
|
|
36
|
+
/**
|
|
37
|
+
* Ping every socket of `wss` each `intervalMs`, and terminate the ones that did
|
|
38
|
+
* not answer the previous ping. Returns the function that stops it.
|
|
39
|
+
*/
|
|
40
|
+
export declare function reapSilentSockets(wss: WebSocketServer, intervalMs?: number): () => void;
|
|
41
|
+
/**
|
|
42
|
+
* Terminate `ws` if its unsent backlog has passed {@link MAX_SOCKET_BUFFERED_BYTES}.
|
|
43
|
+
* Returns whether it did; the caller then sends nothing.
|
|
44
|
+
*/
|
|
45
|
+
export declare function terminateIfBacklogged(ws: WebSocket, clientId: string): boolean;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SQL } from "drizzle-orm";
|
|
2
2
|
import type { AnyPgColumn, PgTable } from "drizzle-orm/pg-core";
|
|
3
3
|
import type { CollectionConfig } from "@rebasepro/types";
|
|
4
|
+
import { ApiError } from "@rebasepro/server";
|
|
4
5
|
/**
|
|
5
6
|
* Soft delete, in one place.
|
|
6
7
|
*
|
|
@@ -23,8 +24,6 @@ import type { CollectionConfig } from "@rebasepro/types";
|
|
|
23
24
|
* (`assertCollectionConfigs`), where the answer is a config change, rather than
|
|
24
25
|
* at the first delete, where it would be a 500 for whoever pressed the button.
|
|
25
26
|
*/
|
|
26
|
-
/** The default field, when `softDelete: true` names none. */
|
|
27
|
-
export declare const DEFAULT_SOFT_DELETE_FIELD = "deletedAt";
|
|
28
27
|
/** How a caller asks about deleted rows. See `FetchCollectionProps.withDeleted`. */
|
|
29
28
|
export type WithDeleted = boolean | "only" | undefined;
|
|
30
29
|
export interface SoftDeleteField {
|
|
@@ -71,3 +70,14 @@ export declare function withSoftDelete(conditions: SQL[], collection: Collection
|
|
|
71
70
|
* For the call sites that hold one composed condition rather than a list.
|
|
72
71
|
*/
|
|
73
72
|
export declare function andSoftDelete(where: SQL | undefined, collection: CollectionConfig | undefined, table: PgTable, withDeleted?: WithDeleted): SQL | undefined;
|
|
73
|
+
/**
|
|
74
|
+
* The refusal of an upsert whose key belongs to a row in the trash.
|
|
75
|
+
*
|
|
76
|
+
* The key is taken — the stamped row still holds it — so the write cannot be a
|
|
77
|
+
* create, and an upsert is not the operation that brings a deleted row back:
|
|
78
|
+
* that is the restore, an update setting the field to `null`, which goes
|
|
79
|
+
* through the update gate and is what history records as one. Left to the
|
|
80
|
+
* statement, `INSERT … ON CONFLICT DO UPDATE` wrote the new values into the
|
|
81
|
+
* hidden row and the caller was told "created", with the row still in the trash.
|
|
82
|
+
*/
|
|
83
|
+
export declare function rowInTrashError(path: string, key: string, softDelete: SoftDeleteField): ApiError;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { Pool, PoolClient } from "pg";
|
|
2
|
+
import type { SqlScriptResult } from "@rebasepro/types";
|
|
3
|
+
/**
|
|
4
|
+
* `INSERT 0 1` → `INSERT`, 1. `CREATE TABLE` → `CREATE TABLE`, no count.
|
|
5
|
+
*
|
|
6
|
+
* Only the commands whose tag ends in a count have one; the numbers are
|
|
7
|
+
* stripped from the end of every other tag, of which there are none.
|
|
8
|
+
*/
|
|
9
|
+
export declare function parseCommandTag(tag: string | undefined): {
|
|
10
|
+
command?: string;
|
|
11
|
+
rowCount?: number;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* What puts a session back after caller SQL: the session user and role, then
|
|
15
|
+
* every setting. Two statements, because the extended protocol refuses a
|
|
16
|
+
* multi-command string. Pinned settings (`search_path` from the connection's
|
|
17
|
+
* startup options) come back as they were pinned.
|
|
18
|
+
*/
|
|
19
|
+
export declare const RESET_SESSION_STATEMENTS: readonly ["SET SESSION AUTHORIZATION DEFAULT", "RESET ALL"];
|
|
20
|
+
/**
|
|
21
|
+
* A connection's transaction state, as its last ReadyForQuery reported it:
|
|
22
|
+
* `I` idle, `T` in a transaction, `E` in a failed one. `null` when the client
|
|
23
|
+
* does not say (node-postgres has since 8.16; the package requires 8.22).
|
|
24
|
+
*/
|
|
25
|
+
export declare function transactionStatus(connection: object): string | null;
|
|
26
|
+
/**
|
|
27
|
+
* SQL a person ran began a transaction and did not end it.
|
|
28
|
+
*
|
|
29
|
+
* Each run is a session of its own, so a transaction cannot outlive the run
|
|
30
|
+
* that began it: a `BEGIN` alone, then an `UPDATE` in the next run, then a
|
|
31
|
+
* `ROLLBACK` in a third, committed the UPDATE and reported three successes.
|
|
32
|
+
* Refused instead, with the transaction rolled back.
|
|
33
|
+
*/
|
|
34
|
+
export declare class SqlTransactionLeftOpenError extends Error {
|
|
35
|
+
readonly code = "SQL_TRANSACTION_LEFT_OPEN";
|
|
36
|
+
constructor();
|
|
37
|
+
}
|
|
38
|
+
export interface OwnSessionOptions {
|
|
39
|
+
/**
|
|
40
|
+
* Put the session in the role the SQL runs as. Called once, before it, at
|
|
41
|
+
* session level — so the role holds for every statement, a `COMMIT` in the
|
|
42
|
+
* middle included. `SET LOCAL ROLE` inside a wrapping transaction did not:
|
|
43
|
+
* the caller's `COMMIT` ended that transaction, and every statement after
|
|
44
|
+
* it ran as the connection owner.
|
|
45
|
+
*/
|
|
46
|
+
assumeRole?: (connection: PoolClient) => Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Run `work` — SQL a person wrote — on a connection of its own from `pool`,
|
|
50
|
+
* and put the connection back the way it was handed out.
|
|
51
|
+
*
|
|
52
|
+
* A transaction the SQL left open is rolled back, and the run refused with
|
|
53
|
+
* {@link SqlTransactionLeftOpenError}; a run that failed with one open says
|
|
54
|
+
* that it was rolled back. The session is then reset — role, session
|
|
55
|
+
* authorization, settings — and destroyed instead when it cannot be. Last,
|
|
56
|
+
* `afterReset` reads what it needs on the clean session, as the owner.
|
|
57
|
+
*/
|
|
58
|
+
export declare function onOwnSession<T>(pool: Pool, options: OwnSessionOptions, work: (connection: PoolClient) => Promise<T>, afterReset?: (connection: PoolClient, value: T) => Promise<T>): Promise<T>;
|
|
59
|
+
/**
|
|
60
|
+
* Run `sql` on a connection of its own from `pool` and return what the last
|
|
61
|
+
* statement returned, with each column's provenance and what the database
|
|
62
|
+
* said along the way — `there is no transaction in progress` for a ROLLBACK
|
|
63
|
+
* with nothing to roll back, which used to read as success.
|
|
64
|
+
*/
|
|
65
|
+
export declare function runSqlScriptOnPool(pool: Pool, sql: string, options?: OwnSessionOptions): Promise<SqlScriptResult>;
|
|
66
|
+
/**
|
|
67
|
+
* The same shape for rows that came back parsed, from a handle that is not a
|
|
68
|
+
* pool (PGlite in process): every value as text, and no column with a source —
|
|
69
|
+
* nothing to say where a value came from, so nothing the console will edit.
|
|
70
|
+
*/
|
|
71
|
+
export declare function sqlScriptResultFromRows(rows: Record<string, unknown>[]): SqlScriptResult;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep enough for any chain that ends (a hook writing another collection whose
|
|
3
|
+
* hook writes a third is depth 3), shallow enough that a loop stops at once.
|
|
4
|
+
*/
|
|
5
|
+
export declare const MAX_WRITE_DEPTH = 16;
|
|
6
|
+
/** One write in progress, and which of its hooks is running. */
|
|
7
|
+
export interface WriteFrame {
|
|
8
|
+
readonly path: string;
|
|
9
|
+
readonly operation: "save" | "delete";
|
|
10
|
+
readonly depth: number;
|
|
11
|
+
readonly parent: WriteFrame | undefined;
|
|
12
|
+
/** The hook running right now — what a nested write was started from. */
|
|
13
|
+
stage: string | undefined;
|
|
14
|
+
}
|
|
15
|
+
/** Run one write as a frame, refusing it when the chain is already too deep. */
|
|
16
|
+
export declare function inWriteFrame<T>(path: string, operation: "save" | "delete", fn: (frame: WriteFrame) => Promise<T>): Promise<T>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type AmbientTransaction } from "@rebasepro/server";
|
|
2
|
+
import type { RebaseCallContext } from "@rebasepro/types";
|
|
2
3
|
import type { DrizzleClient } from "../interfaces.js";
|
|
3
4
|
type RunSql = (sqlText: string, params?: unknown[]) => Promise<Record<string, unknown>[]>;
|
|
4
5
|
/**
|
|
@@ -24,6 +25,10 @@ export declare class WriteTransactionScope implements AmbientTransaction {
|
|
|
24
25
|
private open;
|
|
25
26
|
private readonly inflight;
|
|
26
27
|
private readonly commitHooks;
|
|
28
|
+
/** The first tracked statement that failed, whether or not its caller caught it. */
|
|
29
|
+
private failure?;
|
|
30
|
+
private readonly settledHooks;
|
|
31
|
+
private settledContext?;
|
|
27
32
|
/** The transaction handle, for statements in this package that are already written against drizzle. */
|
|
28
33
|
tx?: DrizzleClient;
|
|
29
34
|
/** Attach the transaction, once it has begun. */
|
|
@@ -49,6 +54,43 @@ export declare class WriteTransactionScope implements AmbientTransaction {
|
|
|
49
54
|
* connection, as if outside any write.
|
|
50
55
|
*/
|
|
51
56
|
settle(): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Refuse to commit a transaction a failed statement has aborted.
|
|
59
|
+
*
|
|
60
|
+
* Once any statement fails, Postgres aborts the whole transaction, and
|
|
61
|
+
* catching the error in JavaScript does not undo that. The COMMIT that
|
|
62
|
+
* follows is answered with the tag `ROLLBACK` and no error, so the driver
|
|
63
|
+
* resolves it: the write reported success — 200, webhooks, realtime,
|
|
64
|
+
* the idempotency record — while nothing was stored. A callback that
|
|
65
|
+
* wrapped a lookup or a job enqueue in `try/catch` was enough.
|
|
66
|
+
*
|
|
67
|
+
* So one statement is asked of the transaction before its commit. On an
|
|
68
|
+
* aborted one it fails with `25P02`, and the write is refused and rolled
|
|
69
|
+
* back instead. The database refusing a `context.data` create, update or
|
|
70
|
+
* delete is not caught by this: each of those statements runs in a
|
|
71
|
+
* savepoint, is undone on its own, and leaves the transaction usable —
|
|
72
|
+
* which is what makes catching one safe. (Deletes ran without one, so a
|
|
73
|
+
* caught refused delete landed here, under a message saying catching it
|
|
74
|
+
* was safe.)
|
|
75
|
+
*/
|
|
76
|
+
assertCommittable(): Promise<void>;
|
|
77
|
+
/**
|
|
78
|
+
* What a hook deferred with {@link afterSettled} is handed as `context`:
|
|
79
|
+
* the same caller, on a driver where every call is a transaction of its
|
|
80
|
+
* own. The write's transaction is gone by the time the hook runs, so a
|
|
81
|
+
* context bound to it would be bound to a connection back in the pool.
|
|
82
|
+
*/
|
|
83
|
+
setSettledContext(context: RebaseCallContext): void;
|
|
84
|
+
/**
|
|
85
|
+
* Run `fn` once this write's transaction is over, whichever way it went,
|
|
86
|
+
* outside it — for what a failure hook hands off (a job, a queue message,
|
|
87
|
+
* a webhook), which must not ride the transaction the failure rolls back.
|
|
88
|
+
* `false` when this scope has no context to hand it; the caller runs it
|
|
89
|
+
* itself then.
|
|
90
|
+
*/
|
|
91
|
+
afterSettled(fn: (context: RebaseCallContext) => Promise<void>): boolean;
|
|
92
|
+
/** The transaction committed or rolled back: run what was waiting for either. */
|
|
93
|
+
settled(): Promise<void>;
|
|
52
94
|
/**
|
|
53
95
|
* The transaction is over, whichever way it went. Nothing may reach it now:
|
|
54
96
|
* its client is back in the pool, and a callback's detached promise still
|