@zvndev/powdb-client 0.8.0 → 0.8.1

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/README.md CHANGED
@@ -322,11 +322,12 @@ try {
322
322
  }
323
323
  ```
324
324
 
325
- If you abort with a custom reason (`ctrl.abort(myError)`) and that reason is
326
- an `Error`, it is thrown as-is instead. Otherwise the client throws a
327
- `PowDBError` with `code === "aborted"`.
325
+ A plain `ctrl.abort()` throws a `PowDBError` with `code === "aborted"`. If you
326
+ abort with a custom `Error` reason (`ctrl.abort(myError)`), that error is thrown
327
+ as-is; any non-`Error` reason is wrapped in a `PowDBError` (`code === "aborted"`).
328
328
 
329
- The socket stays open — the server's reply is silently discarded so other in-flight queries keep working.
329
+ The socket stays open — the aborted query's reply is silently discarded when it
330
+ arrives, and every other in-flight query still receives its own result.
330
331
 
331
332
  ## API
332
333
 
@@ -361,6 +362,19 @@ Sends a PowQL query and returns a `Promise<QueryResult>`:
361
362
 
362
363
  Throws a `PowDBError` (see Structured errors above) on any failure.
363
364
 
365
+ ### `client.querySql(query, opts?)`
366
+
367
+ Runs a statement through the server's **SQL frontend** and returns a
368
+ `Promise<QueryResult>` — the same result shape as `query()`. The plain
369
+ `query()` method stays PowQL for wire compatibility; `querySql()` sends the
370
+ dedicated `QuerySql` wire message. Requires server ≥0.5.0 (the SQL frontend was
371
+ added in 0.5.0); see `docs/SQL.md` for the supported subset. `opts.signal?:
372
+ AbortSignal` cancels the query (see Cancellation above).
373
+
374
+ ```typescript
375
+ const result = await client.querySql("SELECT name, age FROM User WHERE age > 27");
376
+ ```
377
+
364
378
  ### `client.queryTyped(query, schema, opts?)`
365
379
 
366
380
  Like `query()`, but coerces each row's string values to JS types using the
package/dist/index.d.ts CHANGED
@@ -19,7 +19,7 @@ import { EventEmitter } from "node:events";
19
19
  import { type SyncRepairAction, type WireRetainedUnit, type WireSyncStatus } from "./protocol.js";
20
20
  import { type TypedRow, type TypedSchema } from "./typed.js";
21
21
  /** Client library version. Compared to the server's reported version. */
22
- export declare const CLIENT_VERSION = "0.8.0";
22
+ export declare const CLIENT_VERSION = "0.8.1";
23
23
  export type QueryResult = {
24
24
  kind: "rows";
25
25
  columns: string[];
package/dist/index.js CHANGED
@@ -21,7 +21,7 @@ import { encode, tryDecode, MAX_SYNC_PULL_BYTES, MAX_SYNC_PULL_UNITS, } from "./
21
21
  import { PowDBError } from "./errors.js";
22
22
  import { coerceRows, } from "./typed.js";
23
23
  /** Client library version. Compared to the server's reported version. */
24
- export const CLIENT_VERSION = "0.8.0";
24
+ export const CLIENT_VERSION = "0.8.1";
25
25
  function socketChunkToBuffer(chunk) {
26
26
  return typeof chunk === "string" ? Buffer.from(chunk) : chunk;
27
27
  }
@@ -98,16 +98,19 @@ function majorOf(version) {
98
98
  return dot === -1 ? version : version.slice(0, dot);
99
99
  }
100
100
  /**
101
- * Build an AbortError. Prefers the caller-supplied `signal.reason` if it is
102
- * itself an `Error` (matches DOM semantics); otherwise wraps it in a
103
- * `PowDBError` with code `"aborted"` so catch-blocks can branch uniformly.
101
+ * Build an AbortError. A caller-supplied custom `Error` reason
102
+ * (`ctrl.abort(myError)`) passes through as-is to match DOM semantics. The
103
+ * default abort reason — Node's `DOMException` named `"AbortError"` and any
104
+ * non-Error reason are wrapped in a `PowDBError` with code `"aborted"` so the
105
+ * common `ctrl.abort()` case branches uniformly on `err.code === "aborted"`.
104
106
  */
105
107
  function abortError(signal) {
106
108
  if (signal && signal.reason !== undefined) {
107
109
  const r = signal.reason;
108
- if (r instanceof Error)
110
+ const isDefaultAbort = r instanceof DOMException && r.name === "AbortError";
111
+ if (r instanceof Error && !isDefaultAbort)
109
112
  return r;
110
- return new PowDBError(String(r), "aborted");
113
+ return new PowDBError(isDefaultAbort ? "query was aborted" : String(r), "aborted");
111
114
  }
112
115
  return new PowDBError("query was aborted", "aborted");
113
116
  }
@@ -527,8 +530,17 @@ export class Client extends EventEmitter {
527
530
  * timeout so an unresponsive peer cannot make `close()` hang forever.
528
531
  */
529
532
  async close() {
530
- if (this.closed)
533
+ if (this.closed) {
534
+ // Already torn down (e.g. an error in onData/onClose closed the client
535
+ // without destroying the socket). Ensure the socket is released so a
536
+ // post-teardown close() resolves instead of keeping the event loop
537
+ // alive. `writableEnded` distinguishes that case from a concurrent
538
+ // close() mid-graceful-shutdown, which must not be force-destroyed.
539
+ if (!this.socket.destroyed && !this.socket.writableEnded) {
540
+ this.socket.destroy();
541
+ }
531
542
  return;
543
+ }
532
544
  try {
533
545
  this.socket.write(encode({ type: "Disconnect" }));
534
546
  }
@@ -682,19 +694,18 @@ export class Client extends EventEmitter {
682
694
  this.socket.write(encode({ type: "Pong" }));
683
695
  continue;
684
696
  }
685
- // Find the next non-settled pending entry and hand it the reply.
686
- // Settled entries at the head were aborted by the caller but their
687
- // reply is arriving now drop it silently.
688
- let entry = this.pending.shift();
689
- while (entry && entry.settled) {
690
- entry = this.pending.shift();
691
- }
692
- if (!entry) {
693
- // Server sent an unsolicited frame (or we got extra after aborts
694
- // with no replacement). Treat as protocol error.
697
+ // Replies are strictly FIFO: this frame belongs to exactly one pending
698
+ // entry the head. Consume one entry per frame. If it was aborted
699
+ // (settled), its reply is arriving now; drop the frame and keep decoding
700
+ // rather than delivering it to a later, live query.
701
+ const entry = this.pending.shift();
702
+ if (entry === undefined) {
703
+ // No pending entry at all — the server sent an unsolicited frame.
695
704
  this.onClose(new PowDBError("received unexpected frame from server", "protocol_error"));
696
705
  return;
697
706
  }
707
+ if (entry.settled)
708
+ continue;
698
709
  entry.resolve(decoded.msg);
699
710
  }
700
711
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zvndev/powdb-client",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "TypeScript client for PowDB (PowQL wire protocol)",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.29.3",
@@ -61,7 +61,7 @@
61
61
  "prepublishOnly": "npm run build"
62
62
  },
63
63
  "devDependencies": {
64
- "@types/node": "^25",
64
+ "@types/node": "^26",
65
65
  "tsx": "^4",
66
66
  "typescript": "^6.0"
67
67
  }