@crvouga/mockingbird-service-postgres 3.2.0 → 3.4.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/COMPATIBILITY.md +35 -5
  3. package/README.md +219 -57
  4. package/compat/coverage.json +8 -7
  5. package/compat/divergences.json +37 -2
  6. package/compat/requirements.json +1 -1
  7. package/compat/sections/cat.ts +49 -0
  8. package/compat/sections/ddl.ts +11 -0
  9. package/compat/unsupported-register.json +0 -3
  10. package/dist/admin.js +4812 -987
  11. package/dist/admin.js.map +4 -4
  12. package/dist/api/database.d.ts +11 -0
  13. package/dist/ast/nodes.d.ts +43 -1
  14. package/dist/constraints/enforce.d.ts +2 -0
  15. package/dist/executor/alter-column.d.ts +27 -0
  16. package/dist/executor/comment.d.ts +4 -0
  17. package/dist/executor/ddl.d.ts +1 -2
  18. package/dist/executor/dml.d.ts +2 -0
  19. package/dist/executor/extensions.d.ts +11 -0
  20. package/dist/executor/maintenance.d.ts +25 -0
  21. package/dist/expressions/context.d.ts +1 -1
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.js +3249 -549
  24. package/dist/index.js.map +4 -4
  25. package/dist/parser/parser.d.ts +9 -0
  26. package/dist/schema/catalog-extensions.d.ts +5 -0
  27. package/dist/schema/extensions.d.ts +33 -0
  28. package/dist/schema/index-catalog.d.ts +40 -0
  29. package/dist/schema/persistence.d.ts +32 -0
  30. package/dist/schema/storage.d.ts +22 -0
  31. package/dist/server.js +5390 -744
  32. package/dist/server.js.map +4 -4
  33. package/dist/sql/deparse-default.d.ts +12 -0
  34. package/dist/sql/deparse-index.d.ts +12 -2
  35. package/dist/sql/keywords.d.ts +6 -0
  36. package/dist/sql/ruleutils.d.ts +27 -0
  37. package/dist/storage/database-state.d.ts +51 -0
  38. package/dist/types/regnamespace.d.ts +23 -0
  39. package/dist/types/value.d.ts +2 -0
  40. package/dist/unstable.js +11520 -8875
  41. package/dist/unstable.js.map +4 -4
  42. package/dist/wire/cli.js +6445 -1260
  43. package/dist/wire/cli.js.map +4 -4
  44. package/dist/wire/cluster.d.ts +8 -0
  45. package/dist/wire/connection.d.ts +38 -0
  46. package/dist/wire/durable.d.ts +82 -0
  47. package/dist/wire/file-storage.d.ts +3 -0
  48. package/dist/wire/fleet.d.ts +210 -1
  49. package/dist/wire/index.d.ts +21 -1
  50. package/dist/wire/index.js +5383 -741
  51. package/dist/wire/index.js.map +4 -4
  52. package/dist/wire/startup.d.ts +36 -0
  53. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  import { Database } from "../api/database.js";
2
2
  import type { Snapshot } from "../api/snapshot.js";
3
+ import type { Durability } from "./durable.js";
3
4
  /**
4
5
  * What every connection of one server shares: the engine, and the coordination the engine
5
6
  * (single-session, synchronous) does not do itself.
@@ -28,6 +29,8 @@ export declare class Cluster {
28
29
  current: Session | null;
29
30
  private nextPid;
30
31
  private readonly databases;
32
+ /** Set in durable mode: commits are persisted through it before they are acknowledged. */
33
+ durability: Durability | null;
31
34
  constructor(db: Database, defaultDatabaseName?: string);
32
35
  /** The initial database, retained for the server's backwards-compatible public handle. */
33
36
  get db(): Database;
