@bzync/nextsql 0.0.1 → 0.0.2

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 (3) hide show
  1. package/nextsql.d.ts +36 -6
  2. package/nextsql.js +211 -0
  3. package/package.json +1 -1
package/nextsql.d.ts CHANGED
@@ -1,10 +1,9 @@
1
- // Public TypeScript surface for the official NextSQL Node.js driver.
1
+ // Shared TypeScript surface for the official Node.js and Bun NextSQL drivers.
2
+ // Keys and passwords are never accepted in a URL.
2
3
  //
3
- // Self-contained COPY of drivers/js/types.d.ts. It must NOT import from ../js:
4
- // the package tarball contains only the files in package.json "files", so a
5
- // parent-directory import resolves to nothing for an installed consumer. The
6
- // parity test in the driver test suite fails if this drifts from the canonical
7
- // shared file.
4
+ // drivers/node/nextsql.d.ts and drivers/bun/nextsql.d.ts are published copies
5
+ // of this file (npm packs only in-package files); their parity tests fail if
6
+ // they drift from this canonical source.
8
7
 
9
8
  export interface TLSOptions {
10
9
  /** PEM or DER trust material. */
@@ -52,6 +51,8 @@ export interface Config {
52
51
  readConsistency?: ReadConsistencyMode;
53
52
  /** Bounded-read staleness window in milliseconds. 0 selects the server default. */
54
53
  maxStalenessMs?: number;
54
+ /** Maximum number of prepared statements to cache per connection LRU. Default: 0 (disabled on raw connect; 50 in pool). */
55
+ statementCacheSize?: number;
55
56
  }
56
57
 
57
58
  export interface FieldKey {
@@ -330,6 +331,35 @@ export declare const Type: {
330
331
 
331
332
  export declare function connect(cfg: Config): Promise<Conn>;
332
333
  export declare function connectCluster(cfg: Config): Promise<Cluster>;
334
+
335
+ export interface PoolOptions {
336
+ /** Minimum number of idle connections to maintain. Default: 1 */
337
+ min?: number;
338
+ /** Maximum number of connections in the pool. Default: 10 */
339
+ max?: number;
340
+ /** Maximum milliseconds to wait for a connection before timing out. Default: 30000 (30s) */
341
+ timeoutMs?: number;
342
+ /** Maximum idle milliseconds before an unused connection is retired. Default: 300000 (5m) */
343
+ idleTimeoutMs?: number;
344
+ /** Maximum number of prepared statements to cache per connection LRU. Default: 50. 0 disables. */
345
+ statementCacheSize?: number;
346
+ }
347
+
348
+ export declare class Pool {
349
+ constructor(config: Config, options?: PoolOptions);
350
+ readonly size: number;
351
+ readonly idleCount: number;
352
+ readonly inUseCount: number;
353
+ readonly isClosed: boolean;
354
+ acquire(): Promise<Conn>;
355
+ release(conn: Conn): Promise<void>;
356
+ withConnection<T>(fn: (conn: Conn) => Promise<T>): Promise<T>;
357
+ exec(sql: string, params?: unknown[]): Promise<Result>;
358
+ query(sql: string, params?: unknown[]): Promise<Result>;
359
+ close(): Promise<void>;
360
+ }
361
+
362
+ export declare function createPool(config: Config, options?: PoolOptions): Pool;
333
363
  export declare function generateFieldKey(id: string): FieldKey;
334
364
  export declare function inspectField(ciphertext: string): { keyID: string; type: FieldTypeDescriptor };
335
365
  export declare function encryptField(provider: FieldKeyProvider, database: string, table: string, column: string, type: FieldTypeDescriptor, value: unknown): Promise<string | null>;
package/nextsql.js CHANGED
@@ -1759,6 +1759,8 @@ class Conn {
1759
1759
  this.secret = 0n;
1760
1760
  this.busy = false;
1761
1761
  this.publicErrorCodes = false;
1762
+ this.statementCacheSize = cfg?.statementCacheSize ?? 0;
1763
+ this._stmtCache = this.statementCacheSize > 0 ? new Map() : null;
1762
1764
  }
1763
1765
 
1764
1766
  async handshake() {
@@ -1904,6 +1906,30 @@ class Conn {
1904
1906
  if (this.busy) {
1905
1907
  throw new NextSQLError('conflict', 'connection is busy');
1906
1908
  }
1909
+ if (this._stmtCache && params && params.length > 0) {
1910
+ let stmt = this._stmtCache.get(sql);
1911
+ if (stmt) {
1912
+ this._stmtCache.delete(sql);
1913
+ this._stmtCache.set(sql, stmt);
1914
+ } else {
1915
+ stmt = await this.prepare(sql);
1916
+ if (this._stmtCache.size >= this.statementCacheSize) {
1917
+ const oldestKey = this._stmtCache.keys().next().value;
1918
+ const oldestStmt = this._stmtCache.get(oldestKey);
1919
+ this._stmtCache.delete(oldestKey);
1920
+ oldestStmt.close().catch(() => {});
1921
+ }
1922
+ this._stmtCache.set(sql, stmt);
1923
+ }
1924
+ try {
1925
+ return await this._execute(stmt.id, params);
1926
+ } catch (err) {
1927
+ this._stmtCache.delete(sql);
1928
+ stmt.close().catch(() => {});
1929
+ throw err;
1930
+ }
1931
+ }
1932
+
1907
1933
  this.busy = true;
1908
1934
  try {
1909
1935
  await this.wire.writeFrame(Type.Query, encodeQuery(sql, params || []));
@@ -1996,6 +2022,11 @@ class Conn {
1996
2022
  }
1997
2023
 
1998
2024
  async close() {
2025
+ if (this._stmtCache) {
2026
+ const stmts = [...this._stmtCache.values()];
2027
+ this._stmtCache.clear();
2028
+ await Promise.allSettled(stmts.map(s => s.close()));
2029
+ }
1999
2030
  if (!this.wire) {
2000
2031
  return;
2001
2032
  }
@@ -2197,8 +2228,188 @@ async function connectCluster(cfg) {
2197
2228
  return new Cluster(cfg, conns);
2198
2229
  }
2199
2230
 
2231
+ class Pool {
2232
+ constructor(cfg, options = {}) {
2233
+ validateConfig(cfg);
2234
+ const min = options.min ?? 1;
2235
+ const max = options.max ?? 10;
2236
+ const timeoutMs = options.timeoutMs ?? 30000;
2237
+ const idleTimeoutMs = options.idleTimeoutMs ?? 300000;
2238
+ const statementCacheSize = options.statementCacheSize ?? 50;
2239
+
2240
+ if (min < 0) throw new NextSQLError('invalid_argument', 'min must be >= 0');
2241
+ if (max < 1) throw new NextSQLError('invalid_argument', 'max must be >= 1');
2242
+ if (max < min) throw new NextSQLError('invalid_argument', 'max must be >= min');
2243
+ if (statementCacheSize < 0) throw new NextSQLError('invalid_argument', 'statementCacheSize must be >= 0');
2244
+
2245
+ this._cfg = { ...cfg, statementCacheSize };
2246
+ this._min = min;
2247
+ this._max = max;
2248
+ this._timeoutMs = timeoutMs;
2249
+ this._idleTimeoutMs = idleTimeoutMs;
2250
+ this._statementCacheSize = statementCacheSize;
2251
+
2252
+ this._idle = []; // [{ conn, lastUsed }]
2253
+ this._inUse = new Set();
2254
+ this._waiters = []; // [{ resolve, reject, timer }]
2255
+ this._closed = false;
2256
+ }
2257
+
2258
+ get size() {
2259
+ return this._idle.length + this._inUse.size;
2260
+ }
2261
+
2262
+ get idleCount() {
2263
+ return this._idle.length;
2264
+ }
2265
+
2266
+ get inUseCount() {
2267
+ return this._inUse.size;
2268
+ }
2269
+
2270
+ get isClosed() {
2271
+ return this._closed;
2272
+ }
2273
+
2274
+ async acquire() {
2275
+ if (this._closed) {
2276
+ throw new NextSQLError('unavailable', 'connection pool is closed');
2277
+ }
2278
+
2279
+ const now = Date.now();
2280
+ while (this._idle.length > 0) {
2281
+ const item = this._idle.pop();
2282
+ if (this._idleTimeoutMs > 0 && (now - item.lastUsed) > this._idleTimeoutMs) {
2283
+ item.conn.close().catch(() => {});
2284
+ continue;
2285
+ }
2286
+ if (item.conn.wire && item.conn.wire.closed) {
2287
+ continue;
2288
+ }
2289
+ this._inUse.add(item.conn);
2290
+ return item.conn;
2291
+ }
2292
+
2293
+ if (this.size < this._max) {
2294
+ const conn = await connect(this._cfg);
2295
+ this._inUse.add(conn);
2296
+ return conn;
2297
+ }
2298
+
2299
+ return new Promise((resolve, reject) => {
2300
+ let timer = null;
2301
+ if (this._timeoutMs > 0) {
2302
+ timer = setTimeout(() => {
2303
+ const idx = this._waiters.findIndex(w => w.resolve === resolve);
2304
+ if (idx >= 0) {
2305
+ this._waiters.splice(idx, 1);
2306
+ reject(new NextSQLError('resource_exhausted', 'connection pool exhausted, wait timeout exceeded'));
2307
+ }
2308
+ }, this._timeoutMs);
2309
+ }
2310
+ this._waiters.push({ resolve, reject, timer });
2311
+ });
2312
+ }
2313
+
2314
+ async release(conn) {
2315
+ if (!this._inUse.has(conn)) {
2316
+ return;
2317
+ }
2318
+ this._inUse.delete(conn);
2319
+
2320
+ if (this._closed || (conn.wire && conn.wire.closed)) {
2321
+ conn.close().catch(() => {});
2322
+ this._drainWaiters();
2323
+ return;
2324
+ }
2325
+
2326
+ // Clean connection state: roll back uncommitted transaction
2327
+ try {
2328
+ await conn.exec('ROLLBACK');
2329
+ } catch {
2330
+ conn.close().catch(() => {});
2331
+ this._drainWaiters();
2332
+ return;
2333
+ }
2334
+
2335
+ while (this._waiters.length > 0) {
2336
+ const waiter = this._waiters.shift();
2337
+ if (waiter.timer) clearTimeout(waiter.timer);
2338
+ this._inUse.add(conn);
2339
+ waiter.resolve(conn);
2340
+ return;
2341
+ }
2342
+
2343
+ this._idle.push({ conn, lastUsed: Date.now() });
2344
+ }
2345
+
2346
+ _drainWaiters() {
2347
+ if (this._closed) {
2348
+ while (this._waiters.length > 0) {
2349
+ const w = this._waiters.shift();
2350
+ if (w.timer) clearTimeout(w.timer);
2351
+ w.reject(new NextSQLError('unavailable', 'connection pool is closed'));
2352
+ }
2353
+ return;
2354
+ }
2355
+ if (this.size < this._max && this._waiters.length > 0) {
2356
+ const waiter = this._waiters.shift();
2357
+ if (waiter.timer) clearTimeout(waiter.timer);
2358
+ connect(this._cfg).then(conn => {
2359
+ this._inUse.add(conn);
2360
+ waiter.resolve(conn);
2361
+ }).catch(err => {
2362
+ waiter.reject(err);
2363
+ });
2364
+ }
2365
+ }
2366
+
2367
+ async withConnection(fn) {
2368
+ const conn = await this.acquire();
2369
+ try {
2370
+ return await fn(conn);
2371
+ } finally {
2372
+ await this.release(conn);
2373
+ }
2374
+ }
2375
+
2376
+ async exec(sql, params) {
2377
+ return this.withConnection(conn => conn.exec(sql, params));
2378
+ }
2379
+
2380
+ async query(sql, params) {
2381
+ return this.withConnection(async (conn) => {
2382
+ const rows = await conn.query(sql, params);
2383
+ return rows.collect();
2384
+ });
2385
+ }
2386
+
2387
+ async close() {
2388
+ if (this._closed) return;
2389
+ this._closed = true;
2390
+
2391
+ while (this._waiters.length > 0) {
2392
+ const w = this._waiters.shift();
2393
+ if (w.timer) clearTimeout(w.timer);
2394
+ w.reject(new NextSQLError('unavailable', 'connection pool is closed'));
2395
+ }
2396
+
2397
+ const conns = [...this._idle.map(item => item.conn), ...this._inUse];
2398
+ this._idle = [];
2399
+ this._inUse.clear();
2400
+
2401
+ await Promise.allSettled(conns.map(c => c.close()));
2402
+ }
2403
+ }
2404
+
2405
+ function createPool(cfg, options) {
2406
+ return new Pool(cfg, options);
2407
+ }
2408
+
2200
2409
  module.exports = {
2201
2410
  connect,
2411
+ createPool,
2412
+ Pool,
2202
2413
  FlagCancel,
2203
2414
  FlagPublicErrorCodes,
2204
2415
  connectCluster,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bzync/nextsql",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Official NextSQL Node.js driver. Keys never go in URLs.",
5
5
  "license": "MIT",
6
6
  "main": "nextsql.js",