@zvndev/powdb-client 0.3.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,150 @@
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: "213.188.194.202",
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 TypedRow, type TypedSchema } from "./typed.js";
20
+ /** Client library version. Compared to the server's reported version. */
21
+ export declare const CLIENT_VERSION = "0.3.0";
22
+ export type QueryResult = {
23
+ kind: "rows";
24
+ columns: string[];
25
+ rows: string[][];
26
+ } | {
27
+ kind: "scalar";
28
+ value: string;
29
+ } | {
30
+ kind: "ok";
31
+ affected: bigint;
32
+ };
33
+ export interface ClientOptions {
34
+ host: string;
35
+ port: number;
36
+ dbName?: string;
37
+ password?: string | null;
38
+ /** Connection timeout in ms. Defaults to 5000. */
39
+ connectTimeoutMs?: number;
40
+ /**
41
+ * Enable TLS. When `true`, connect over TLS with system defaults
42
+ * (servername is taken from `host`). When an object, passed through to
43
+ * `tls.connect(port, host, options)`. Defaults to plain TCP.
44
+ */
45
+ tls?: boolean | tls.ConnectionOptions;
46
+ }
47
+ /**
48
+ * Event map for {@link Client}. Typed so `client.on("query", ...)` gets
49
+ * inference for the payload.
50
+ *
51
+ * - `"query"`: one emission per completed (or failed) query attempt,
52
+ * whether via `query` or `queryTyped`. `durationMs` is the client-side
53
+ * round-trip including abort handling. `ok=false` means the server
54
+ * returned an Error frame or the client rejected locally.
55
+ * - `"close"`: the underlying socket has been fully torn down. Emitted
56
+ * once per client.
57
+ */
58
+ export interface ClientEvents {
59
+ query: [
60
+ {
61
+ query: string;
62
+ durationMs: number;
63
+ ok: boolean;
64
+ kind?: "rows" | "scalar" | "ok";
65
+ error?: Error;
66
+ }
67
+ ];
68
+ close: [{
69
+ error: Error | null;
70
+ }];
71
+ }
72
+ export declare class Client extends EventEmitter<ClientEvents> {
73
+ private readonly socket;
74
+ /** FIFO of raw chunks; concatenated lazily when we try to decode. */
75
+ private readonly chunks;
76
+ /** Cached length of everything currently in `chunks`. */
77
+ private totalLen;
78
+ private readonly pending;
79
+ private closed;
80
+ private closeError;
81
+ readonly serverVersion: string;
82
+ private constructor();
83
+ /** Open a connection, send Connect, wait for ConnectOk. */
84
+ static connect(opts: ClientOptions): Promise<Client>;
85
+ /**
86
+ * Run a PowQL statement and return the typed result.
87
+ *
88
+ * When `opts.signal` is provided and fires, the returned promise rejects
89
+ * with the signal's `reason` (or an `AbortError`). The socket is NOT
90
+ * destroyed — the server will still eventually send its reply, which we
91
+ * silently discard so other in-flight queries keep working.
92
+ */
93
+ query(query: string, opts?: {
94
+ signal?: AbortSignal;
95
+ }): Promise<QueryResult>;
96
+ /**
97
+ * Like {@link query}, but coerces string result columns to typed JS values
98
+ * using the caller-supplied schema. See `./typed.ts` for the coercion
99
+ * rules and supported column types.
100
+ *
101
+ * Returns a `TypedRow[]` — an array of objects keyed by column name.
102
+ * Throws `PowDBError(code="query_failed")` if the query is not a
103
+ * rows-returning query.
104
+ */
105
+ queryTyped(query: string, schema: TypedSchema, opts?: {
106
+ signal?: AbortSignal;
107
+ }): Promise<TypedRow[]>;
108
+ /**
109
+ * Poll a query on an interval and invoke `onRows` for every successful
110
+ * run. Returns an unsubscribe function. Does NOT deduplicate results —
111
+ * `onRows` fires every interval, even if the rows are unchanged.
112
+ *
113
+ * Pragmatic first-cut live-data. If a query takes longer than
114
+ * `intervalMs`, the next tick waits for the in-flight one to finish
115
+ * (no pile-up). Errors fire `onError` (if provided) without stopping
116
+ * the watcher unless `stopOnError: true`.
117
+ */
118
+ watch(query: string, opts: {
119
+ intervalMs: number;
120
+ onRows: (rows: QueryResult) => void;
121
+ onError?: (err: Error) => void;
122
+ stopOnError?: boolean;
123
+ }): {
124
+ stop: () => void;
125
+ };
126
+ /**
127
+ * Send Disconnect and tear down the socket. Waits for the remote FIN-ack
128
+ * (`socket.end` callback) but falls back to `destroy()` after a bounded
129
+ * timeout so an unresponsive peer cannot make `close()` hang forever.
130
+ */
131
+ close(): Promise<void>;
132
+ private send;
133
+ private onData;
134
+ /** Collapse the chunk queue into a single Buffer. */
135
+ private coalesce;
136
+ /** Drop the first `n` bytes off the chunk queue. */
137
+ private consume;
138
+ private onClose;
139
+ }
140
+ export { encode, tryDecode } from "./protocol.js";
141
+ export type { Message } from "./protocol.js";
142
+ export { MAX_PAYLOAD_SIZE, MAX_ROWS, MAX_COLUMNS, } from "./protocol.js";
143
+ export { escapeLiteral, escapeIdent, ident, powql, PowqlIdent, } from "./escape.js";
144
+ export { Pool } from "./pool.js";
145
+ export type { PoolOptions } from "./pool.js";
146
+ export { PowDBError, isPowDBError } from "./errors.js";
147
+ export type { PowDBErrorCode } from "./errors.js";
148
+ export { coerceValue, coerceRow, coerceRows, } from "./typed.js";
149
+ export type { ColumnType, TypedSchema, TypedRow, Coerced, } from "./typed.js";
150
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,515 @@
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: "213.188.194.202",
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 net from "node:net";
18
+ import * as tls from "node:tls";
19
+ import { EventEmitter } from "node:events";
20
+ import { encode, tryDecode } from "./protocol.js";
21
+ import { PowDBError } from "./errors.js";
22
+ import { coerceRows, } from "./typed.js";
23
+ /** Client library version. Compared to the server's reported version. */
24
+ export const CLIENT_VERSION = "0.3.0";
25
+ /** Module-level set of host:port pairs we've already warned about. */
26
+ const versionWarnings = new Set();
27
+ /** Extract the major component of a dotted version string, e.g. "0.2.0" → "0". */
28
+ function majorOf(version) {
29
+ const dot = version.indexOf(".");
30
+ return dot === -1 ? version : version.slice(0, dot);
31
+ }
32
+ /**
33
+ * Build an AbortError. Prefers the caller-supplied `signal.reason` if it is
34
+ * itself an `Error` (matches DOM semantics); otherwise wraps it in a
35
+ * `PowDBError` with code `"aborted"` so catch-blocks can branch uniformly.
36
+ */
37
+ function abortError(signal) {
38
+ if (signal && signal.reason !== undefined) {
39
+ const r = signal.reason;
40
+ if (r instanceof Error)
41
+ return r;
42
+ return new PowDBError(String(r), "aborted");
43
+ }
44
+ return new PowDBError("query was aborted", "aborted");
45
+ }
46
+ export class Client extends EventEmitter {
47
+ socket;
48
+ /** FIFO of raw chunks; concatenated lazily when we try to decode. */
49
+ chunks = [];
50
+ /** Cached length of everything currently in `chunks`. */
51
+ totalLen = 0;
52
+ pending = [];
53
+ closed = false;
54
+ closeError = null;
55
+ serverVersion;
56
+ constructor(socket, serverVersion) {
57
+ super();
58
+ this.socket = socket;
59
+ this.serverVersion = serverVersion;
60
+ this.socket.on("data", (chunk) => this.onData(chunk));
61
+ this.socket.on("error", (err) => this.onClose(err));
62
+ this.socket.on("close", () => this.onClose(null));
63
+ }
64
+ /** Open a connection, send Connect, wait for ConnectOk. */
65
+ static async connect(opts) {
66
+ const { host, port, dbName = "default", password = null, connectTimeoutMs = 5000, tls: tlsOpt = false, } = opts;
67
+ const socket = await openSocket(host, port, connectTimeoutMs, tlsOpt);
68
+ // We need to read the initial ConnectOk before wiring up the normal
69
+ // pending-queue machinery, so we do a one-shot handshake here.
70
+ const handshake = new Promise((resolve, reject) => {
71
+ let scratch = Buffer.alloc(0);
72
+ const cleanup = () => {
73
+ socket.removeListener("data", onData);
74
+ socket.removeListener("error", onError);
75
+ socket.removeListener("close", onClose);
76
+ };
77
+ const onData = (chunk) => {
78
+ scratch = Buffer.concat([scratch, chunk]);
79
+ let decoded;
80
+ try {
81
+ decoded = tryDecode(scratch);
82
+ }
83
+ catch (err) {
84
+ cleanup();
85
+ reject(err);
86
+ return;
87
+ }
88
+ if (decoded !== null) {
89
+ cleanup();
90
+ // Any bytes past the handshake frame belong to later responses.
91
+ // This should not happen in practice, but handle it defensively.
92
+ const leftover = scratch.subarray(decoded.consumed);
93
+ if (leftover.length > 0) {
94
+ socket.unshift(leftover);
95
+ }
96
+ resolve(decoded.msg);
97
+ }
98
+ };
99
+ const onError = (err) => {
100
+ cleanup();
101
+ reject(err);
102
+ };
103
+ // Without this, a peer that FINs the connection mid-handshake (no
104
+ // explicit error event) would leave this promise pending forever.
105
+ const onClose = () => {
106
+ cleanup();
107
+ reject(new PowDBError("connection closed during handshake", "connect_failed"));
108
+ };
109
+ socket.on("data", onData);
110
+ socket.on("error", onError);
111
+ socket.on("close", onClose);
112
+ });
113
+ socket.write(encode({ type: "Connect", dbName, password }));
114
+ const reply = await handshake;
115
+ if (reply.type === "Error") {
116
+ socket.destroy();
117
+ throw new PowDBError(`connect failed: ${reply.message}`, "auth_failed");
118
+ }
119
+ if (reply.type !== "ConnectOk") {
120
+ socket.destroy();
121
+ throw new PowDBError(`expected ConnectOk, got ${reply.type}`, "protocol_error");
122
+ }
123
+ // Advisory: warn once per host:port if the server's major differs
124
+ // from the client's. Do not throw or close — this is best-effort.
125
+ const serverMajor = majorOf(reply.version);
126
+ const clientMajor = majorOf(CLIENT_VERSION);
127
+ if (serverMajor !== clientMajor) {
128
+ const key = `${host}:${port}`;
129
+ if (!versionWarnings.has(key)) {
130
+ versionWarnings.add(key);
131
+ console.warn(`[powdb] server version ${reply.version} major (${serverMajor}) ` +
132
+ `differs from client ${CLIENT_VERSION} major (${clientMajor}); ` +
133
+ `behaviour may be inconsistent.`);
134
+ }
135
+ }
136
+ return new Client(socket, reply.version);
137
+ }
138
+ /**
139
+ * Run a PowQL statement and return the typed result.
140
+ *
141
+ * When `opts.signal` is provided and fires, the returned promise rejects
142
+ * with the signal's `reason` (or an `AbortError`). The socket is NOT
143
+ * destroyed — the server will still eventually send its reply, which we
144
+ * silently discard so other in-flight queries keep working.
145
+ */
146
+ async query(query, opts) {
147
+ const start = Date.now();
148
+ try {
149
+ const reply = await this.send({ type: "Query", query }, opts);
150
+ let result;
151
+ switch (reply.type) {
152
+ case "ResultRows":
153
+ result = { kind: "rows", columns: reply.columns, rows: reply.rows };
154
+ break;
155
+ case "ResultScalar":
156
+ result = { kind: "scalar", value: reply.value };
157
+ break;
158
+ case "ResultOk":
159
+ result = { kind: "ok", affected: reply.affected };
160
+ break;
161
+ case "Error":
162
+ throw new PowDBError(`query failed: ${reply.message}`, "query_failed");
163
+ default:
164
+ throw new PowDBError(`unexpected reply: ${reply.type}`, "protocol_error");
165
+ }
166
+ this.emit("query", {
167
+ query,
168
+ durationMs: Date.now() - start,
169
+ ok: true,
170
+ kind: result.kind,
171
+ });
172
+ return result;
173
+ }
174
+ catch (err) {
175
+ this.emit("query", {
176
+ query,
177
+ durationMs: Date.now() - start,
178
+ ok: false,
179
+ error: err,
180
+ });
181
+ throw err;
182
+ }
183
+ }
184
+ /**
185
+ * Like {@link query}, but coerces string result columns to typed JS values
186
+ * using the caller-supplied schema. See `./typed.ts` for the coercion
187
+ * rules and supported column types.
188
+ *
189
+ * Returns a `TypedRow[]` — an array of objects keyed by column name.
190
+ * Throws `PowDBError(code="query_failed")` if the query is not a
191
+ * rows-returning query.
192
+ */
193
+ async queryTyped(query, schema, opts) {
194
+ const result = await this.query(query, opts);
195
+ if (result.kind !== "rows") {
196
+ throw new PowDBError(`queryTyped: expected rows result, got ${result.kind}`, "query_failed");
197
+ }
198
+ return coerceRows(result.columns, result.rows, schema);
199
+ }
200
+ /**
201
+ * Poll a query on an interval and invoke `onRows` for every successful
202
+ * run. Returns an unsubscribe function. Does NOT deduplicate results —
203
+ * `onRows` fires every interval, even if the rows are unchanged.
204
+ *
205
+ * Pragmatic first-cut live-data. If a query takes longer than
206
+ * `intervalMs`, the next tick waits for the in-flight one to finish
207
+ * (no pile-up). Errors fire `onError` (if provided) without stopping
208
+ * the watcher unless `stopOnError: true`.
209
+ */
210
+ watch(query, opts) {
211
+ if (!(opts.intervalMs > 0)) {
212
+ throw new PowDBError(`watch: intervalMs must be > 0, got ${opts.intervalMs}`, "protocol_error");
213
+ }
214
+ let stopped = false;
215
+ let inFlight = false;
216
+ const tick = async () => {
217
+ if (stopped || inFlight)
218
+ return;
219
+ inFlight = true;
220
+ try {
221
+ const r = await this.query(query);
222
+ if (!stopped)
223
+ opts.onRows(r);
224
+ }
225
+ catch (err) {
226
+ if (stopped)
227
+ return;
228
+ if (opts.onError)
229
+ opts.onError(err);
230
+ if (opts.stopOnError) {
231
+ stopped = true;
232
+ clearInterval(handle);
233
+ return;
234
+ }
235
+ }
236
+ finally {
237
+ inFlight = false;
238
+ }
239
+ };
240
+ // Run immediately so callers don't wait a full interval for the first
241
+ // emission, then every `intervalMs`.
242
+ void tick();
243
+ const handle = setInterval(tick, opts.intervalMs);
244
+ if (typeof handle.unref === "function")
245
+ handle.unref();
246
+ return {
247
+ stop: () => {
248
+ stopped = true;
249
+ clearInterval(handle);
250
+ },
251
+ };
252
+ }
253
+ /**
254
+ * Send Disconnect and tear down the socket. Waits for the remote FIN-ack
255
+ * (`socket.end` callback) but falls back to `destroy()` after a bounded
256
+ * timeout so an unresponsive peer cannot make `close()` hang forever.
257
+ */
258
+ async close() {
259
+ if (this.closed)
260
+ return;
261
+ try {
262
+ this.socket.write(encode({ type: "Disconnect" }));
263
+ }
264
+ catch {
265
+ // socket may already be half-closed; ignore
266
+ }
267
+ this.closed = true;
268
+ await new Promise((resolve) => {
269
+ let done = false;
270
+ const finish = () => {
271
+ if (done)
272
+ return;
273
+ done = true;
274
+ clearTimeout(timer);
275
+ resolve();
276
+ };
277
+ const timer = setTimeout(() => {
278
+ // Peer didn't ack FIN in time — force-close so we don't hang.
279
+ this.socket.destroy();
280
+ finish();
281
+ }, 5_000);
282
+ if (typeof timer.unref === "function")
283
+ timer.unref();
284
+ this.socket.end(finish);
285
+ });
286
+ }
287
+ // ───── internals ─────────────────────────────────────────────────────────
288
+ send(msg, opts) {
289
+ if (this.closed) {
290
+ return Promise.reject(this.closeError ?? new PowDBError("client is closed", "closed"));
291
+ }
292
+ const signal = opts?.signal;
293
+ // Pre-check: if already aborted, reject immediately and do not enqueue.
294
+ // This matches fetch() semantics for pre-aborted signals.
295
+ if (signal?.aborted) {
296
+ return Promise.reject(abortError(signal));
297
+ }
298
+ return new Promise((resolve, reject) => {
299
+ const entry = {
300
+ resolve: (m) => {
301
+ entry.settled = true;
302
+ resolve(m);
303
+ },
304
+ reject: (e) => {
305
+ entry.settled = true;
306
+ reject(e);
307
+ },
308
+ settled: false,
309
+ };
310
+ this.pending.push(entry);
311
+ let onAbort = null;
312
+ if (signal) {
313
+ onAbort = () => {
314
+ if (entry.settled)
315
+ return;
316
+ // Mark settled but DO NOT remove the entry from the queue — the
317
+ // server will still send a reply, and onData drops replies for
318
+ // already-settled entries at the head of the queue.
319
+ entry.settled = true;
320
+ reject(abortError(signal));
321
+ };
322
+ signal.addEventListener("abort", onAbort, { once: true });
323
+ // Strip the listener once the entry resolves/rejects naturally.
324
+ const origResolve = entry.resolve;
325
+ const origReject = entry.reject;
326
+ entry.resolve = (m) => {
327
+ if (onAbort)
328
+ signal.removeEventListener("abort", onAbort);
329
+ origResolve(m);
330
+ };
331
+ entry.reject = (e) => {
332
+ if (onAbort)
333
+ signal.removeEventListener("abort", onAbort);
334
+ origReject(e);
335
+ };
336
+ }
337
+ this.socket.write(encode(msg), (err) => {
338
+ if (err) {
339
+ if (entry.settled)
340
+ return;
341
+ // Writer error — the promise will also be rejected by onClose,
342
+ // but rejecting here gives a faster, more specific failure.
343
+ //
344
+ // Splicing an entry from the middle of `pending` is only safe
345
+ // because a write-callback error implies the bytes never reached
346
+ // the server — no reply will ever come for this slot, so FIFO
347
+ // alignment with subsequent entries is preserved.
348
+ const idx = this.pending.indexOf(entry);
349
+ if (idx !== -1)
350
+ this.pending.splice(idx, 1);
351
+ entry.reject(err);
352
+ }
353
+ });
354
+ });
355
+ }
356
+ onData(chunk) {
357
+ // Append to the chunk queue — O(1) — and lazily concat only when
358
+ // we actually need contiguous bytes to decode.
359
+ this.chunks.push(chunk);
360
+ this.totalLen += chunk.length;
361
+ while (this.totalLen > 0) {
362
+ // Fast path: if the first chunk already contains a full frame, we
363
+ // can decode without concatenating.
364
+ let view;
365
+ if (this.chunks.length === 1) {
366
+ view = this.chunks[0];
367
+ }
368
+ else {
369
+ // Peek at the header if we don't already have >=6 bytes up front.
370
+ // We need up to 6 bytes to read payloadLen, then enough to hold
371
+ // the full frame. Coalesce lazily.
372
+ if (this.chunks[0].length < 6 && this.totalLen >= 6) {
373
+ this.coalesce();
374
+ }
375
+ // If the first chunk still has a full frame, great. Otherwise
376
+ // coalesce the whole queue so tryDecode sees contiguous bytes.
377
+ const first = this.chunks[0];
378
+ if (first.length >= 6) {
379
+ const payloadLen = first.readUInt32LE(2);
380
+ if (first.length >= 6 + payloadLen) {
381
+ view = first;
382
+ }
383
+ else if (this.totalLen >= 6 + payloadLen) {
384
+ this.coalesce();
385
+ view = this.chunks[0];
386
+ }
387
+ else {
388
+ // Not enough bytes yet for the full frame — wait for more data.
389
+ break;
390
+ }
391
+ }
392
+ else {
393
+ // Still short of a header even after coalesce attempt above.
394
+ break;
395
+ }
396
+ }
397
+ let decoded;
398
+ try {
399
+ decoded = tryDecode(view);
400
+ }
401
+ catch (err) {
402
+ this.onClose(err);
403
+ return;
404
+ }
405
+ if (decoded === null)
406
+ break;
407
+ // Advance past the consumed bytes without copying the trailing data.
408
+ this.consume(decoded.consumed);
409
+ // Find the next non-settled pending entry and hand it the reply.
410
+ // Settled entries at the head were aborted by the caller but their
411
+ // reply is arriving now — drop it silently.
412
+ let entry = this.pending.shift();
413
+ while (entry && entry.settled) {
414
+ entry = this.pending.shift();
415
+ }
416
+ if (!entry) {
417
+ // Server sent an unsolicited frame (or we got extra after aborts
418
+ // with no replacement). Treat as protocol error.
419
+ this.onClose(new PowDBError("received unexpected frame from server", "protocol_error"));
420
+ return;
421
+ }
422
+ entry.resolve(decoded.msg);
423
+ }
424
+ }
425
+ /** Collapse the chunk queue into a single Buffer. */
426
+ coalesce() {
427
+ if (this.chunks.length <= 1)
428
+ return;
429
+ const merged = Buffer.concat(this.chunks, this.totalLen);
430
+ this.chunks.length = 0;
431
+ this.chunks.push(merged);
432
+ }
433
+ /** Drop the first `n` bytes off the chunk queue. */
434
+ consume(n) {
435
+ let remaining = n;
436
+ while (remaining > 0 && this.chunks.length > 0) {
437
+ const head = this.chunks[0];
438
+ if (head.length <= remaining) {
439
+ remaining -= head.length;
440
+ this.totalLen -= head.length;
441
+ this.chunks.shift();
442
+ }
443
+ else {
444
+ this.chunks[0] = head.subarray(remaining);
445
+ this.totalLen -= remaining;
446
+ remaining = 0;
447
+ }
448
+ }
449
+ }
450
+ onClose(err) {
451
+ const firstClose = !this.closed;
452
+ if (this.closed && err === null)
453
+ return;
454
+ this.closed = true;
455
+ this.closeError = err;
456
+ const error = err ?? new PowDBError("connection closed", "closed");
457
+ while (this.pending.length > 0) {
458
+ const entry = this.pending.shift();
459
+ if (!entry.settled) {
460
+ entry.reject(error);
461
+ }
462
+ }
463
+ if (firstClose) {
464
+ // Best-effort: surface the close event once. Late listeners on a
465
+ // closed client miss it, which matches Node socket semantics.
466
+ this.emit("close", { error: err });
467
+ }
468
+ }
469
+ }
470
+ function openSocket(host, port, timeoutMs, tlsOpt) {
471
+ return new Promise((resolve, reject) => {
472
+ let socket;
473
+ const timer = setTimeout(() => {
474
+ socket.destroy();
475
+ reject(new PowDBError(`connect timeout after ${timeoutMs}ms`, "timeout"));
476
+ }, timeoutMs);
477
+ const onConnect = () => {
478
+ clearTimeout(timer);
479
+ socket.setNoDelay(true);
480
+ // Enable TCP keepalive on the underlying OS socket so dead peers are
481
+ // detected even when the app is otherwise idle.
482
+ socket.setKeepAlive(true, 30_000);
483
+ resolve(socket);
484
+ };
485
+ const onError = (err) => {
486
+ clearTimeout(timer);
487
+ // Wrap raw socket errors in the PowDBError taxonomy — callers can
488
+ // branch on `.code === "connect_failed"` rather than string-matching.
489
+ reject(new PowDBError(`connect failed: ${err.message}`, "connect_failed", { cause: err }));
490
+ };
491
+ if (tlsOpt) {
492
+ // TLS path: `tls.connect` wraps an underlying net.Socket. `secureConnect`
493
+ // fires once the TLS handshake is complete — that is the right hook for
494
+ // "ready to send application data".
495
+ const tlsOptions = tlsOpt === true ? {} : tlsOpt;
496
+ const tlsSock = tls.connect(port, host, tlsOptions);
497
+ socket = tlsSock;
498
+ tlsSock.once("secureConnect", onConnect);
499
+ tlsSock.once("error", onError);
500
+ }
501
+ else {
502
+ socket = new net.Socket();
503
+ socket.once("connect", onConnect);
504
+ socket.once("error", onError);
505
+ socket.connect(port, host);
506
+ }
507
+ });
508
+ }
509
+ export { encode, tryDecode } from "./protocol.js";
510
+ export { MAX_PAYLOAD_SIZE, MAX_ROWS, MAX_COLUMNS, } from "./protocol.js";
511
+ export { escapeLiteral, escapeIdent, ident, powql, PowqlIdent, } from "./escape.js";
512
+ export { Pool } from "./pool.js";
513
+ export { PowDBError, isPowDBError } from "./errors.js";
514
+ export { coerceValue, coerceRow, coerceRows, } from "./typed.js";
515
+ //# sourceMappingURL=index.js.map