@zvndev/powdb-client 0.3.2 → 0.3.5
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/CHANGELOG.md +48 -0
- package/LICENSE +21 -0
- package/README.md +14 -6
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/protocol.d.ts +4 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +35 -7
- package/dist/protocol.js.map +1 -1
- package/package.json +28 -4
- package/src/errors.ts +0 -54
- package/src/escape.ts +0 -141
- package/src/index.ts +0 -658
- package/src/pool.ts +0 -375
- package/src/protocol.ts +0 -234
- package/src/typed.ts +0 -148
package/src/pool.ts
DELETED
|
@@ -1,375 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A tiny connection pool for the PowDB TypeScript client.
|
|
3
|
-
*
|
|
4
|
-
* const pool = new Pool({ host, port, max: 8 });
|
|
5
|
-
*
|
|
6
|
-
* // manual acquire/release
|
|
7
|
-
* const c = await pool.acquire();
|
|
8
|
-
* try {
|
|
9
|
-
* await c.query("...");
|
|
10
|
-
* pool.release(c);
|
|
11
|
-
* } catch (err) {
|
|
12
|
-
* // a broken client should NOT be returned to the pool
|
|
13
|
-
* pool.destroy(c);
|
|
14
|
-
* throw err;
|
|
15
|
-
* }
|
|
16
|
-
*
|
|
17
|
-
* // ...or, preferred, use the scoped helper:
|
|
18
|
-
* const users = await pool.withClient((c) => c.query("Users"));
|
|
19
|
-
*
|
|
20
|
-
* await pool.close();
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import { Client, type ClientOptions } from "./index.js";
|
|
24
|
-
import { PowDBError, isPowDBError } from "./errors.js";
|
|
25
|
-
|
|
26
|
-
export interface PoolOptions extends ClientOptions {
|
|
27
|
-
/** Maximum concurrent clients. Default 10. */
|
|
28
|
-
max?: number;
|
|
29
|
-
/** Acquire timeout in ms. Default 30_000. */
|
|
30
|
-
acquireTimeoutMs?: number;
|
|
31
|
-
/**
|
|
32
|
-
* How many times to retry `Client.connect` on transient errors
|
|
33
|
-
* (connect_failed / timeout). Auth failures and other non-transient
|
|
34
|
-
* errors are never retried. Default 3. Set to 0 to disable.
|
|
35
|
-
*/
|
|
36
|
-
connectRetries?: number;
|
|
37
|
-
/**
|
|
38
|
-
* Initial backoff (ms) between connect retries. Each subsequent retry
|
|
39
|
-
* doubles the delay up to `connectMaxBackoffMs`. Default 100.
|
|
40
|
-
*/
|
|
41
|
-
connectBackoffMs?: number;
|
|
42
|
-
/** Maximum backoff (ms) between connect retries. Default 2_000. */
|
|
43
|
-
connectMaxBackoffMs?: number;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Is this error worth retrying? Only the transient connect-phase errors
|
|
48
|
-
* are retryable. Auth failures, size violations, aborts, and protocol
|
|
49
|
-
* errors are not — retrying would just fail the same way.
|
|
50
|
-
*/
|
|
51
|
-
function isRetryable(err: unknown): boolean {
|
|
52
|
-
if (!isPowDBError(err)) {
|
|
53
|
-
// Raw socket errors surface as generic Error before we wrap them in
|
|
54
|
-
// connect_failed. In practice connect() wraps them, but be defensive.
|
|
55
|
-
return err instanceof Error;
|
|
56
|
-
}
|
|
57
|
-
return err.code === "connect_failed" || err.code === "timeout";
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function sleep(ms: number): Promise<void> {
|
|
61
|
-
return new Promise((resolve) => {
|
|
62
|
-
const t = setTimeout(resolve, ms);
|
|
63
|
-
if (typeof t.unref === "function") t.unref();
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Connect to the server with bounded retry + exponential backoff. Throws
|
|
69
|
-
* the last error if every attempt fails, or the first error unchanged if
|
|
70
|
-
* it was not retryable.
|
|
71
|
-
*/
|
|
72
|
-
async function connectWithRetry(
|
|
73
|
-
opts: ClientOptions,
|
|
74
|
-
retries: number,
|
|
75
|
-
initialBackoffMs: number,
|
|
76
|
-
maxBackoffMs: number,
|
|
77
|
-
): Promise<Client> {
|
|
78
|
-
let lastErr: unknown;
|
|
79
|
-
let delay = initialBackoffMs;
|
|
80
|
-
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
81
|
-
try {
|
|
82
|
-
return await Client.connect(opts);
|
|
83
|
-
} catch (err) {
|
|
84
|
-
lastErr = err;
|
|
85
|
-
if (!isRetryable(err) || attempt === retries) break;
|
|
86
|
-
await sleep(delay);
|
|
87
|
-
delay = Math.min(delay * 2, maxBackoffMs);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
throw lastErr;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
type Waiter = {
|
|
94
|
-
resolve: (c: Client) => void;
|
|
95
|
-
reject: (err: Error) => void;
|
|
96
|
-
timer: NodeJS.Timeout | null;
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Lazy, FIFO connection pool.
|
|
101
|
-
*
|
|
102
|
-
* Semantics:
|
|
103
|
-
* - `acquire` returns an idle client if one exists; otherwise creates a new
|
|
104
|
-
* one up to `max`; otherwise waits until `release`/`destroy` frees a slot
|
|
105
|
-
* (subject to `acquireTimeoutMs`).
|
|
106
|
-
* - `release(c)` puts `c` back in the idle queue unconditionally. **If the
|
|
107
|
-
* caller observed a socket/connection error on `c`, they MUST call
|
|
108
|
-
* `destroy(c)` instead** — the pool cannot safely detect dead sockets.
|
|
109
|
-
* - `destroy(c)` closes `c` and decrements the live count, freeing a slot
|
|
110
|
-
* for a new client to be created.
|
|
111
|
-
* - `withClient(fn)` acquires, runs `fn`, and always destroys on error or
|
|
112
|
-
* releases on success. This is the safe default for most call sites.
|
|
113
|
-
* - `close()` rejects any pending waiters and awaits `close()` on every
|
|
114
|
-
* idle client. Subsequent calls to `acquire` reject immediately.
|
|
115
|
-
*/
|
|
116
|
-
export class Pool {
|
|
117
|
-
private readonly opts: ClientOptions;
|
|
118
|
-
private readonly max: number;
|
|
119
|
-
private readonly acquireTimeoutMs: number;
|
|
120
|
-
private readonly connectRetries: number;
|
|
121
|
-
private readonly connectBackoffMs: number;
|
|
122
|
-
private readonly connectMaxBackoffMs: number;
|
|
123
|
-
|
|
124
|
-
private readonly idleClients: Client[] = [];
|
|
125
|
-
private readonly waiters: Waiter[] = [];
|
|
126
|
-
/** Clients owned by this pool (idle + checked-out). Used to reject
|
|
127
|
-
* foreign/double-destroyed clients from corrupting the slot accounting. */
|
|
128
|
-
private readonly owned: Set<Client> = new Set();
|
|
129
|
-
private live = 0;
|
|
130
|
-
private _closed = false;
|
|
131
|
-
|
|
132
|
-
constructor(opts: PoolOptions) {
|
|
133
|
-
const {
|
|
134
|
-
max = 10,
|
|
135
|
-
acquireTimeoutMs = 30_000,
|
|
136
|
-
connectRetries = 3,
|
|
137
|
-
connectBackoffMs = 100,
|
|
138
|
-
connectMaxBackoffMs = 2_000,
|
|
139
|
-
...clientOpts
|
|
140
|
-
} = opts;
|
|
141
|
-
if (!Number.isFinite(max) || max < 1) {
|
|
142
|
-
throw new TypeError(`Pool: max must be a positive integer, got ${max}`);
|
|
143
|
-
}
|
|
144
|
-
if (!Number.isFinite(acquireTimeoutMs) || acquireTimeoutMs < 0) {
|
|
145
|
-
throw new TypeError(
|
|
146
|
-
`Pool: acquireTimeoutMs must be a non-negative number, got ${acquireTimeoutMs}`
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
if (!Number.isFinite(connectRetries) || connectRetries < 0) {
|
|
150
|
-
throw new TypeError(
|
|
151
|
-
`Pool: connectRetries must be a non-negative integer, got ${connectRetries}`
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
this.opts = clientOpts;
|
|
155
|
-
this.max = max;
|
|
156
|
-
this.acquireTimeoutMs = acquireTimeoutMs;
|
|
157
|
-
this.connectRetries = connectRetries;
|
|
158
|
-
this.connectBackoffMs = connectBackoffMs;
|
|
159
|
-
this.connectMaxBackoffMs = connectMaxBackoffMs;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Wraps Client.connect with the pool's configured retry policy. Used by
|
|
164
|
-
* `acquire` and `drainWaiters`.
|
|
165
|
-
*/
|
|
166
|
-
private connect(): Promise<Client> {
|
|
167
|
-
return connectWithRetry(
|
|
168
|
-
this.opts,
|
|
169
|
-
this.connectRetries,
|
|
170
|
-
this.connectBackoffMs,
|
|
171
|
-
this.connectMaxBackoffMs,
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/** Live client count (idle + checked out). */
|
|
176
|
-
get size(): number {
|
|
177
|
-
return this.live;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/** Clients currently sitting in the idle queue. */
|
|
181
|
-
get idle(): number {
|
|
182
|
-
return this.idleClients.length;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/** `true` once {@link close} has been called. */
|
|
186
|
-
get closed(): boolean {
|
|
187
|
-
return this._closed;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Acquire a client from the pool.
|
|
192
|
-
*
|
|
193
|
-
* Resolves with an idle client if one is available, creates a new one if
|
|
194
|
-
* `live < max`, or waits in a FIFO queue. Rejects if `acquireTimeoutMs`
|
|
195
|
-
* elapses, or if the pool is closed.
|
|
196
|
-
*/
|
|
197
|
-
async acquire(): Promise<Client> {
|
|
198
|
-
if (this._closed) {
|
|
199
|
-
throw new Error("pool closed");
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const existing = this.idleClients.shift();
|
|
203
|
-
if (existing !== undefined) {
|
|
204
|
-
return existing;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (this.live < this.max) {
|
|
208
|
-
this.live++;
|
|
209
|
-
try {
|
|
210
|
-
const c = await this.connect();
|
|
211
|
-
this.owned.add(c);
|
|
212
|
-
return c;
|
|
213
|
-
} catch (err) {
|
|
214
|
-
// Creation failed — we never had a live client, so give the slot
|
|
215
|
-
// back and propagate.
|
|
216
|
-
this.live--;
|
|
217
|
-
// A newly-freed slot may let a pending waiter try again... but only
|
|
218
|
-
// via a future release/destroy. To be fair to waiters, schedule a
|
|
219
|
-
// drain here.
|
|
220
|
-
this.drainWaiters();
|
|
221
|
-
throw err;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
return new Promise<Client>((resolve, reject) => {
|
|
226
|
-
const waiter: Waiter = {
|
|
227
|
-
resolve,
|
|
228
|
-
reject,
|
|
229
|
-
timer: null,
|
|
230
|
-
};
|
|
231
|
-
if (this.acquireTimeoutMs > 0) {
|
|
232
|
-
waiter.timer = setTimeout(() => {
|
|
233
|
-
const idx = this.waiters.indexOf(waiter);
|
|
234
|
-
if (idx !== -1) this.waiters.splice(idx, 1);
|
|
235
|
-
reject(new Error("pool acquire timeout"));
|
|
236
|
-
}, this.acquireTimeoutMs);
|
|
237
|
-
// Don't keep the event loop alive just for the timeout.
|
|
238
|
-
if (typeof waiter.timer.unref === "function") waiter.timer.unref();
|
|
239
|
-
}
|
|
240
|
-
this.waiters.push(waiter);
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Return `c` to the idle queue so subsequent `acquire` calls can hand it
|
|
246
|
-
* out. If the pool is closed, `c` is closed instead.
|
|
247
|
-
*
|
|
248
|
-
* **Do not call this if `c` has experienced a connection error** — use
|
|
249
|
-
* {@link destroy} for that case.
|
|
250
|
-
*/
|
|
251
|
-
release(c: Client): void {
|
|
252
|
-
if (!this.owned.has(c)) {
|
|
253
|
-
// Foreign client or already-destroyed — refuse to touch accounting.
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
if (this._closed) {
|
|
258
|
-
// Pool is gone — just close the client, fire-and-forget.
|
|
259
|
-
this.owned.delete(c);
|
|
260
|
-
if (this.live > 0) this.live--;
|
|
261
|
-
void c.close().catch(() => {});
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
const waiter = this.waiters.shift();
|
|
266
|
-
if (waiter !== undefined) {
|
|
267
|
-
if (waiter.timer !== null) clearTimeout(waiter.timer);
|
|
268
|
-
waiter.resolve(c);
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
this.idleClients.push(c);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Close `c` and free its slot so a new client can be created on the next
|
|
277
|
-
* `acquire`. Use this when `c` has hit a socket error or is otherwise
|
|
278
|
-
* known-bad.
|
|
279
|
-
*/
|
|
280
|
-
destroy(c: Client): void {
|
|
281
|
-
if (!this.owned.has(c)) {
|
|
282
|
-
// Foreign client or already destroyed — don't touch slot accounting.
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
this.owned.delete(c);
|
|
286
|
-
// Decrement first so any waiter we hand off to can create a fresh one.
|
|
287
|
-
if (this.live > 0) this.live--;
|
|
288
|
-
void c.close().catch(() => {});
|
|
289
|
-
|
|
290
|
-
if (this._closed) return;
|
|
291
|
-
|
|
292
|
-
// Drain one waiter if possible — the freed slot lets them create a new
|
|
293
|
-
// connection.
|
|
294
|
-
this.drainWaiters();
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Acquire a client, run `fn`, and either release (on success) or destroy
|
|
299
|
-
* (on error). This is the recommended call site — it makes it impossible
|
|
300
|
-
* to leak a client on an unhandled exception.
|
|
301
|
-
*/
|
|
302
|
-
async withClient<T>(fn: (c: Client) => Promise<T>): Promise<T> {
|
|
303
|
-
const c = await this.acquire();
|
|
304
|
-
try {
|
|
305
|
-
const out = await fn(c);
|
|
306
|
-
this.release(c);
|
|
307
|
-
return out;
|
|
308
|
-
} catch (err) {
|
|
309
|
-
this.destroy(c);
|
|
310
|
-
throw err;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Close the pool. Rejects all pending waiters, awaits `close()` on every
|
|
316
|
-
* idle client, and marks the pool closed. Subsequent `acquire` calls
|
|
317
|
-
* reject immediately.
|
|
318
|
-
*
|
|
319
|
-
* Checked-out clients are NOT tracked — callers that still hold one when
|
|
320
|
-
* `close()` is called are responsible for closing it themselves.
|
|
321
|
-
*/
|
|
322
|
-
async close(): Promise<void> {
|
|
323
|
-
if (this._closed) return;
|
|
324
|
-
this._closed = true;
|
|
325
|
-
|
|
326
|
-
// Reject every pending waiter.
|
|
327
|
-
while (this.waiters.length > 0) {
|
|
328
|
-
const w = this.waiters.shift()!;
|
|
329
|
-
if (w.timer !== null) clearTimeout(w.timer);
|
|
330
|
-
w.reject(new Error("pool closed"));
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Close every idle client.
|
|
334
|
-
const idle = this.idleClients.splice(0, this.idleClients.length);
|
|
335
|
-
for (const c of idle) this.owned.delete(c);
|
|
336
|
-
this.live -= idle.length;
|
|
337
|
-
await Promise.all(idle.map((c) => c.close().catch(() => {})));
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// ──────────────────────────────────────────────────────────
|
|
341
|
-
// internals
|
|
342
|
-
// ──────────────────────────────────────────────────────────
|
|
343
|
-
|
|
344
|
-
private drainWaiters(): void {
|
|
345
|
-
// If we have headroom AND a waiter, try to create a client for them.
|
|
346
|
-
// (Handles the "acquire failed → slot freed" path cleanly without
|
|
347
|
-
// forcing the caller to re-poll.)
|
|
348
|
-
if (this._closed) return;
|
|
349
|
-
while (this.waiters.length > 0 && this.live < this.max) {
|
|
350
|
-
const waiter = this.waiters.shift()!;
|
|
351
|
-
if (waiter.timer !== null) clearTimeout(waiter.timer);
|
|
352
|
-
this.live++;
|
|
353
|
-
this.connect().then(
|
|
354
|
-
(c) => {
|
|
355
|
-
// Pool may have been closed while the connect was in flight.
|
|
356
|
-
// If so, close the freshly-opened client instead of handing it
|
|
357
|
-
// to a waiter that's already been rejected.
|
|
358
|
-
if (this._closed) {
|
|
359
|
-
if (this.live > 0) this.live--;
|
|
360
|
-
void c.close().catch(() => {});
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
363
|
-
this.owned.add(c);
|
|
364
|
-
waiter.resolve(c);
|
|
365
|
-
},
|
|
366
|
-
(err) => {
|
|
367
|
-
this.live--;
|
|
368
|
-
waiter.reject(err as Error);
|
|
369
|
-
// Keep trying for the next waiter, if any.
|
|
370
|
-
this.drainWaiters();
|
|
371
|
-
}
|
|
372
|
-
);
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
}
|
package/src/protocol.ts
DELETED
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PowDB wire protocol.
|
|
3
|
-
*
|
|
4
|
-
* Frame format: [type(1)][flags(1)][len(4 LE)][payload]
|
|
5
|
-
* Strings are encoded as [len(4 LE)][utf-8 bytes].
|
|
6
|
-
*
|
|
7
|
-
* This mirrors crates/server/src/protocol.rs — keep them in sync.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
export const MSG_CONNECT = 0x01;
|
|
11
|
-
export const MSG_CONNECT_OK = 0x02;
|
|
12
|
-
export const MSG_QUERY = 0x03;
|
|
13
|
-
export const MSG_RESULT_ROWS = 0x07;
|
|
14
|
-
export const MSG_RESULT_SCALAR = 0x08;
|
|
15
|
-
export const MSG_RESULT_OK = 0x09;
|
|
16
|
-
export const MSG_ERROR = 0x0a;
|
|
17
|
-
export const MSG_DISCONNECT = 0x10;
|
|
18
|
-
export const MSG_PING = 0x11;
|
|
19
|
-
export const MSG_PONG = 0x12;
|
|
20
|
-
|
|
21
|
-
// ───── Size limits (mirror crates/server/src/protocol.rs) ──────────────────
|
|
22
|
-
|
|
23
|
-
/** Maximum payload size accepted from the wire (64 MB). */
|
|
24
|
-
export const MAX_PAYLOAD_SIZE = 64 * 1024 * 1024;
|
|
25
|
-
|
|
26
|
-
/** Maximum number of rows allowed in a single result message. */
|
|
27
|
-
export const MAX_ROWS = 10_000_000;
|
|
28
|
-
|
|
29
|
-
/** Maximum number of columns allowed in a result set. */
|
|
30
|
-
export const MAX_COLUMNS = 4096;
|
|
31
|
-
|
|
32
|
-
export type Message =
|
|
33
|
-
| { type: "Connect"; dbName: string; password: string | null }
|
|
34
|
-
| { type: "ConnectOk"; version: string }
|
|
35
|
-
| { type: "Query"; query: string }
|
|
36
|
-
| { type: "ResultRows"; columns: string[]; rows: string[][] }
|
|
37
|
-
| { type: "ResultScalar"; value: string }
|
|
38
|
-
| { type: "ResultOk"; affected: bigint }
|
|
39
|
-
| { type: "Error"; message: string }
|
|
40
|
-
| { type: "Disconnect" }
|
|
41
|
-
| { type: "Ping" }
|
|
42
|
-
| { type: "Pong" };
|
|
43
|
-
|
|
44
|
-
// ───── Encoding ────────────────────────────────────────────────────────────
|
|
45
|
-
|
|
46
|
-
export function encode(msg: Message): Buffer {
|
|
47
|
-
let msgType: number;
|
|
48
|
-
let payload: Buffer;
|
|
49
|
-
|
|
50
|
-
switch (msg.type) {
|
|
51
|
-
case "Connect": {
|
|
52
|
-
const dbBuf = encodeString(msg.dbName);
|
|
53
|
-
const pwBuf =
|
|
54
|
-
msg.password === null ? u32LE(0) : encodeString(msg.password);
|
|
55
|
-
payload = Buffer.concat([dbBuf, pwBuf]);
|
|
56
|
-
msgType = MSG_CONNECT;
|
|
57
|
-
break;
|
|
58
|
-
}
|
|
59
|
-
case "ConnectOk":
|
|
60
|
-
payload = encodeString(msg.version);
|
|
61
|
-
msgType = MSG_CONNECT_OK;
|
|
62
|
-
break;
|
|
63
|
-
case "Query":
|
|
64
|
-
payload = encodeString(msg.query);
|
|
65
|
-
msgType = MSG_QUERY;
|
|
66
|
-
break;
|
|
67
|
-
case "ResultRows": {
|
|
68
|
-
const parts: Buffer[] = [];
|
|
69
|
-
const colCount = Buffer.alloc(2);
|
|
70
|
-
colCount.writeUInt16LE(msg.columns.length, 0);
|
|
71
|
-
parts.push(colCount);
|
|
72
|
-
for (const col of msg.columns) parts.push(encodeString(col));
|
|
73
|
-
parts.push(u32LE(msg.rows.length));
|
|
74
|
-
for (const row of msg.rows) {
|
|
75
|
-
for (const val of row) parts.push(encodeString(val));
|
|
76
|
-
}
|
|
77
|
-
payload = Buffer.concat(parts);
|
|
78
|
-
msgType = MSG_RESULT_ROWS;
|
|
79
|
-
break;
|
|
80
|
-
}
|
|
81
|
-
case "ResultScalar":
|
|
82
|
-
payload = encodeString(msg.value);
|
|
83
|
-
msgType = MSG_RESULT_SCALAR;
|
|
84
|
-
break;
|
|
85
|
-
case "ResultOk": {
|
|
86
|
-
payload = Buffer.alloc(8);
|
|
87
|
-
payload.writeBigUInt64LE(msg.affected, 0);
|
|
88
|
-
msgType = MSG_RESULT_OK;
|
|
89
|
-
break;
|
|
90
|
-
}
|
|
91
|
-
case "Error":
|
|
92
|
-
payload = encodeString(msg.message);
|
|
93
|
-
msgType = MSG_ERROR;
|
|
94
|
-
break;
|
|
95
|
-
case "Disconnect":
|
|
96
|
-
payload = Buffer.alloc(0);
|
|
97
|
-
msgType = MSG_DISCONNECT;
|
|
98
|
-
break;
|
|
99
|
-
case "Ping":
|
|
100
|
-
payload = Buffer.alloc(0);
|
|
101
|
-
msgType = MSG_PING;
|
|
102
|
-
break;
|
|
103
|
-
case "Pong":
|
|
104
|
-
payload = Buffer.alloc(0);
|
|
105
|
-
msgType = MSG_PONG;
|
|
106
|
-
break;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const frame = Buffer.alloc(6 + payload.length);
|
|
110
|
-
frame.writeUInt8(msgType, 0);
|
|
111
|
-
frame.writeUInt8(0, 1); // flags
|
|
112
|
-
frame.writeUInt32LE(payload.length, 2);
|
|
113
|
-
payload.copy(frame, 6);
|
|
114
|
-
return frame;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// ───── Decoding ────────────────────────────────────────────────────────────
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Attempts to parse a single frame from the start of `buf`. Returns the parsed
|
|
121
|
-
* message and the number of bytes consumed, or `null` if the buffer does not
|
|
122
|
-
* yet contain a complete frame.
|
|
123
|
-
*/
|
|
124
|
-
export function tryDecode(
|
|
125
|
-
buf: Buffer,
|
|
126
|
-
): { msg: Message; consumed: number } | null {
|
|
127
|
-
if (buf.length < 6) return null;
|
|
128
|
-
const msgType = buf.readUInt8(0);
|
|
129
|
-
// flags byte at offset 1 is currently unused
|
|
130
|
-
const payloadLen = buf.readUInt32LE(2);
|
|
131
|
-
if (payloadLen > MAX_PAYLOAD_SIZE) {
|
|
132
|
-
throw new Error(
|
|
133
|
-
`payload too large: ${payloadLen} bytes (max ${MAX_PAYLOAD_SIZE})`,
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
if (buf.length < 6 + payloadLen) return null;
|
|
137
|
-
const payload = buf.subarray(6, 6 + payloadLen);
|
|
138
|
-
const msg = decodePayload(msgType, payload);
|
|
139
|
-
return { msg, consumed: 6 + payloadLen };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function decodePayload(msgType: number, payload: Buffer): Message {
|
|
143
|
-
const cursor = { pos: 0 };
|
|
144
|
-
switch (msgType) {
|
|
145
|
-
case MSG_CONNECT: {
|
|
146
|
-
const dbName = decodeString(payload, cursor);
|
|
147
|
-
let password: string | null = null;
|
|
148
|
-
if (cursor.pos < payload.length) {
|
|
149
|
-
const p = decodeString(payload, cursor);
|
|
150
|
-
password = p.length === 0 ? null : p;
|
|
151
|
-
}
|
|
152
|
-
return { type: "Connect", dbName, password };
|
|
153
|
-
}
|
|
154
|
-
case MSG_CONNECT_OK:
|
|
155
|
-
return { type: "ConnectOk", version: decodeString(payload, cursor) };
|
|
156
|
-
case MSG_QUERY:
|
|
157
|
-
return { type: "Query", query: decodeString(payload, cursor) };
|
|
158
|
-
case MSG_RESULT_ROWS: {
|
|
159
|
-
const colCount = payload.readUInt16LE(cursor.pos);
|
|
160
|
-
cursor.pos += 2;
|
|
161
|
-
if (colCount > MAX_COLUMNS) {
|
|
162
|
-
throw new Error(
|
|
163
|
-
`too many columns: ${colCount} (max ${MAX_COLUMNS})`,
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
const columns: string[] = [];
|
|
167
|
-
for (let i = 0; i < colCount; i++) {
|
|
168
|
-
columns.push(decodeString(payload, cursor));
|
|
169
|
-
}
|
|
170
|
-
const rowCount = payload.readUInt32LE(cursor.pos);
|
|
171
|
-
cursor.pos += 4;
|
|
172
|
-
if (rowCount > MAX_ROWS) {
|
|
173
|
-
throw new Error(
|
|
174
|
-
`too many rows: ${rowCount} (max ${MAX_ROWS})`,
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
const rows: string[][] = [];
|
|
178
|
-
for (let r = 0; r < rowCount; r++) {
|
|
179
|
-
const row: string[] = [];
|
|
180
|
-
for (let c = 0; c < colCount; c++) {
|
|
181
|
-
row.push(decodeString(payload, cursor));
|
|
182
|
-
}
|
|
183
|
-
rows.push(row);
|
|
184
|
-
}
|
|
185
|
-
return { type: "ResultRows", columns, rows };
|
|
186
|
-
}
|
|
187
|
-
case MSG_RESULT_SCALAR:
|
|
188
|
-
return { type: "ResultScalar", value: decodeString(payload, cursor) };
|
|
189
|
-
case MSG_RESULT_OK: {
|
|
190
|
-
const affected = payload.readBigUInt64LE(0);
|
|
191
|
-
return { type: "ResultOk", affected };
|
|
192
|
-
}
|
|
193
|
-
case MSG_ERROR:
|
|
194
|
-
return { type: "Error", message: decodeString(payload, cursor) };
|
|
195
|
-
case MSG_DISCONNECT:
|
|
196
|
-
return { type: "Disconnect" };
|
|
197
|
-
case MSG_PING:
|
|
198
|
-
return { type: "Ping" };
|
|
199
|
-
case MSG_PONG:
|
|
200
|
-
return { type: "Pong" };
|
|
201
|
-
default:
|
|
202
|
-
throw new Error(`unknown message type: 0x${msgType.toString(16)}`);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// ───── String helpers ──────────────────────────────────────────────────────
|
|
207
|
-
|
|
208
|
-
function encodeString(s: string): Buffer {
|
|
209
|
-
const bytes = Buffer.from(s, "utf8");
|
|
210
|
-
const out = Buffer.alloc(4 + bytes.length);
|
|
211
|
-
out.writeUInt32LE(bytes.length, 0);
|
|
212
|
-
bytes.copy(out, 4);
|
|
213
|
-
return out;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function decodeString(buf: Buffer, cursor: { pos: number }): string {
|
|
217
|
-
if (cursor.pos + 4 > buf.length) {
|
|
218
|
-
throw new Error("truncated string length");
|
|
219
|
-
}
|
|
220
|
-
const len = buf.readUInt32LE(cursor.pos);
|
|
221
|
-
cursor.pos += 4;
|
|
222
|
-
if (cursor.pos + len > buf.length) {
|
|
223
|
-
throw new Error("truncated string data");
|
|
224
|
-
}
|
|
225
|
-
const s = buf.toString("utf8", cursor.pos, cursor.pos + len);
|
|
226
|
-
cursor.pos += len;
|
|
227
|
-
return s;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function u32LE(n: number): Buffer {
|
|
231
|
-
const b = Buffer.alloc(4);
|
|
232
|
-
b.writeUInt32LE(n, 0);
|
|
233
|
-
return b;
|
|
234
|
-
}
|