@zvndev/powdb-client 0.8.1 → 0.10.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.
@@ -0,0 +1,393 @@
1
+ /**
2
+ * PowDB TypeScript client.
3
+ *
4
+ * Thin async wrapper around a TCP (or TLS) socket speaking the PowDB wire
5
+ * protocol.
6
+ *
7
+ * const client = await Client.connect({
8
+ * host: "127.0.0.1",
9
+ * port: 5433,
10
+ * dbName: "default",
11
+ * password: process.env.POWDB_PASSWORD,
12
+ * });
13
+ *
14
+ * const result = await client.query("User filter .age > 27 { .name, .age }");
15
+ * await client.close();
16
+ */
17
+ import * as tls from "node:tls";
18
+ import { EventEmitter } from "node:events";
19
+ import { type SyncRepairAction, type WireRetainedUnit, type WireSyncStatus } from "./protocol.js";
20
+ import { type TypedRow, type TypedSchema } from "./typed.js";
21
+ /** Client library version. Compared to the server's reported version. */
22
+ export declare const CLIENT_VERSION = "0.10.0";
23
+ export type QueryResult = {
24
+ kind: "rows";
25
+ columns: string[];
26
+ rows: string[][];
27
+ } | {
28
+ kind: "scalar";
29
+ value: string;
30
+ } | {
31
+ kind: "ok";
32
+ affected: bigint;
33
+ } | {
34
+ kind: "message";
35
+ message: string;
36
+ };
37
+ /**
38
+ * A value bound to a positional `$N` placeholder in {@link Client.query}.
39
+ *
40
+ * The server binds these at the token level — a string is substituted as a
41
+ * literal token, never interpolated — so injection-shaped input is inert.
42
+ * Numbers bind as ints when integral and floats otherwise; `bigint` always
43
+ * binds as an int; `null` binds PowQL `null`.
44
+ */
45
+ export type QueryParam = string | number | bigint | boolean | null;
46
+ /** Options for {@link Client.execScript} / {@link Pool.execScript}. */
47
+ export interface ExecScriptOptions {
48
+ /**
49
+ * When `true`, keep dispatching statements after one fails and return a
50
+ * per-statement outcome array instead of throwing. Defaults to `false`
51
+ * (fail-fast: stop dispatching and reject with a {@link PowDBScriptError}
52
+ * carrying the failing statement's index and the results so far).
53
+ */
54
+ continueOnError?: boolean;
55
+ /**
56
+ * When `true`, run the whole script atomically: `execScript` opens a
57
+ * transaction before dispatching and sends `commit` only after EVERY
58
+ * statement's reply has arrived successfully — on any failure it sends
59
+ * `rollback` instead, so no statement's effect survives. This is the only
60
+ * all-or-nothing mode: embedding your own `begin`/`commit` in a pipelined
61
+ * script is NOT safe (a trailing `commit` is already on the wire when an
62
+ * earlier error reply arrives, so partial work would commit), and a
63
+ * transactional script containing its own transaction-control statements
64
+ * is rejected up front. Mutually exclusive with `continueOnError`.
65
+ */
66
+ transactional?: boolean;
67
+ /** Abort the remaining statements (see {@link Client.query}). */
68
+ signal?: AbortSignal;
69
+ }
70
+ /**
71
+ * Per-statement outcome from `execScript(script, { continueOnError: true })`.
72
+ * Array order matches statement order in the script.
73
+ */
74
+ export type ScriptStatementOutcome = {
75
+ statement: string;
76
+ ok: true;
77
+ result: QueryResult;
78
+ } | {
79
+ statement: string;
80
+ ok: false;
81
+ error: Error;
82
+ };
83
+ /** Unsigned 64-bit sync protocol value. Numbers must be safe non-negative integers. */
84
+ export type SyncU64 = bigint | number;
85
+ /**
86
+ * Database identity carried by the sync protocol. Strings are the 32-hex-char
87
+ * form stored in `.powdb-sync/identity.json`; byte arrays must be exactly 16B.
88
+ */
89
+ export type SyncDatabaseId = string | Uint8Array;
90
+ export interface SyncPullRequest {
91
+ replicaId: string;
92
+ sinceLsn: SyncU64;
93
+ maxUnits: number;
94
+ maxBytes: SyncU64;
95
+ databaseId: SyncDatabaseId;
96
+ primaryGeneration: SyncU64;
97
+ walFormatVersion: number;
98
+ catalogVersion: number;
99
+ segmentFormatVersion: number;
100
+ }
101
+ export interface SyncAckRequest {
102
+ replicaId: string;
103
+ appliedLsn: SyncU64;
104
+ remoteLsn: SyncU64;
105
+ }
106
+ export interface SyncPullResult {
107
+ status: WireSyncStatus;
108
+ units: WireRetainedUnit[];
109
+ hasMore: boolean;
110
+ }
111
+ export interface SyncAckResult {
112
+ previousAppliedLsn: bigint;
113
+ appliedLsn: bigint;
114
+ remoteLsn: bigint;
115
+ advanced: boolean;
116
+ status: WireSyncStatus;
117
+ }
118
+ export interface ClientOptions {
119
+ /** TCP host. Required unless `path` (a Unix domain socket) is given. */
120
+ host?: string;
121
+ /** TCP port. Required unless `path` (a Unix domain socket) is given. */
122
+ port?: number;
123
+ /**
124
+ * Path to a Unix domain socket. When set, the client connects over the
125
+ * socket instead of TCP (same-host, ~2× lower round-trip latency) and
126
+ * `host`/`port`/`tls` are ignored. Requires a server started with
127
+ * `--socket <path>`.
128
+ */
129
+ path?: string;
130
+ dbName?: string;
131
+ password?: string | null;
132
+ /**
133
+ * User name for multi-user authentication. Servers ≥0.4.5 with named users
134
+ * defined require a `(user, password)` pair; role enforcement (readonly vs
135
+ * readwrite) requires server ≥0.4.6. Omit for legacy shared-password or
136
+ * no-auth servers — the Connect frame is then byte-identical to 0.3.x.
137
+ */
138
+ user?: string;
139
+ /** Connection timeout in ms. Defaults to 5000. */
140
+ connectTimeoutMs?: number;
141
+ /**
142
+ * Enable TLS. When `true`, connect over TLS with system defaults
143
+ * (servername is taken from `host`). When an object, passed through to
144
+ * `tls.connect(port, host, options)`. Defaults to plain TCP.
145
+ */
146
+ tls?: boolean | tls.ConnectionOptions;
147
+ /**
148
+ * Pipelined ("eager") connect. When `true`, {@link Client.connect}
149
+ * resolves as soon as the socket is open and the Connect frame has been
150
+ * written — it does NOT wait for the server's ConnectOk. Queries issued
151
+ * immediately are queued and written right behind the Connect frame (the
152
+ * server reads frames sequentially, so this is valid on the wire), saving
153
+ * a full round trip on every fresh connection.
154
+ *
155
+ * If the handshake then fails (bad password, protocol mismatch), every
156
+ * queued query rejects with the handshake error and the client closes.
157
+ * `serverVersion` is `""` until the ConnectOk arrives; await
158
+ * {@link Client.ready} if you need the handshake settled. Defaults to
159
+ * `false` (connect blocks until ConnectOk, exactly as before).
160
+ */
161
+ eager?: boolean;
162
+ }
163
+ /**
164
+ * Event map for {@link Client}. Typed so `client.on("query", ...)` gets
165
+ * inference for the payload.
166
+ *
167
+ * - `"query"`: one emission per completed (or failed) query attempt,
168
+ * whether via `query` or `queryTyped`. `durationMs` is the client-side
169
+ * round-trip including abort handling. `ok=false` means the server
170
+ * returned an Error frame or the client rejected locally.
171
+ * - `"close"`: the underlying socket has been fully torn down. Emitted
172
+ * once per client.
173
+ */
174
+ export interface ClientEvents {
175
+ query: [
176
+ {
177
+ query: string;
178
+ durationMs: number;
179
+ ok: boolean;
180
+ kind?: "rows" | "scalar" | "ok" | "message";
181
+ error?: Error;
182
+ }
183
+ ];
184
+ sync: [
185
+ {
186
+ operation: "status" | "pull" | "ack";
187
+ replicaId: string;
188
+ durationMs: number;
189
+ ok: boolean;
190
+ status?: WireSyncStatus;
191
+ stale?: boolean;
192
+ repairAction?: SyncRepairAction;
193
+ units?: number;
194
+ advanced?: boolean;
195
+ error?: Error;
196
+ }
197
+ ];
198
+ close: [{
199
+ error: Error | null;
200
+ }];
201
+ }
202
+ export declare class Client extends EventEmitter<ClientEvents> {
203
+ private readonly socket;
204
+ /** FIFO of raw chunks; concatenated lazily when we try to decode. */
205
+ private readonly chunks;
206
+ /** Cached length of everything currently in `chunks`. */
207
+ private totalLen;
208
+ private readonly pending;
209
+ private closed;
210
+ private closeError;
211
+ /** Settled once the Connect→ConnectOk handshake completes (or fails). */
212
+ private handshake;
213
+ /** True once ConnectOk has been received. */
214
+ private handshakeComplete;
215
+ private _serverVersion;
216
+ /**
217
+ * Server version from the ConnectOk frame. For a client opened with
218
+ * `eager: true` this is `""` until the handshake reply arrives (await
219
+ * {@link ready} to guarantee it is populated).
220
+ */
221
+ get serverVersion(): string;
222
+ private constructor();
223
+ /**
224
+ * Open a connection, send Connect, and wait for ConnectOk.
225
+ *
226
+ * With `eager: true`, resolve as soon as the Connect frame is written
227
+ * instead — queries may be issued immediately and are pipelined behind
228
+ * the handshake (see {@link ClientOptions.eager} and {@link ready}).
229
+ */
230
+ static connect(opts: ClientOptions): Promise<Client>;
231
+ /**
232
+ * Resolves once the Connect→ConnectOk handshake has completed; rejects
233
+ * with the handshake error if it failed. For non-eager clients this has
234
+ * already settled by the time {@link connect} returns. Eager callers can
235
+ * await it to learn the handshake outcome without issuing a query.
236
+ */
237
+ ready(): Promise<void>;
238
+ /**
239
+ * Write the Connect frame and register the handshake as the first entry
240
+ * in the pending queue. The reply-matching machinery is strictly FIFO, so
241
+ * the ConnectOk (or Error) frame is matched to the handshake before any
242
+ * pipelined query sees a reply. On failure, every queued query is
243
+ * rejected with the handshake error and the socket is torn down.
244
+ */
245
+ private startHandshake;
246
+ /**
247
+ * Run a PowQL statement and return the typed result.
248
+ *
249
+ * When `opts.signal` is provided and fires, the returned promise rejects
250
+ * with the signal's `reason` (or an `AbortError`). The socket is NOT
251
+ * destroyed — the server will still eventually send its reply, which we
252
+ * silently discard so other in-flight queries keep working.
253
+ */
254
+ query(query: string, paramsOrOpts?: QueryParam[] | {
255
+ signal?: AbortSignal;
256
+ }, maybeOpts?: {
257
+ signal?: AbortSignal;
258
+ }): Promise<QueryResult>;
259
+ /**
260
+ * Run a SQL statement through the server-side SQL frontend. The plain
261
+ * {@link query} method remains PowQL for wire compatibility.
262
+ */
263
+ querySql(query: string, opts?: {
264
+ signal?: AbortSignal;
265
+ }): Promise<QueryResult>;
266
+ /**
267
+ * Fetch primary-side sync status for one embedded replica cursor.
268
+ *
269
+ * This speaks the private authenticated sync frame added for the embedded
270
+ * replica product. Servers without sync support return a protocol/query
271
+ * error; unauthenticated or readonly users are rejected server-side.
272
+ */
273
+ syncStatus(replicaId: string, opts?: {
274
+ signal?: AbortSignal;
275
+ }): Promise<WireSyncStatus>;
276
+ /**
277
+ * Pull a bounded retained-unit chunk after this replica's primary-side cursor.
278
+ *
279
+ * The caller supplies the database identity and format versions from the
280
+ * sync bootstrap metadata. The server rejects mismatches and non-applyable
281
+ * transaction cuts instead of returning ambiguous history.
282
+ */
283
+ syncPull(request: SyncPullRequest, opts?: {
284
+ signal?: AbortSignal;
285
+ }): Promise<SyncPullResult>;
286
+ /** Acknowledge that the replica applied retained history through `appliedLsn`. */
287
+ syncAck(request: SyncAckRequest, opts?: {
288
+ signal?: AbortSignal;
289
+ }): Promise<SyncAckResult>;
290
+ /**
291
+ * Like {@link query}, but coerces string result columns to typed JS values
292
+ * using the caller-supplied schema. See `./typed.ts` for the coercion
293
+ * rules and supported column types.
294
+ *
295
+ * Returns a `TypedRow[]` — an array of objects keyed by column name.
296
+ * Throws `PowDBError(code="query_failed")` if the query is not a
297
+ * rows-returning query.
298
+ */
299
+ queryTyped(query: string, schema: TypedSchema, opts?: {
300
+ signal?: AbortSignal;
301
+ }): Promise<TypedRow[]>;
302
+ /**
303
+ * Execute a multi-statement PowQL script on this connection, pipelined.
304
+ *
305
+ * The script is split with the same statement-aware semantics as the CLI
306
+ * (see {@link splitStatements}: `;` inside string literals and `#`
307
+ * comments never splits; empty statements are dropped). All statements
308
+ * are then written down the single connection back-to-back WITHOUT
309
+ * waiting for each reply — the server reads frames sequentially, so an
310
+ * N-statement script costs one round trip instead of N. Results are
311
+ * collected in statement order.
312
+ *
313
+ * Error handling:
314
+ * - Default (fail-fast): resolve with `QueryResult[]` (one entry per
315
+ * statement). On the first failed statement, stop dispatching further
316
+ * statements and reject with a {@link PowDBScriptError} carrying the
317
+ * failing `statementIndex`, the `statement` text, and the successful
318
+ * `results` so far. NOTE: because dispatch is pipelined, statements
319
+ * already written when the error reply arrives (typically the whole
320
+ * script) still execute server-side. If you need all-or-nothing
321
+ * behavior use `transactional: true` — do NOT embed `begin`/`commit`
322
+ * in the script yourself: the trailing `commit` is already on the
323
+ * wire when an error reply arrives, so it commits the partial work.
324
+ * - `transactional: true`: `execScript` opens the transaction itself,
325
+ * waits for every statement's reply, and only then sends `commit`
326
+ * (or `rollback` if any statement failed), so no statement's effect
327
+ * survives a failure. On failure the {@link PowDBScriptError}'s
328
+ * `results` are the replies received before rollback — their effects
329
+ * are NOT persisted.
330
+ * - `continueOnError: true`: dispatch every statement regardless of
331
+ * failures and resolve with a dense {@link ScriptStatementOutcome}
332
+ * array recording each statement's result or error.
333
+ *
334
+ * Each statement emits the usual `"query"` event.
335
+ */
336
+ execScript(script: string, opts?: {
337
+ continueOnError?: false;
338
+ transactional?: boolean;
339
+ signal?: AbortSignal;
340
+ }): Promise<QueryResult[]>;
341
+ execScript(script: string, opts: {
342
+ continueOnError: true;
343
+ signal?: AbortSignal;
344
+ }): Promise<ScriptStatementOutcome[]>;
345
+ /**
346
+ * Poll a query on an interval and invoke `onRows` for every successful
347
+ * run. Returns an unsubscribe function. Does NOT deduplicate results —
348
+ * `onRows` fires every interval, even if the rows are unchanged.
349
+ *
350
+ * Pragmatic first-cut live-data. If a query takes longer than
351
+ * `intervalMs`, the next tick waits for the in-flight one to finish
352
+ * (no pile-up). Errors fire `onError` (if provided) without stopping
353
+ * the watcher unless `stopOnError: true`.
354
+ */
355
+ watch(query: string, opts: {
356
+ intervalMs: number;
357
+ onRows: (rows: QueryResult) => void;
358
+ onError?: (err: Error) => void;
359
+ stopOnError?: boolean;
360
+ }): {
361
+ stop: () => void;
362
+ };
363
+ /**
364
+ * Send Disconnect and tear down the socket. Waits for the remote FIN-ack
365
+ * (`socket.end` callback) but falls back to `destroy()` after a bounded
366
+ * timeout so an unresponsive peer cannot make `close()` hang forever.
367
+ */
368
+ close(): Promise<void>;
369
+ private send;
370
+ /**
371
+ * Resolve once the socket's write buffer has drained. Also resolves if
372
+ * the client closes while waiting, so a dead peer cannot hang a
373
+ * backpressured `execScript` dispatch loop forever.
374
+ */
375
+ private drained;
376
+ private onData;
377
+ /** Collapse the chunk queue into a single Buffer. */
378
+ private coalesce;
379
+ /** Drop the first `n` bytes off the chunk queue. */
380
+ private consume;
381
+ private onClose;
382
+ }
383
+ export { encode, tryDecode } from "./protocol.js";
384
+ export type { Message, SyncRepairAction, WireParam, WireRetainedUnit, WireSyncStatus, } from "./protocol.js";
385
+ export { MAX_PAYLOAD_SIZE, MAX_ROWS, MAX_COLUMNS, MAX_PARAMS, MAX_SYNC_UNITS, MAX_SYNC_PULL_UNITS, MAX_SYNC_PULL_BYTES, } from "./protocol.js";
386
+ export { escapeLiteral, escapeIdent, ident, powql, PowqlIdent, } from "./escape.js";
387
+ export { Pool } from "./pool.js";
388
+ export type { PoolOptions } from "./pool.js";
389
+ export { splitStatements } from "./script.js";
390
+ export { PowDBError, isPowDBError, PowDBScriptError, isPowDBScriptError, } from "./errors.js";
391
+ export type { PowDBErrorCode } from "./errors.js";
392
+ export { coerceValue, coerceRow, coerceRows, } from "./typed.js";
393
+ export type { ColumnType, TypedSchema, TypedRow, Coerced, } from "./typed.js";