@@ -35,6 +38,11 @@ export declare class Cluster {
35
38
  snapshotAll(): Map<string, Snapshot>;
36
39
  restoreAll(points: ReadonlyMap<string, Snapshot>): void;
37
40
  getDatabase(name: string): Database | undefined;
41
+ /** Add a database that already exists (restored from durable storage) to the catalog. */
42
+ attach(name: string, db: Database): void;
43
+ /** The catalog as it stands, to put back with {@link restoreCatalog} if a change cannot be persisted. */
44
+ catalog(): Map<string, Database>;
45
+ restoreCatalog(entries: ReadonlyMap<string, Database>): void;
38
46
  requireDatabase(name: string): Database;
39
47
  createDatabase(name: string, template?: string): void;
40
48
  dropDatabase(name: string, requester: Session): void;
@@ -8,6 +8,11 @@ export type ServerFaults = {
8
8
  delayStatementMs?: number;
9
9
  /** Fail the next COMMIT with this SQLSTATE (`40001` serialization failure) after rolling back. */
10
10
  failCommit?: string;
11
+ /**
12
+ * Interrupt the next `CREATE INDEX CONCURRENTLY` / `REINDEX ... CONCURRENTLY` with this SQLSTATE
13
+ * (`57014` canceled, `40P01` deadlock). The index it was building stays in the catalog, invalid.
14
+ */
15
+ failConcurrentIndexBuild?: string;
11
16
  };
12
17
  export type ServerLog = {
13
18
  pid: number;
@@ -67,11 +72,24 @@ export declare class Connection implements Session {
67
72
  /** Successful writes in this session's READ COMMITTED transaction workspace. */
68
73
  private transaction;
69
74
  private closed;
75
+ /** This session's settings. They stand in for the database's own around each of its engine calls. */
76
+ private settings;
77
+ /** What `RESET` returns to: the database's settings at connect, then the startup packet's. */
78
+ private settingDefaults;
79
+ /** Durable commits that are persisted and not yet acknowledged on the socket. */
80
+ private readonly unacknowledged;
81
+ /** A commit of this session is being persisted; it still owns the statement turn. */
82
+ private persisting;
70
83
  constructor(socket: Socket, cluster: Cluster, options: ConnectionOptions);
71
84
  private receive;
72
85
  private pump;
73
86
  private write;
74
87
  private dispose;
88
+ /**
89
+ * Run engine work with this session's settings in place of the database's own, so one
90
+ * connection's `SET` never reaches another. Whatever the work leaves behind is the session's.
91
+ */
92
+ private inSession;
75
93
  /** Terminate from the server side (close, or the drop-connection fault). */
76
94
  destroy(): void;
77
95
  queueNotification(pid: number, channel: string, payload: string): void;
@@ -92,6 +110,24 @@ export declare class Connection implements Session {
92
110
  /** Run one statement against the engine, honoring the turn, locks, aborted state and faults. */
93
111
  private execute;
94
112
  private executeInner;
113
+ /**
114
+ * Durable mode, before a statement that may commit: how to take that commit back out of
115
+ * memory if storage then refuses it. Null when the statement cannot commit a change.
116
+ */
117
+ private durableUndo;
118
+ /** What a transaction's BEGIN captures and its ROLLBACK restores, for a commit already applied. */
119
+ private checkpoint;
120
+ /**
121
+ * Persist a commit this session just applied, while it still holds the statement turn, so no
122
+ * other session has seen it. Storage refusing it takes the commit back out of memory and
123
+ * fails the statement (`58030`, or `53100` when storage is full); the transaction is gone,
124
+ * as after any failed COMMIT. Resolves to the commit when storage was written.
125
+ */
126
+ private persist;
127
+ /** The acknowledgement of every persisted commit is on the socket. */
128
+ private acknowledged;
129
+ /** CREATE / DROP / ALTER DATABASE in durable mode: the catalog change is persisted, or undone. */
130
+ private durableClusterDdl;
95
131
  private waitForLock;
96
132
  /** The engine call itself: statement-atomic, and transaction control handled as the server sees it. */
97
133
  private runStatement;
@@ -119,6 +155,8 @@ export declare class Connection implements Session {
119
155
  private copyStatement;
120
156
  private copyColumnCount;
121
157
  private handleCopyIn;
158
+ /** Engine work of a COPY in progress. A workspace already carries the session's settings. */
159
+ private inCopy;
122
160
  private beginCopyIn;
123
161
  private finishCopyIn;
124
162
  private flushCopyRecords;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Opt-in durable mode for the wire server: a commit is acknowledged only once the whole cluster,
3
+ * as PGMM snapshots, has reached a {@link DurableStorage}. It is snapshot persistence, not a
4
+ * write-ahead log and not PostgreSQL's on-disk format. The guarantee it models is the one a
5
+ * client relies on: https://www.postgresql.org/docs/18/wal-intro.html (an acknowledged COMMIT
6
+ * survives a crash) and https://www.postgresql.org/docs/18/sql-commit.html.
7
+ *
8
+ * Nothing here touches a filesystem: the storage is a port the server awaits, so a test can
9
+ * model failure and pauses, and {@link fileStorage} (./file-storage.ts) is the real one.
10
+ */
11
+ import "../serialization/codec.js";
12
+ import { type Database } from "../api/database.js";
13
+ import { PostgresError } from "../errors/error.js";
14
+ import type { Cluster } from "./cluster.js";
15
+ /** One durable image of the cluster: each database's PGMM snapshot bytes by database name. */
16
+ export type ClusterImage = ReadonlyMap<string, Uint8Array>;
17
+ /** Where durable mode keeps the cluster. The server awaits it before acknowledging a commit. */
18
+ export interface DurableStorage {
19
+ /** The image of the last {@link write} that resolved, or `null` when nothing was persisted yet. */
20
+ read(): Promise<ClusterImage | null>;
21
+ /**
22
+ * Replace the persisted image with `image`, atomically: resolve once it would survive the
23
+ * process being killed, or reject and leave the previous image intact. The server never
24
+ * calls it concurrently, and passes the same `Uint8Array` object for a database that did
25
+ * not change since the last call, so an implementation can skip rewriting it.
26
+ */
27
+ write(image: ClusterImage): Promise<void>;
28
+ }
29
+ /** A commit the server persisted, as the test controls of {@link DurableOptions} see it. */
30
+ export type DurableCommit = {
31
+ /** Backend pid of the session that committed. */
32
+ pid: number;
33
+ /** Database the commit belongs to. */
34
+ database: string;
35
+ /** The statement that committed: the write itself in autocommit, else `COMMIT`. */
36
+ sql: string;
37
+ };
38
+ export type DurableOptions = {
39
+ /** A directory for the file-backed store, or an injected {@link DurableStorage}. */
40
+ storage: string | DurableStorage;
41
+ /**
42
+ * Test control: awaited after a commit was persisted and before the server acknowledges it.
43
+ * A process killed while this is pending restarts with the commit; the client never saw it.
44
+ */
45
+ beforeAcknowledge?: (commit: DurableCommit) => void | Promise<void>;
46
+ /**
47
+ * Test control: awaited right after the acknowledgement was written to the socket
48
+ * (`CommandComplete`, or `ReadyForQuery` for the implicit commit of a multi-statement query).
49
+ */
50
+ afterAcknowledge?: (commit: DurableCommit) => void | Promise<void>;
51
+ };
52
+ /**
53
+ * A storage failure as the client sees it: the commit is not acknowledged, and the statement
54
+ * ends with `58030` (io_error), or `53100` (disk_full) when the storage ran out of space.
55
+ */
56
+ export declare const storageFailure: (cause: unknown) => PostgresError;
57
+ /** Rebuild the databases of a persisted image, with the runtime (clock, seed, entropy) of `template`. */
58
+ export declare const openImage: (image: ClusterImage, template: Database) => Map<string, Database>;
59
+ /** Persists the cluster for the server, re-encoding only the databases a commit touched. */
60
+ export declare class Durability {
61
+ private readonly cluster;
62
+ private readonly storage;
63
+ private readonly hooks;
64
+ /** PGMM bytes of each database as storage holds them. */
65
+ private readonly persisted;
66
+ private image;
67
+ private tail;
68
+ constructor(cluster: Cluster, storage: DurableStorage, hooks: Pick<DurableOptions, "beforeAcknowledge" | "afterAcknowledge">);
69
+ /** Record that storage already holds `image` for the cluster's databases (after a restart). */
70
+ adopt(image: ClusterImage): void;
71
+ /**
72
+ * Write the cluster with `changed` (or every database) encoded from its live state. Resolves
73
+ * to whether storage was written: an image identical to the persisted one is skipped. The
74
+ * caller holds the statement turn, so no session runs between the commit and this.
75
+ */
76
+ persist(changed: readonly Database[] | "all"): Promise<boolean>;
77
+ /** Resolves when no write is in flight. */
78
+ idle(): Promise<void>;
79
+ private write;
80
+ beforeAcknowledge(commit: DurableCommit): void | Promise<void>;
81
+ afterAcknowledge(commit: DurableCommit): void | Promise<void>;
82
+ }
@@ -0,0 +1,3 @@
1
+ import type { DurableStorage } from "./durable.js";
2
+ /** Persist a cluster under the directory `root` (created when missing). One server per root. */
3
+ export declare const fileStorage: (root: string) => DurableStorage;
@@ -10,7 +10,7 @@ type ClockState = {
10
10
  now: number;
11
11
  /** True while time does not advance on its own. */
12
12
  frozen: boolean;
13
- /** Milliseconds this clock adds to its underlying source. */
13
+ /** Milliseconds a running clock adds to its underlying source. `0` while frozen. */
14
14
  offsetMs: number;
15
15
  };
16
16
  type Clock = {
@@ -28,6 +28,213 @@ type Clock = {
28
28
  state(): ClockState;
29
29
  };
30
30
 
31
+ /**
32
+ * A deliberate failure injected in front of an operation.
33
+ *
34
+ * This is how a suite reaches the vendor's failure modes without the vendor: the
35
+ * quota error that only appears when a shared sandbox is full, the 429 that only
36
+ * appears under load, the 5xx that proves a retry path works.
37
+ *
38
+ * A rule has three parts. What it **matches** (`operationId`, `method`, `path`, …), **when**
39
+ * it fires among the matching requests (`after`, `every`, `count`, `rate`, `from`, `until`),
40
+ * and what it **does** (`status`, `delay`, `timeout`, `drop`, `truncate`, `effect`).
41
+ */
42
+ type FaultRule = {
43
+ /** Stable id, so a suite can retire exactly the rule it added. Unique within a namespace. */
44
+ id: string;
45
+ /** Fault only this operation. Omit to match every operation. */
46
+ operationId?: string;
47
+ /** Fault only this HTTP method, case-insensitive. Omit to match every method. */
48
+ method?: string;
49
+ /** Fault only this exact path. */
50
+ path?: string;
51
+ /** Fault only paths starting with this prefix. Omit to match every path. */
52
+ pathPrefix?: string;
53
+ /** Fault only paths matching this regular expression (JavaScript syntax, unanchored). */
54
+ pathPattern?: string;
55
+ /** Fault only requests carrying these headers. `"*"` matches any value. */
56
+ requestHeaders?: Record<string, string>;
57
+ /** Fault only requests carrying these query parameters. `"*"` matches any value. */
58
+ query?: Record<string, string>;
59
+ /**
60
+ * Fault only this namespace. Omit (or `"*"`) to fault every namespace — which is what
61
+ * an in-process caller usually wants, and what a parallel worker usually does not:
62
+ * rules added through `POST /__admin/faults` default to the calling namespace.
63
+ */
64
+ namespace?: string;
65
+ /** Let this many matching requests through first. */
66
+ after?: number;
67
+ /** Then fault only every nth matching request: `3` faults the 3rd, 6th, 9th. */
68
+ every?: number;
69
+ /** Retire the rule after this many faults. Omit to keep it until removed. */
70
+ count?: number;
71
+ /** Fault this fraction of the remaining requests, `0`–`1`, drawn from the seed. Default `1`. */
72
+ rate?: number;
73
+ /**
74
+ * Fault only from this instant on the namespace clock. A duration (`"5m"`) counts from the
75
+ * clock when the rule was added. Listed as epoch ms.
76
+ */
77
+ from?: Instant | Duration;
78
+ /** Stop faulting at this instant on the namespace clock. A duration counts from when added. */
79
+ until?: Instant | Duration;
80
+ /**
81
+ * Status of the injected response. Omit for a rule that only delays, times out, drops,
82
+ * truncates, or switches on an `effect`: the request then still reaches the service.
83
+ */
84
+ status?: number;
85
+ /** Response body, serialized as JSON. A string is sent as-is. */
86
+ body?: unknown;
87
+ /** Response headers. */
88
+ headers?: Record<string, string>;
89
+ /** Hold the response back this long. Listed as `delayMs`. */
90
+ delay?: Duration;
91
+ /** `delay`, in milliseconds. */
92
+ delayMs?: number;
93
+ /** Alias of `delayMs`. */
94
+ latencyMs?: number;
95
+ /** Extra seeded delay in `[0, jitter]`. Listed as `jitterMs`. */
96
+ jitter?: Duration;
97
+ jitterMs?: number;
98
+ /**
99
+ * Never answer: hold the request this long, then drop the connection. Listed as
100
+ * `timeoutMs`. With virtual time the wait is spent on the mock clock.
101
+ */
102
+ timeout?: Duration;
103
+ timeoutMs?: number;
104
+ /**
105
+ * Drop the connection instead of answering: an in-process `fetch` rejects with a
106
+ * `TypeError`, and a served mock destroys the socket. Models "unknown outcome" failures.
107
+ */
108
+ drop?: boolean;
109
+ /**
110
+ * Cut the response body off after this many bytes: the status and headers arrive, then
111
+ * the connection dies mid-body.
112
+ */
113
+ truncate?: number;
114
+ /**
115
+ * A named service behaviour to switch on for the matching request instead of (or
116
+ * before) a canned response, e.g. `created_but_500` or `numeric_tracking_id`. Services
117
+ * read it with `faultEffects(request)`.
118
+ */
119
+ effect?: string;
120
+ /** Parameters for `effect`. */
121
+ params?: Record<string, unknown>;
122
+ /** The preset this rule was expanded from, if any. */
123
+ fromPreset?: string;
124
+ };
125
+
126
+ /**
127
+ * The admin controls every mock shares, as one document.
128
+ *
129
+ * The same shape is accepted in three places, and means the same thing in each:
130
+ *
131
+ * - at boot: `createRuntime({ adminControls })`, `serve --admin-controls`,
132
+ * `MOCKINGBIRD_ADMIN_CONTROLS`
133
+ * - over HTTP: `PUT /__admin/controls`
134
+ * - per request: the `x-mockingbird-*` headers in {@link CONTROL_HEADERS}
135
+ *
136
+ * Every control is deterministic: given the same seed, clock and requests, a mock answers
137
+ * the same bytes. Nothing here reads the wall clock or `Math.random`.
138
+ */
139
+ /** Milliseconds, or a string like `"250ms"`, `"2s"`, `"15m"`, `"1h"`, `"3d"`. */
140
+ type Duration = number | string;
141
+ /** Epoch milliseconds, or an ISO-8601 string. */
142
+ type Instant = number | string;
143
+ type ClockControls = {
144
+ /** Move the clock to this instant. */
145
+ now?: Instant;
146
+ /** Stop time. A frozen clock moves only when told to, so timestamps replay exactly. */
147
+ frozen?: boolean;
148
+ /**
149
+ * Step the clock forward by this much before every vendor request. With a frozen clock
150
+ * each request still gets its own, reproducible timestamp.
151
+ */
152
+ autoAdvance?: Duration;
153
+ };
154
+ /** How long a response takes: a fixed delay plus seeded jitter in `[0, jitter]`. */
155
+ type LatencyRuleInput = {
156
+ delay?: Duration;
157
+ jitter?: Duration;
158
+ };
159
+ type LatencyControls = LatencyRuleInput & {
160
+ /** Per-operation overrides, keyed by operationId. A bare duration is its `delay`. */
161
+ operations?: Record<string, Duration | LatencyRuleInput>;
162
+ };
163
+ /** A fault to add: a rule (the id is assigned when omitted), or a named preset with overrides. */
164
+ type FaultInput = (Omit<FaultRule, "id"> & {
165
+ id?: string;
166
+ }) | ({
167
+ preset: string;
168
+ } & Partial<FaultRule>);
169
+ /** Identifies a {@link SnapshotDocument}. */
170
+ declare const SNAPSHOT_FORMAT = "mockingbird.snapshot/v1";
171
+ /**
172
+ * A namespace, as portable JSON: what `GET /__admin/snapshots/:id` returns and what
173
+ * `PUT /__admin/snapshots/:id` and the boot `snapshot` control accept. Only `format` and
174
+ * `records` are required, so a hand-written seed file is a valid snapshot.
175
+ */
176
+ type SnapshotDocument = {
177
+ format: typeof SNAPSHOT_FORMAT;
178
+ /** The service that wrote it. A snapshot is rejected by any other service. */
179
+ service?: string;
180
+ /**
181
+ * Stored rows. `seq` orders a collection; omitted, rows keep their listed order. `raw` is
182
+ * the stored text of a row that is not a plain record, and replaces `value` when present.
183
+ */
184
+ records: {
185
+ collection: string;
186
+ id: string;
187
+ seq?: number;
188
+ value?: unknown;
189
+ raw?: string;
190
+ }[];
191
+ /** Id counters, so the next generated id continues where the snapshot left off. */
192
+ sequences?: {
193
+ name: string;
194
+ kind: string;
195
+ value: number;
196
+ }[];
197
+ clock?: {
198
+ now: number;
199
+ frozen: boolean;
200
+ autoAdvanceMs?: number;
201
+ };
202
+ seed?: number | string;
203
+ /** Positions of the seeded random streams. */
204
+ rng?: {
205
+ state: number;
206
+ faults?: number;
207
+ latency?: number;
208
+ };
209
+ faults?: unknown;
210
+ latency?: unknown;
211
+ virtualTime?: boolean;
212
+ };
213
+ /** Controls for one namespace. */
214
+ type NamespaceControls = {
215
+ /** Seeds every random choice: fault rates, jitter, and whatever the service invents. */
216
+ seed?: number | string;
217
+ clock?: ClockControls;
218
+ /**
219
+ * Spend injected waits (latency, fault delays, timeouts) on the mock clock instead of the
220
+ * wall clock: the request returns at once, the clock has moved, and the journal records
221
+ * the wait exactly. Default `false`: waits are real, so a client's own timeout can fire.
222
+ */
223
+ virtualTime?: boolean;
224
+ /** Baseline response time for every vendor request. A bare duration is its `delay`. */
225
+ latency?: Duration | LatencyControls;
226
+ faults?: readonly FaultInput[];
227
+ /** Start from this state instead of the service's own seed data. */
228
+ snapshot?: SnapshotDocument;
229
+ };
230
+ /** {@link NamespaceControls} for every namespace, with per-namespace overrides. */
231
+ type MockControls = NamespaceControls & {
232
+ /** Vendor credential → namespace, for SDKs that cannot send a namespace header. */
233
+ credentials?: Record<string, string>;
234
+ /** Overrides for named namespaces. A key given here replaces the shared value. */
235
+ namespaces?: Record<string, NamespaceControls>;
236
+ };
237
+
31
238
  type FleetDiagnostics = {
32
239
  activeRequests: number;
33
240
  activeConnections: number;
@@ -66,6 +273,8 @@ type ConfigService = {
66
273
  adminPrefix?: string;
67
274
  adminKey?: string;
68
275
  seed?: string;
276
+ /** Admin controls this service boots with. A key given here replaces the fleet-wide one. */
277
+ adminControls?: MockControls;
69
278
  fixtures?: Record<string, unknown>;
70
279
  baseUrl?: string;
71
280
  password?: string;
@@ -11,6 +11,9 @@
11
11
  * transaction blocks. Advisory locks, `LISTEN`/`NOTIFY`, `CancelRequest` and per-connection
12
12
  * aborted-transaction state (`25P02`) are coordinated across connections.
13
13
  *
14
+ * State lives in memory. The opt-in `durable` option persists the cluster before each commit is
15
+ * acknowledged (see ./durable.ts); without it the server touches no filesystem.
16
+ *
14
17
  * @example
15
18
  * ```ts
16
19
  * import { serve } from "@crvouga/mockingbird-service-postgres/wire";
@@ -26,6 +29,7 @@ import { type Server } from "node:net";
26
29
  import { Database } from "../api/database.js";
27
30
  import type { Snapshot } from "../api/snapshot.js";
28
31
  import { type ServerFaults, type ServerLog } from "./connection.js";
32
+ import { type DurableOptions, type DurableStorage } from "./durable.js";
29
33
  export type ServeOptions = {
30
34
  /** TCP port; `0` (the default) picks a free one, reported as `server.port`. */
31
35
  port?: number;
@@ -59,6 +63,15 @@ export type ServeOptions = {
59
63
  allowDatabaseSelection?: boolean;
60
64
  /** Role shown in `connectionString`. A URI's username fills this. Default `postgres`. */
61
65
  user?: string;
66
+ /**
67
+ * Opt-in durable mode: a directory (the file-backed store), an injected {@link DurableStorage},
68
+ * or {@link DurableOptions} with test controls. An autocommit write or `COMMIT` is then
69
+ * persisted before it is acknowledged, and a server started on the same storage resumes from
70
+ * the last acknowledged commit, even after a `SIGKILL`. Storage that already holds a cluster
71
+ * wins over `database`, which then only supplies the clock, seed and entropy mode. Omitted
72
+ * (the default), state lives in memory only and no filesystem is touched.
73
+ */
74
+ durable?: string | DurableStorage | DurableOptions;
62
75
  };
63
76
  export type PostgresServer = {
64
77
  /** The bound port. */
@@ -85,7 +98,12 @@ export type PostgresServer = {
85
98
  snapshotAll(): Map<string, Snapshot>;
86
99
  restoreAll(points: ReadonlyMap<string, Snapshot>): void;
87
100
  faultState(): ServerFaults;
88
- /** Stop listening and close every connection. */
101
+ /**
102
+ * Durable mode: persist every database now, for changes made through `database` /
103
+ * `getDatabase()` / `restoreAll()` rather than a connection. Resolves at once without `durable`.
104
+ */
105
+ persist(): Promise<void>;
106
+ /** Stop listening and close every connection. Durable mode also waits for a write in flight. */
89
107
  close(): Promise<void>;
90
108
  };
91
109
  export type ServeInput = ServeOptions | string;
@@ -93,3 +111,5 @@ export type ServeInput = ServeOptions | string;
93
111
  export declare const serve: (input?: ServeInput) => Promise<PostgresServer>;
94
112
  export { Cluster } from "./cluster.js";
95
113
  export type { ServerFaults, ServerLog } from "./connection.js";
114
+ export type { ClusterImage, DurableCommit, DurableOptions, DurableStorage } from "./durable.js";
115
+ export { fileStorage } from "./file-storage.js";