@rebasepro/server-postgres 0.23.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-DGPL6G_C.js → BranchService-BRt78gfa.js} +35 -18
- package/dist/BranchService-BRt78gfa.js.map +1 -0
- package/dist/PostgresBackendDriver.d.ts +84 -12
- package/dist/auth/services.d.ts +72 -14
- 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-cli-24v4OlSp.js → backup-cli-DW5p9_zv.js} +2 -2
- package/dist/{backup-cli-24v4OlSp.js.map → backup-cli-DW5p9_zv.js.map} +1 -1
- package/dist/{backup-service-HQ9GC4tN.js → backup-service-EwcDVG-8.js} +7 -9
- package/dist/{backup-service-HQ9GC4tN.js.map → backup-service-EwcDVG-8.js.map} +1 -1
- package/dist/{cli-errors-B8qHg02P.js → cli-errors-DsA-K9uP.js} +96 -1
- package/dist/{cli-errors-B8qHg02P.js.map → cli-errors-DsA-K9uP.js.map} +1 -1
- package/dist/cli-errors.d.ts +42 -0
- package/dist/cli-helpers.d.ts +12 -12
- package/dist/cli.js +91 -37
- package/dist/cli.js.map +1 -1
- package/dist/{column-plan-helpers-DF-8dTVa.js → column-plan-helpers-1-LQD0yI.js} +34 -24
- package/dist/column-plan-helpers-1-LQD0yI.js.map +1 -0
- package/dist/data-transformer.d.ts +0 -8
- package/dist/{doctor-Cb2thZ8s.js → doctor-C-sYWbmt.js} +224 -43
- package/dist/doctor-C-sYWbmt.js.map +1 -0
- package/dist/{ensure-collection-policies-RHUcEp4v.js → ensure-collection-policies-B1ureSIV.js} +73 -18
- package/dist/ensure-collection-policies-B1ureSIV.js.map +1 -0
- package/dist/{ensure-collection-tables-BEEjn5cn.js → ensure-collection-tables-kkkHk8oo.js} +76 -20
- package/dist/ensure-collection-tables-kkkHk8oo.js.map +1 -0
- package/dist/{ensure-tables-Dhn9KM3B.js → ensure-tables-BmI_tRxc.js} +10 -3
- package/dist/ensure-tables-BmI_tRxc.js.map +1 -0
- package/dist/{generate-drizzle-schema-yzY_BLhr.js → generate-drizzle-schema-93M0lUxK.js} +2 -2
- package/dist/{generate-drizzle-schema-yzY_BLhr.js.map → generate-drizzle-schema-93M0lUxK.js.map} +1 -1
- package/dist/{generate-drizzle-schema-logic-BfzK7UQd.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 +3415 -917
- package/dist/index.es.js.map +1 -1
- package/dist/{introspect-db-logic-WMuAfxvw.js → introspect-db-logic-kCETE8TY.js} +523 -349
- 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-C0fxM8dY.js → plan-schema-DU9exq6C.js} +139 -504
- package/dist/plan-schema-DU9exq6C.js.map +1 -0
- package/dist/{policy-drift-DljYdrpW.js → policy-drift-B-J2hhm0.js} +3 -3
- package/dist/policy-drift-B-J2hhm0.js.map +1 -0
- package/dist/{generate-postgres-ddl-logic-Bt2d2mRH.js → render-ddl-Ds2t_d9V.js} +11 -149
- package/dist/render-ddl-Ds2t_d9V.js.map +1 -0
- package/dist/{rls-bootstrap-sql-B8EclDyM.js → rls-bootstrap-sql-_KNnjanK.js} +263 -39
- 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/auth-schema.d.ts +170 -0
- package/dist/schema/classify-change.d.ts +29 -1
- package/dist/schema/column-plan-helpers.d.ts +33 -21
- package/dist/schema/destructive-sql.d.ts +20 -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 -3
- package/dist/schema/introspect-db.js.map +1 -1
- package/dist/schema/plan/diff-plan.d.ts +1 -1
- package/dist/schema/plan/plan-schema.d.ts +16 -8
- package/dist/schema/plan/render-ddl.d.ts +6 -0
- package/dist/schema/plan/types.d.ts +37 -11
- 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 +5 -4
- package/dist/services/RelationService.d.ts +18 -0
- package/dist/services/cdc/CdcListener.d.ts +9 -4
- 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 +11 -1
- package/dist/services/pg-notify-listener.d.ts +88 -6
- package/dist/services/realtimeService.d.ts +159 -45
- package/dist/services/socket-liveness.d.ts +45 -0
- package/dist/services/soft-delete.d.ts +12 -0
- 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 +6 -3
- package/dist/utils/drizzle-conditions.d.ts +42 -2
- package/dist/utils/sql-redaction.d.ts +21 -0
- package/dist/websocket.d.ts +50 -18
- package/package.json +7 -7
- package/dist/BranchService-DGPL6G_C.js.map +0 -1
- package/dist/column-plan-helpers-DF-8dTVa.js.map +0 -1
- package/dist/doctor-Cb2thZ8s.js.map +0 -1
- package/dist/ensure-collection-policies-RHUcEp4v.js.map +0 -1
- package/dist/ensure-collection-tables-BEEjn5cn.js.map +0 -1
- package/dist/ensure-tables-Dhn9KM3B.js.map +0 -1
- package/dist/generate-drizzle-schema-logic-BfzK7UQd.js.map +0 -1
- package/dist/generate-postgres-ddl-logic-Bt2d2mRH.js.map +0 -1
- package/dist/introspect-db-logic-WMuAfxvw.js.map +0 -1
- package/dist/plan-schema-C0fxM8dY.js.map +0 -1
- package/dist/policy-drift-DljYdrpW.js.map +0 -1
- package/dist/rls-bootstrap-sql-B8EclDyM.js.map +0 -1
- package/dist/rls-enforcement-C6Xk0lA6.js.map +0 -1
|
@@ -23,10 +23,31 @@ export declare class BranchingUnsupportedError extends Error {
|
|
|
23
23
|
readonly code = "BRANCHING_UNSUPPORTED";
|
|
24
24
|
constructor(message: string);
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Where a server's branches are kept: the main database — what a branch is
|
|
28
|
+
* copied from unless told otherwise, and what may never be dropped — and the
|
|
29
|
+
* connection whose `rebase.branches` records them.
|
|
30
|
+
*/
|
|
31
|
+
export interface BranchHome {
|
|
32
|
+
/** The main database's name. */
|
|
33
|
+
database(): Promise<string>;
|
|
34
|
+
/**
|
|
35
|
+
* A connection on the database the registry is in. Asked for at each use:
|
|
36
|
+
* a pool to the main database is closed before it is copied.
|
|
37
|
+
*/
|
|
38
|
+
registry(): Promise<DrizzleClient>;
|
|
39
|
+
}
|
|
26
40
|
export declare class BranchService {
|
|
27
41
|
private db;
|
|
28
42
|
private poolManager;
|
|
29
|
-
|
|
43
|
+
private readonly home;
|
|
44
|
+
/**
|
|
45
|
+
* @param db A connection on the server the branches are made on. Branch
|
|
46
|
+
* DDL runs here.
|
|
47
|
+
* @param home Where branches are kept. Without one, the database the
|
|
48
|
+
* pool manager's connection string names, and `db`'s registry.
|
|
49
|
+
*/
|
|
50
|
+
constructor(db: DrizzleClient, poolManager: DatabasePoolManager, home?: BranchHome);
|
|
30
51
|
/**
|
|
31
52
|
* Refuse a branch mutation the connected server cannot honour.
|
|
32
53
|
*
|
|
@@ -313,9 +313,10 @@ export declare class FetchService {
|
|
|
313
313
|
* Extract cursor pagination conditions from startAfter options.
|
|
314
314
|
*
|
|
315
315
|
* "Every row that sorts after this one", written out as a comparison over
|
|
316
|
-
* the same keys the `ORDER BY` uses and ending on the same
|
|
317
|
-
* one key that is the familiar
|
|
318
|
-
* several it nests, each key's
|
|
316
|
+
* the same keys the `ORDER BY` uses and ending on the same primary key,
|
|
317
|
+
* descending. With one sort key that is the familiar
|
|
318
|
+
* `k > v OR (k = v AND id < cursorId)`; with several it nests, each key's
|
|
319
|
+
* tie handing the decision to the next.
|
|
319
320
|
*/
|
|
320
321
|
private buildCursorConditions;
|
|
321
322
|
/** The table's primary key columns, in key order. */
|
|
@@ -359,7 +360,7 @@ export declare class FetchService {
|
|
|
359
360
|
*/
|
|
360
361
|
private preciseCursorValue;
|
|
361
362
|
/**
|
|
362
|
-
* "Sorts strictly after the cursor row", over `keys` and then the
|
|
363
|
+
* "Sorts strictly after the cursor row", over `keys` and then the primary key.
|
|
363
364
|
*
|
|
364
365
|
* Built by recursion rather than as a row-value comparison — `(a, b) > (x, y)`
|
|
365
366
|
* would be shorter, but it is only correct when every key runs the same
|
|
@@ -16,6 +16,24 @@ import { type ReadCallContextProvider } from "./read-scope.js";
|
|
|
16
16
|
* the same claim about the same drizzle behaviour in a second spelling.
|
|
17
17
|
*/
|
|
18
18
|
export declare function applyDynamicJoin<T>(query: T, joinTable: PgTable, condition: SQL): T;
|
|
19
|
+
/**
|
|
20
|
+
* The ON condition of one `joinPath` step: every `from` column equal to its
|
|
21
|
+
* `to` column, or `undefined` naming the first column either table lacks.
|
|
22
|
+
*
|
|
23
|
+
* `on.from`/`on.to` are a column or a tuple, and a tuple is how a step joins on
|
|
24
|
+
* a composite key. Comparing the first pair alone joined each row to every row
|
|
25
|
+
* sharing that column — every locale of a translation, not the one addressed.
|
|
26
|
+
*/
|
|
27
|
+
export declare function joinStepCondition(currentTable: PgTable, joinTable: PgTable, step: {
|
|
28
|
+
on: {
|
|
29
|
+
from: string | string[];
|
|
30
|
+
to: string | string[];
|
|
31
|
+
};
|
|
32
|
+
}): {
|
|
33
|
+
condition: SQL;
|
|
34
|
+
} | {
|
|
35
|
+
missing: string;
|
|
36
|
+
};
|
|
19
37
|
/**
|
|
20
38
|
* Service for handling all relation-related operations.
|
|
21
39
|
* Handles fetching, updating, and managing row relations.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type PgNotifyListenerStatus } from "../pg-notify-listener.js";
|
|
1
2
|
/**
|
|
2
3
|
* A single database change captured by the CDC triggers and delivered over the
|
|
3
4
|
* `rebase_cdc` NOTIFY channel.
|
|
@@ -7,12 +8,13 @@ export interface CdcChangeEvent {
|
|
|
7
8
|
table: string;
|
|
8
9
|
op: "INSERT" | "UPDATE" | "DELETE";
|
|
9
10
|
/**
|
|
10
|
-
* The changed
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* The changed row's identity, by column: its key, and for a junction table
|
|
12
|
+
* the two ids naming the child list (from NEW for insert/update, OLD for
|
|
13
|
+
* delete). Never the row's other values — any login can LISTEN; see
|
|
14
|
+
* `buildCdcFunctionSql`.
|
|
13
15
|
*/
|
|
14
16
|
row: Record<string, unknown>;
|
|
15
|
-
/** True when the
|
|
17
|
+
/** True when even the key was too large to notify, so `row` is empty. */
|
|
16
18
|
truncated?: boolean;
|
|
17
19
|
}
|
|
18
20
|
/**
|
|
@@ -49,4 +51,7 @@ export declare class CdcListener {
|
|
|
49
51
|
start(): Promise<void>;
|
|
50
52
|
/** Stop listening and release the connection. */
|
|
51
53
|
stop(): Promise<void>;
|
|
54
|
+
/** Listening on a connection that answered its last heartbeat — see {@link PgNotifyListener}. */
|
|
55
|
+
get connected(): boolean;
|
|
56
|
+
status(): PgNotifyListenerStatus;
|
|
52
57
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CollectionConfig } from "@rebasepro/types";
|
|
2
|
+
import type { PostgresCollectionRegistry } from "../../collections/PostgresCollectionRegistry.js";
|
|
3
|
+
/** One key field of a collection, and the column it is stored in. */
|
|
4
|
+
export interface KeyColumn {
|
|
5
|
+
fieldName: string;
|
|
6
|
+
columnName: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The columns a collection's rows are addressed by.
|
|
10
|
+
*
|
|
11
|
+
* A change notification names its row by column — that is all a trigger sees —
|
|
12
|
+
* and an address is built from the key's *field* names. Both ends read the
|
|
13
|
+
* mapping from here: the provisioner, to attach the trigger with exactly these
|
|
14
|
+
* columns (and nothing else leaves the database), and the consumer, to turn the
|
|
15
|
+
* captured columns back into an address. A key declared as `userId` over
|
|
16
|
+
* `user_id` used to be looked up by field name on the captured row, never
|
|
17
|
+
* found, and every external write reached no single-row subscriber.
|
|
18
|
+
*/
|
|
19
|
+
export declare function collectionKeyColumns(collection: CollectionConfig, registry: PostgresCollectionRegistry): KeyColumn[];
|
|
@@ -27,23 +27,49 @@ export declare const CDC_TRIGGER_FUNCTION = "rebase.rebase_cdc_notify";
|
|
|
27
27
|
export declare const CDC_TRIGGER_NAME = "rebase_cdc_trigger";
|
|
28
28
|
/**
|
|
29
29
|
* SQL that (re)creates the generic CDC trigger function. Safe to run repeatedly:
|
|
30
|
-
* `CREATE OR REPLACE` updates in place without dropping dependent triggers
|
|
30
|
+
* `CREATE OR REPLACE` updates in place without dropping dependent triggers —
|
|
31
|
+
* which is also how a database instrumented by an older version gets this body
|
|
32
|
+
* on its next boot, for every table at once, re-attached or not.
|
|
31
33
|
*
|
|
32
|
-
* The function emits `{ schema, table, op, row }
|
|
33
|
-
* tuple
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
34
|
+
* The function emits `{ schema, table, op, row }`, and `row` is an **identity,
|
|
35
|
+
* never the tuple**. Postgres puts no privilege on `LISTEN`: any role that can
|
|
36
|
+
* connect can listen on this channel, whatever it may `SELECT`. When `row` was
|
|
37
|
+
* `to_jsonb(NEW)`, a login with no grant on anything received every changed row
|
|
38
|
+
* of every instrumented table — `password_hash` and verification tokens from
|
|
39
|
+
* the auth table included — past RLS and column grants. The consumer never
|
|
40
|
+
* read anything but the key: every subscriber re-reads the row under its own
|
|
41
|
+
* scope.
|
|
42
|
+
*
|
|
43
|
+
* The identity is, in order:
|
|
44
|
+
* - the columns the trigger was attached with (`TG_ARGV`) — the key the
|
|
45
|
+
* consumer addresses the row by, and for a junction table the two ids that
|
|
46
|
+
* name the child list it changed; see {@link buildCdcTriggerSql};
|
|
47
|
+
* - otherwise the table's primary key, read from the catalogue — a trigger
|
|
48
|
+
* attached with no columns, by an older boot or on a table no collection
|
|
49
|
+
* maps any more;
|
|
50
|
+
* - otherwise `id`, if the table has one, and else nothing: a change the
|
|
51
|
+
* consumer can only treat as collection-wide.
|
|
37
52
|
*/
|
|
38
53
|
export declare function buildCdcFunctionSql(): string;
|
|
39
54
|
/**
|
|
40
55
|
* SQL that (re)attaches the CDC trigger to a single table. `DROP ... IF EXISTS`
|
|
41
56
|
* before `CREATE` keeps it idempotent and picks up any function signature change.
|
|
57
|
+
*
|
|
58
|
+
* `identityColumns` are what the payload names the changed row by — see
|
|
59
|
+
* {@link buildCdcFunctionSql}. Passed as trigger arguments, so one shared
|
|
60
|
+
* function serves every table without a catalogue read per row. Empty means
|
|
61
|
+
* "the primary key".
|
|
42
62
|
*/
|
|
43
|
-
export declare function buildCdcTriggerSql(schema: string, table: string): string;
|
|
63
|
+
export declare function buildCdcTriggerSql(schema: string, table: string, identityColumns?: readonly string[]): string;
|
|
44
64
|
export interface CdcTableRef {
|
|
45
65
|
schema: string;
|
|
46
66
|
table: string;
|
|
67
|
+
/**
|
|
68
|
+
* The columns a change to this table is announced by — the collection's
|
|
69
|
+
* key columns, or a junction's two id columns. Absent means the primary
|
|
70
|
+
* key. Nothing else ever leaves the trigger; see {@link buildCdcFunctionSql}.
|
|
71
|
+
*/
|
|
72
|
+
identityColumns?: string[];
|
|
47
73
|
}
|
|
48
74
|
export interface ProvisionResult {
|
|
49
75
|
/** Tables the trigger was successfully attached to. */
|
|
@@ -53,6 +79,18 @@ export interface ProvisionResult {
|
|
|
53
79
|
reason: string;
|
|
54
80
|
}>;
|
|
55
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Replace an already-installed trigger function with the current body, and
|
|
84
|
+
* install nothing where there is none.
|
|
85
|
+
*
|
|
86
|
+
* For a process that owns the schema but is not provisioning capture on this
|
|
87
|
+
* boot — `REALTIME_CDC=off`, or no direct URL to listen on. The triggers an
|
|
88
|
+
* earlier boot attached keep firing whether anything consumes them or not, so
|
|
89
|
+
* a database instrumented by a version whose function put whole rows on the
|
|
90
|
+
* channel kept doing so, for every login that cares to `LISTEN`, until capture
|
|
91
|
+
* was switched back on. Returns whether a function was there to replace.
|
|
92
|
+
*/
|
|
93
|
+
export declare function refreshInstalledCdcFunction(run: RawSqlRunner): Promise<boolean>;
|
|
56
94
|
/**
|
|
57
95
|
* Idempotently install the CDC trigger function and per-table triggers.
|
|
58
96
|
*
|
|
@@ -6,13 +6,15 @@
|
|
|
6
6
|
* properties of `NOTIFY` shape everything below:
|
|
7
7
|
*
|
|
8
8
|
* - **8000 bytes per payload.** Presence and cursors fit with room to spare; a
|
|
9
|
-
* scene snapshot does not.
|
|
10
|
-
*
|
|
11
|
-
* `rebase.channel_messages` with a sequence
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
9
|
+
* scene snapshot does not. A message on a *retained* channel never travels
|
|
10
|
+
* here at all, whatever its size: the statement that numbers it notifies a
|
|
11
|
+
* pointer — the body is already in `rebase.channel_messages` with a sequence
|
|
12
|
+
* number, so the receiver reads it back (`ChannelHistoryStore.append`). That
|
|
13
|
+
* is the same trick the entity path uses (notify an address, refetch the
|
|
14
|
+
* row), applied to a different table, and for the same reason: any database
|
|
15
|
+
* login can `LISTEN`. On an ephemeral channel there is nothing to point at,
|
|
16
|
+
* so the frame carries the message, and an oversized one is refused loudly
|
|
17
|
+
* instead of reaching some instances and not others.
|
|
16
18
|
*
|
|
17
19
|
* - **A notify is a query on the primary database.** Not a slow one, but it
|
|
18
20
|
* competes with the application's real queries, and that — not throughput —
|
|
@@ -26,6 +28,7 @@
|
|
|
26
28
|
* than correctness. That is what makes coalescing safe.
|
|
27
29
|
*/
|
|
28
30
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
31
|
+
import { type PgNotifyListenerStatus } from "../pg-notify-listener.js";
|
|
29
32
|
import { ChannelBus, ChannelBusFrame, ChannelBusHandler } from "./ChannelBus.js";
|
|
30
33
|
/** NOTIFY channel carrying channel-bus frames. */
|
|
31
34
|
export declare const CHANNEL_BUS_NOTIFY_CHANNEL = "rebase_channel_bus";
|
|
@@ -79,6 +82,8 @@ export declare class PostgresChannelBus implements ChannelBus {
|
|
|
79
82
|
* still holds.
|
|
80
83
|
*/
|
|
81
84
|
publish(frame: ChannelBusFrame): Promise<void>;
|
|
85
|
+
/** The LISTEN connection's state, for `/health` — see {@link PgNotifyListener}. */
|
|
86
|
+
status(): PgNotifyListenerStatus | undefined;
|
|
82
87
|
stop(): Promise<void>;
|
|
83
88
|
private openWindow;
|
|
84
89
|
/** Send everything queued and settle the promises waiting on it. */
|
|
@@ -53,6 +53,22 @@ export interface ResolvedRetention {
|
|
|
53
53
|
limit?: number;
|
|
54
54
|
ttlMs?: number;
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* A bus frame to send in the statement that numbers a retained message — see
|
|
58
|
+
* {@link ChannelHistoryStore.append}.
|
|
59
|
+
*
|
|
60
|
+
* It is always a pointer, `{ kind: "broadcast_ref", sid, channel, seq }`, and
|
|
61
|
+
* never the message. Postgres puts no privilege on `LISTEN`, so a NOTIFY payload
|
|
62
|
+
* reaches every role that can connect to the database; the body stays in
|
|
63
|
+
* `rebase.channel_messages`, behind the table grants, and each receiving
|
|
64
|
+
* instance reads it back by `(channel, seq)` — the sender's id with it.
|
|
65
|
+
*/
|
|
66
|
+
export interface RetainedAnnouncement {
|
|
67
|
+
/** The NOTIFY channel the bus listens on. */
|
|
68
|
+
notifyChannel: string;
|
|
69
|
+
/** The sending instance. */
|
|
70
|
+
sid: string;
|
|
71
|
+
}
|
|
56
72
|
/**
|
|
57
73
|
* Persistence and replay for retained channels.
|
|
58
74
|
*
|
|
@@ -91,7 +107,7 @@ export declare class ChannelHistoryStore {
|
|
|
91
107
|
* broadcasts to one channel line up in a single order — and what keeps
|
|
92
108
|
* different channels from contending with each other at all.
|
|
93
109
|
*/
|
|
94
|
-
append(channel: string, event: string, payload: unknown, senderId?: string): Promise<{
|
|
110
|
+
append(channel: string, event: string, payload: unknown, senderId?: string, announce?: RetainedAnnouncement): Promise<{
|
|
95
111
|
seq: number;
|
|
96
112
|
at: string;
|
|
97
113
|
}>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PgTable, AnyPgColumn } from "drizzle-orm/pg-core";
|
|
2
|
-
import { SQL } from "drizzle-orm";
|
|
2
|
+
import { type SQL } from "drizzle-orm";
|
|
3
3
|
import { CollectionConfig, ResolvedHasMany, ResolvedHasOne } from "@rebasepro/types";
|
|
4
4
|
import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry.js";
|
|
5
5
|
import { ApiError } from "@rebasepro/server";
|
|
@@ -133,6 +133,16 @@ export declare function getPrimaryKeys(collection: CollectionConfig, registry: P
|
|
|
133
133
|
* wrong and which collection it is wrong about.
|
|
134
134
|
*/
|
|
135
135
|
export declare function requirePrimaryKeys(collection: CollectionConfig, registry: PostgresCollectionRegistry): PrimaryKeyInfo[];
|
|
136
|
+
/**
|
|
137
|
+
* The table's key columns, in key order.
|
|
138
|
+
*
|
|
139
|
+
* Throws when there are none, or when the table has no column for one of them:
|
|
140
|
+
* a WHERE built from fewer columns than the key matches every row that shares
|
|
141
|
+
* the columns it did find, so there is no partial answer worth returning.
|
|
142
|
+
*
|
|
143
|
+
* @param collectionPath named in the error.
|
|
144
|
+
*/
|
|
145
|
+
export declare function primaryKeyColumns(table: PgTable, primaryKeys: PrimaryKeyInfo[], collectionPath: string): AnyPgColumn[];
|
|
136
146
|
/**
|
|
137
147
|
* The column on the *source* table that a `hasOne`/`hasMany` link points at.
|
|
138
148
|
*
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Every cross-instance feature in the backend needs the same thing: one
|
|
5
5
|
* connection *outside* the Drizzle pool that stays open, holds a `LISTEN`, and
|
|
6
|
-
* comes back on its own after the database or the network drops it. CDC
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* comes back on its own after the database or the network drops it. CDC, the
|
|
7
|
+
* channel bus and the cross-instance broadcast all use this one. By default the
|
|
8
|
+
* **first** connect is validated and rethrown, so a caller can fall back to a
|
|
9
|
+
* different strategy, while every later drop is repaired quietly in the
|
|
10
|
+
* background; `retryInitialConnect` repairs the first one the same way.
|
|
11
11
|
*
|
|
12
12
|
* `LISTEN` is session state, so this connection must not go through a
|
|
13
13
|
* transaction-mode pooler (PgBouncer): give it the direct database URL.
|
|
@@ -30,25 +30,107 @@ export interface PgNotifyListenerOptions {
|
|
|
30
30
|
* gone for good; a caller that mirrors state from the channel resyncs here.
|
|
31
31
|
*/
|
|
32
32
|
onReconnect?: () => void;
|
|
33
|
+
/**
|
|
34
|
+
* How often the connection proves it is alive with a round trip. `0`
|
|
35
|
+
* turns the heartbeat off. See {@link PgNotifyListener} for why it exists.
|
|
36
|
+
*/
|
|
37
|
+
heartbeatIntervalMs?: number;
|
|
38
|
+
/** How long a heartbeat may take before the connection is declared dead. */
|
|
39
|
+
heartbeatTimeoutMs?: number;
|
|
40
|
+
/**
|
|
41
|
+
* Treat a failed first connect like any later drop: `start()` resolves,
|
|
42
|
+
* the listener reports itself down, and it keeps dialling in the
|
|
43
|
+
* background. For a caller with nothing to fall back to, for whom a
|
|
44
|
+
* database that is unreachable at boot is an outage to wait out rather
|
|
45
|
+
* than a reason to run without the channel. Off by default: `start()`
|
|
46
|
+
* rejects, so a caller with a fallback can take it.
|
|
47
|
+
*/
|
|
48
|
+
retryInitialConnect?: boolean;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* What a listener can say about itself — read by `/health`.
|
|
52
|
+
*
|
|
53
|
+
* `connected` is "listening, and the last heartbeat answered", not "a socket
|
|
54
|
+
* object exists": a half-open connection has a perfectly good socket object.
|
|
55
|
+
*/
|
|
56
|
+
export interface PgNotifyListenerStatus {
|
|
57
|
+
channel: string;
|
|
58
|
+
connected: boolean;
|
|
59
|
+
/** Since when (epoch ms) it has been down, while it is down. */
|
|
60
|
+
downSince?: number;
|
|
61
|
+
/** When (epoch ms) it last answered a heartbeat. */
|
|
62
|
+
lastHeartbeatAt?: number;
|
|
33
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Below every common idle-flow timeout: Azure's load balancer drops an idle
|
|
66
|
+
* flow at 4 minutes, AWS's NLB at 350 s, and NAT and conntrack tables and
|
|
67
|
+
* pooler-side reapers are often shorter. A LISTEN connection is idle by
|
|
68
|
+
* nature, so it is exactly the connection those timeouts kill.
|
|
69
|
+
*/
|
|
70
|
+
export declare const DEFAULT_HEARTBEAT_INTERVAL_MS = 30000;
|
|
71
|
+
export declare const DEFAULT_HEARTBEAT_TIMEOUT_MS = 10000;
|
|
72
|
+
/**
|
|
73
|
+
* A half-open connection — one whose packets stop arriving, with no FIN and no
|
|
74
|
+
* RST, which is what an idle-flow timeout on a load balancer, a NAT, a dead
|
|
75
|
+
* middlebox or a failed-over network looks like — fires no `error` and no
|
|
76
|
+
* `end`. The connection object stays, `LISTEN` stays registered on a backend
|
|
77
|
+
* that may no longer exist, and every notification from then on is lost: CDC,
|
|
78
|
+
* the cross-instance broadcast and the channel bus all went quiet with no
|
|
79
|
+
* error, no reconnect and a green `/health`, while writes made through this
|
|
80
|
+
* instance still looked live because they are delivered locally.
|
|
81
|
+
*
|
|
82
|
+
* So the connection proves itself: TCP keepalive is on, and every
|
|
83
|
+
* {@link DEFAULT_HEARTBEAT_INTERVAL_MS} a `SELECT 1` must come back within
|
|
84
|
+
* {@link DEFAULT_HEARTBEAT_TIMEOUT_MS}. One that does not is torn down and
|
|
85
|
+
* replaced, and `onReconnect` resyncs whatever was missed.
|
|
86
|
+
*/
|
|
34
87
|
export declare class PgNotifyListener {
|
|
35
88
|
private readonly options;
|
|
36
89
|
private client?;
|
|
37
90
|
private running;
|
|
38
91
|
private reconnectTimer?;
|
|
92
|
+
private heartbeatTimer?;
|
|
93
|
+
private heartbeatInFlight;
|
|
94
|
+
private downSince?;
|
|
95
|
+
private lastHeartbeatAt?;
|
|
39
96
|
constructor(options: PgNotifyListenerOptions);
|
|
40
97
|
/** Whether the listener is meant to be connected right now. */
|
|
41
98
|
get active(): boolean;
|
|
99
|
+
/** Whether it is listening on a connection that answered its last heartbeat. */
|
|
100
|
+
get connected(): boolean;
|
|
101
|
+
status(): PgNotifyListenerStatus;
|
|
42
102
|
/**
|
|
43
103
|
* Connect and begin listening. Idempotent.
|
|
44
104
|
*
|
|
45
105
|
* Rejects if the *initial* connection or `LISTEN` fails, leaving the
|
|
46
106
|
* listener stopped — callers use that to degrade deliberately instead of
|
|
47
|
-
* running blind against a channel nothing is delivering.
|
|
107
|
+
* running blind against a channel nothing is delivering. With
|
|
108
|
+
* `retryInitialConnect` it resolves instead, reports itself down, and
|
|
109
|
+
* keeps dialling.
|
|
48
110
|
*/
|
|
49
111
|
start(): Promise<void>;
|
|
50
112
|
/** Stop listening and release the connection. Idempotent. */
|
|
51
113
|
stop(): Promise<void>;
|
|
114
|
+
/**
|
|
115
|
+
* @param first The connect `start()` makes: never a reconnect, so it owes
|
|
116
|
+
* no resync, and — unless `retryInitialConnect` — a failure is
|
|
117
|
+
* thrown to the caller rather than retried.
|
|
118
|
+
*/
|
|
52
119
|
private connect;
|
|
120
|
+
/**
|
|
121
|
+
* Give up on the current connection: it is no longer the listener's, so
|
|
122
|
+
* its late events are ignored and `connected` says so at once; it is
|
|
123
|
+
* ended without waiting on it; and a replacement is scheduled.
|
|
124
|
+
*/
|
|
125
|
+
private abandon;
|
|
126
|
+
private startHeartbeat;
|
|
127
|
+
private stopHeartbeat;
|
|
128
|
+
/**
|
|
129
|
+
* One round trip on the LISTEN connection. Not answered in time, it is
|
|
130
|
+
* treated as dead: the client is ended — which, with the query still
|
|
131
|
+
* outstanding, destroys the socket rather than waiting on a goodbye that
|
|
132
|
+
* cannot arrive — and replaced.
|
|
133
|
+
*/
|
|
134
|
+
private heartbeat;
|
|
53
135
|
private scheduleReconnect;
|
|
54
136
|
}
|