@crvouga/mockingbird-service-postgres 1.0.0 → 1.2.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/AGENTS.md +8 -2
- package/CHANGELOG.md +20 -0
- package/COMPATIBILITY.md +1 -1
- package/README.md +65 -13
- package/dist/wire/cli.d.ts +2 -0
- package/dist/wire/cli.js +23770 -0
- package/dist/wire/cli.js.map +7 -0
- package/dist/wire/cluster.d.ts +71 -0
- package/dist/wire/connection.d.ts +107 -0
- package/dist/wire/index.d.ts +69 -0
- package/dist/wire/index.js +23715 -0
- package/dist/wire/index.js.map +7 -0
- package/dist/wire/protocol.d.ts +119 -0
- package/dist/wire/scram.d.ts +17 -0
- package/dist/wire/split.d.ts +6 -0
- package/package.json +18 -5
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { Database } from "../api/database.js";
|
|
2
|
+
/**
|
|
3
|
+
* What every connection of one server shares: the engine, and the coordination the engine
|
|
4
|
+
* (single-session, synchronous) does not do itself.
|
|
5
|
+
*
|
|
6
|
+
* - The **turn**: the engine holds one transaction at a time, so a connection inside an
|
|
7
|
+
* explicit transaction block holds the turn until it commits or rolls back, and every other
|
|
8
|
+
* connection's statement waits. A statement outside a block takes the turn for itself only.
|
|
9
|
+
* Uncommitted rows are therefore never visible to another connection (read committed holds),
|
|
10
|
+
* at the cost of running transaction blocks one at a time.
|
|
11
|
+
* - **Advisory locks** per session, with waiters woken in order, and deadlock detection
|
|
12
|
+
* between a lock and the turn (`40P01`).
|
|
13
|
+
* - **LISTEN / NOTIFY** fan-out, delivered when a listening connection is idle.
|
|
14
|
+
* - Which session's statement is executing, so the SQL functions registered on the engine
|
|
15
|
+
* (`pg_backend_pid`, `pg_advisory_lock`, `pg_notify`, …) know whom they serve.
|
|
16
|
+
*/
|
|
17
|
+
export declare class Cluster {
|
|
18
|
+
readonly db: Database;
|
|
19
|
+
turnHolder: Session | null;
|
|
20
|
+
private readonly turnQueue;
|
|
21
|
+
readonly locks: Map<string, {
|
|
22
|
+
holder: Session;
|
|
23
|
+
count: number;
|
|
24
|
+
xact: boolean;
|
|
25
|
+
}>;
|
|
26
|
+
private readonly lockQueues;
|
|
27
|
+
readonly listeners: Map<string, Set<Session>>;
|
|
28
|
+
readonly sessions: Map<number, Session>;
|
|
29
|
+
current: Session | null;
|
|
30
|
+
private nextPid;
|
|
31
|
+
constructor(db: Database);
|
|
32
|
+
newPid(): number;
|
|
33
|
+
/** Wait until this session may execute: at once when nobody holds the turn, or it does. */
|
|
34
|
+
acquireTurn(session: Session): Promise<void>;
|
|
35
|
+
releaseTurn(session: Session): void;
|
|
36
|
+
/** Take `key` for `session` now, or report that another session holds it. */
|
|
37
|
+
tryLock(session: Session, key: string, xact: boolean): boolean;
|
|
38
|
+
/** Wait until `key` is free for `session`; rejects with `40P01` when that can never happen. */
|
|
39
|
+
waitLock(session: Session, key: string): Promise<void>;
|
|
40
|
+
unlock(session: Session, key: string): boolean;
|
|
41
|
+
/** Release every lock `session` holds, or only its transaction-scoped ones. */
|
|
42
|
+
unlockAll(session: Session, xactOnly?: boolean): void;
|
|
43
|
+
/** Forget a session: its transaction, turn, locks, waits and subscriptions. */
|
|
44
|
+
drop(session: Session): void;
|
|
45
|
+
/** Run `fn` as `session`'s statement, so registered functions can tell who is calling. */
|
|
46
|
+
as<T>(session: Session, fn: () => T): T;
|
|
47
|
+
notify(from: Session, channel: string, payload: string): void;
|
|
48
|
+
}
|
|
49
|
+
type Waiter = {
|
|
50
|
+
session: Session;
|
|
51
|
+
resolve: () => void;
|
|
52
|
+
reject: (error: Error) => void;
|
|
53
|
+
};
|
|
54
|
+
/** A statement stopped by a lock another session holds; the session waits, then runs it again. */
|
|
55
|
+
export declare class LockWait extends Error {
|
|
56
|
+
readonly key: string;
|
|
57
|
+
constructor(key: string);
|
|
58
|
+
}
|
|
59
|
+
/** What the cluster needs from a connection. */
|
|
60
|
+
export interface Session {
|
|
61
|
+
readonly pid: number;
|
|
62
|
+
waitingForTurn: Waiter | null;
|
|
63
|
+
waitingForLock: string | null;
|
|
64
|
+
/** NOTIFYs issued inside the current transaction block, sent at commit. */
|
|
65
|
+
pendingNotifies: {
|
|
66
|
+
channel: string;
|
|
67
|
+
payload: string;
|
|
68
|
+
}[];
|
|
69
|
+
queueNotification(pid: number, channel: string, payload: string): void;
|
|
70
|
+
}
|
|
71
|
+
export {};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { Socket } from "node:net";
|
|
2
|
+
import { type Cluster, type Session } from "./cluster.js";
|
|
3
|
+
export type ServerFaults = {
|
|
4
|
+
/** Destroy the socket of the next connection that runs a statement inside a transaction block. */
|
|
5
|
+
dropConnection?: boolean;
|
|
6
|
+
/** Hold the next statement this long before it runs. */
|
|
7
|
+
delayStatementMs?: number;
|
|
8
|
+
/** Fail the next COMMIT with this SQLSTATE (`40001` serialization failure) after rolling back. */
|
|
9
|
+
failCommit?: string;
|
|
10
|
+
};
|
|
11
|
+
export type ServerLog = {
|
|
12
|
+
pid: number;
|
|
13
|
+
sql: string;
|
|
14
|
+
durationMs: number;
|
|
15
|
+
/** `ok`, or the SQLSTATE of the error. */
|
|
16
|
+
status: string;
|
|
17
|
+
};
|
|
18
|
+
export type ConnectionOptions = {
|
|
19
|
+
password?: string;
|
|
20
|
+
serverVersion: string;
|
|
21
|
+
parameters: Record<string, string>;
|
|
22
|
+
faults: ServerFaults;
|
|
23
|
+
onLog?: (entry: ServerLog) => void;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* One client connection: the protocol state machine over the shared {@link Cluster}.
|
|
27
|
+
* Statements run one at a time per connection; between connections the cluster decides.
|
|
28
|
+
*/
|
|
29
|
+
export declare class Connection implements Session {
|
|
30
|
+
private readonly socket;
|
|
31
|
+
private readonly cluster;
|
|
32
|
+
private readonly options;
|
|
33
|
+
readonly pid: number;
|
|
34
|
+
readonly secret: number;
|
|
35
|
+
waitingForTurn: Session["waitingForTurn"];
|
|
36
|
+
waitingForLock: string | null;
|
|
37
|
+
pendingNotifies: {
|
|
38
|
+
channel: string;
|
|
39
|
+
payload: string;
|
|
40
|
+
}[];
|
|
41
|
+
private readonly frames;
|
|
42
|
+
private readonly inbox;
|
|
43
|
+
private pumping;
|
|
44
|
+
private started;
|
|
45
|
+
private scram;
|
|
46
|
+
private user;
|
|
47
|
+
/** After an error inside a transaction block: `25P02` until ROLLBACK (`E` in ReadyForQuery). */
|
|
48
|
+
private aborted;
|
|
49
|
+
/** A query is in flight (a CancelRequest applies to it). */
|
|
50
|
+
private busy;
|
|
51
|
+
private cancelled;
|
|
52
|
+
/** Extended protocol: an error was sent, so messages are ignored until Sync. */
|
|
53
|
+
private skipUntilSync;
|
|
54
|
+
private readonly prepared;
|
|
55
|
+
private readonly portals;
|
|
56
|
+
private readonly notifications;
|
|
57
|
+
private closed;
|
|
58
|
+
constructor(socket: Socket, cluster: Cluster, options: ConnectionOptions);
|
|
59
|
+
private receive;
|
|
60
|
+
private pump;
|
|
61
|
+
private write;
|
|
62
|
+
private dispose;
|
|
63
|
+
/** Terminate from the server side (close, or the drop-connection fault). */
|
|
64
|
+
destroy(): void;
|
|
65
|
+
queueNotification(pid: number, channel: string, payload: string): void;
|
|
66
|
+
private flushNotifications;
|
|
67
|
+
/** A CancelRequest with this connection's key: the query in flight fails with `57014`. */
|
|
68
|
+
cancel(): void;
|
|
69
|
+
private lockWaiter;
|
|
70
|
+
private handle;
|
|
71
|
+
private fatal;
|
|
72
|
+
private startup;
|
|
73
|
+
private authenticate;
|
|
74
|
+
private scramStarted;
|
|
75
|
+
private startupParameters;
|
|
76
|
+
private ready;
|
|
77
|
+
private get inTransaction();
|
|
78
|
+
private status;
|
|
79
|
+
private readyForQuery;
|
|
80
|
+
/** Run one statement against the engine, honoring the turn, locks, aborted state and faults. */
|
|
81
|
+
private execute;
|
|
82
|
+
private executeInner;
|
|
83
|
+
private waitForLock;
|
|
84
|
+
/** The engine call itself: statement-atomic, and transaction control handled as the server sees it. */
|
|
85
|
+
private runStatement;
|
|
86
|
+
/** Release the turn and transaction-scoped locks once no block is open; deliver NOTIFYs at commit. */
|
|
87
|
+
private afterStatement;
|
|
88
|
+
private flushNotifies;
|
|
89
|
+
private listen;
|
|
90
|
+
private unlisten;
|
|
91
|
+
private query;
|
|
92
|
+
private sendOutcome;
|
|
93
|
+
private columnFormats;
|
|
94
|
+
private fields;
|
|
95
|
+
private parse;
|
|
96
|
+
private bind;
|
|
97
|
+
private describe;
|
|
98
|
+
/**
|
|
99
|
+
* The row shape of a statement that returns rows, for Describe on a statement: a trial run with
|
|
100
|
+
* null parameters inside a transaction that is rolled back. Anything else (or a trial that
|
|
101
|
+
* fails) is described as returning no data.
|
|
102
|
+
*/
|
|
103
|
+
private shapeOf;
|
|
104
|
+
private executePortal;
|
|
105
|
+
private close;
|
|
106
|
+
private sync;
|
|
107
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A PostgreSQL wire-protocol (frontend/backend 3.0) TCP server in front of the in-memory
|
|
3
|
+
* engine, so a separate process can use it through a normal `postgres://` connection string:
|
|
4
|
+
* `pg`, `postgres.js`, a JDBC client, `psql`. It runs in Node and Bun (it needs `node:net`),
|
|
5
|
+
* not the browser.
|
|
6
|
+
*
|
|
7
|
+
* One {@link Database} is shared by every connection through a {@link Cluster}: the engine
|
|
8
|
+
* runs one statement at a time, so a connection inside an explicit `BEGIN` block holds the
|
|
9
|
+
* engine until it commits or rolls back and the others queue, which keeps read-committed
|
|
10
|
+
* visibility (uncommitted rows never reach another connection) at the cost of serializing
|
|
11
|
+
* transaction blocks. Advisory locks, `LISTEN`/`NOTIFY`, `CancelRequest` and per-connection
|
|
12
|
+
* aborted-transaction state (`25P02`) are coordinated across connections.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { serve } from "@crvouga/mockingbird-service-postgres/wire";
|
|
17
|
+
*
|
|
18
|
+
* const server = await serve({ port: 0 });
|
|
19
|
+
* // new pg.Pool({ connectionString: `postgres://postgres@127.0.0.1:${server.port}/db` })
|
|
20
|
+
* await server.close();
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @module
|
|
24
|
+
*/
|
|
25
|
+
import { type Server } from "node:net";
|
|
26
|
+
import { Database } from "../api/database.js";
|
|
27
|
+
import type { Snapshot } from "../api/snapshot.js";
|
|
28
|
+
import { type ServerFaults, type ServerLog } from "./connection.js";
|
|
29
|
+
export type ServeOptions = {
|
|
30
|
+
/** TCP port; `0` (the default) picks a free one, reported as `server.port`. */
|
|
31
|
+
port?: number;
|
|
32
|
+
/** Interface to bind; default `127.0.0.1`. */
|
|
33
|
+
host?: string;
|
|
34
|
+
/**
|
|
35
|
+
* The database to serve. Pass a {@link Database} to share an existing one, a {@link Snapshot}
|
|
36
|
+
* to boot every server from one frozen template, or omit it for a fresh deterministic engine.
|
|
37
|
+
*/
|
|
38
|
+
database?: Database | Snapshot;
|
|
39
|
+
/** Require SCRAM-SHA-256 with this password; omitted means trust (AuthenticationOk). */
|
|
40
|
+
password?: string;
|
|
41
|
+
/** `server_version` reported at startup and by `SHOW server_version`. Default `18.3`. */
|
|
42
|
+
serverVersion?: string;
|
|
43
|
+
/** Extra `ParameterStatus` values sent at startup (override the defaults). */
|
|
44
|
+
parameters?: Record<string, string>;
|
|
45
|
+
/** Per-statement log sink, for tests and debugging. */
|
|
46
|
+
onLog?: (entry: ServerLog) => void;
|
|
47
|
+
};
|
|
48
|
+
export type PostgresServer = {
|
|
49
|
+
/** The bound port. */
|
|
50
|
+
readonly port: number;
|
|
51
|
+
/** The bound host. */
|
|
52
|
+
readonly host: string;
|
|
53
|
+
/** The shared engine, for seeding, snapshotting or asserting from the test process. */
|
|
54
|
+
readonly database: Database;
|
|
55
|
+
/** The underlying `net.Server`. */
|
|
56
|
+
readonly server: Server;
|
|
57
|
+
/** Open connections right now. */
|
|
58
|
+
readonly connections: number;
|
|
59
|
+
/** Arm a fault preset for the next statement / connection (see {@link ServerFaults}). */
|
|
60
|
+
fault(fault: ServerFaults): void;
|
|
61
|
+
/** Freeze the live state (the admin `snapshot()` control). */
|
|
62
|
+
snapshot(): Snapshot;
|
|
63
|
+
/** Stop listening and close every connection. */
|
|
64
|
+
close(): Promise<void>;
|
|
65
|
+
};
|
|
66
|
+
/** Start a server and resolve once it is listening. */
|
|
67
|
+
export declare const serve: (options?: ServeOptions) => Promise<PostgresServer>;
|
|
68
|
+
export { Cluster } from "./cluster.js";
|
|
69
|
+
export type { ServerFaults, ServerLog } from "./connection.js";
|