@steve31415/baselib 2.2.1 → 2.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.
package/dist/db.d.ts CHANGED
@@ -1,11 +1,30 @@
1
1
  import pg from 'pg';
2
- import type { Logger } from './log-core.js';
2
+ import { type Logger } from './log-core.js';
3
3
  export interface PoolOptions {
4
4
  /** Override the database name (default: env DB_NAME / DATABASE_URL path). */
5
5
  database?: string;
6
6
  max?: number;
7
+ /** Receives idle-client errors (see logPoolErrors). Without one they go to
8
+ * stderr — on the Cloud Logging backstop, but not in Axiom. */
9
+ logger?: Logger;
7
10
  }
11
+ /** pg-pool re-emits an idle client's socket error as its own 'error' event —
12
+ * in production "Connection terminated unexpectedly" when Cloud SQL closes an
13
+ * idle connection. Unhandled, that event throws and takes the process down
14
+ * (todo 2026-08-20, notes 2026-08-27 ×2). The failed client has already been
15
+ * removed from the pool and the next checkout opens a fresh connection, so
16
+ * the right response is to log it and carry on. createPool attaches this;
17
+ * exported for apps that build their own pg.Pool. */
18
+ export declare function logPoolErrors(pool: pg.Pool, logger?: Logger): void;
8
19
  export declare function createPool(opts?: PoolOptions): Promise<pg.Pool>;
20
+ /** End a pool and resolve only once every client's connection has actually
21
+ * closed. pg-pool's own `end()` resolves as soon as it has *asked* its idle
22
+ * clients to close — their sockets are still open — so a caller that then
23
+ * drops the database `with (force)` can kill a backend mid-goodbye, and the
24
+ * FATAL it sends back surfaces as an unhandled pool 'error' (it took down a
25
+ * todo2 deploy with every test green, 2026-08-20). pg-pool emits 'remove' for
26
+ * each client once its socket has closed; wait for all of them. */
27
+ export declare function endPool(pool: pg.Pool): Promise<void>;
9
28
  export declare function migrate(pool: pg.Pool, migrationsDir: string, logger?: Logger): Promise<{
10
29
  applied: string[];
11
30
  }>;
package/dist/db.js CHANGED
@@ -6,6 +6,7 @@ import { readdir, readFile } from 'node:fs/promises';
6
6
  import { join } from 'node:path';
7
7
  import pg from 'pg';
8
8
  import { requireEnv } from './config.js';
9
+ import { serializeError } from './log-core.js';
9
10
  // DATE columns come back as 'YYYY-MM-DD' strings, not JS Dates: calendar
10
11
  // dates are timezone-free by meaning, and a Date object smears them across
11
12
  // zones (and JSON-serializes to a full ISO timestamp). Set here — on the
@@ -13,7 +14,27 @@ import { requireEnv } from './config.js';
13
14
  // own pg copy miss this one (file:-dependency installs resolve separate pg
14
15
  // instances).
15
16
  pg.types.setTypeParser(pg.types.builtins.DATE, (v) => v);
17
+ /** pg-pool re-emits an idle client's socket error as its own 'error' event —
18
+ * in production "Connection terminated unexpectedly" when Cloud SQL closes an
19
+ * idle connection. Unhandled, that event throws and takes the process down
20
+ * (todo 2026-08-20, notes 2026-08-27 ×2). The failed client has already been
21
+ * removed from the pool and the next checkout opens a fresh connection, so
22
+ * the right response is to log it and carry on. createPool attaches this;
23
+ * exported for apps that build their own pg.Pool. */
24
+ export function logPoolErrors(pool, logger) {
25
+ pool.on('error', (err) => {
26
+ if (logger)
27
+ logger.error('database pool: idle client error', { error: err });
28
+ else
29
+ console.error('database pool: idle client error', serializeError(err));
30
+ });
31
+ }
16
32
  export async function createPool(opts = {}) {
33
+ const pool = await newPool(opts);
34
+ logPoolErrors(pool, opts.logger);
35
+ return pool;
36
+ }
37
+ async function newPool(opts) {
17
38
  const instance = process.env.CLOUD_SQL_INSTANCE;
18
39
  if (instance) {
19
40
  const { Connector, AuthTypes, IpAddressTypes } = await import('@google-cloud/cloud-sql-connector');
@@ -35,6 +56,26 @@ export async function createPool(opts = {}) {
35
56
  url.pathname = `/${opts.database}`;
36
57
  return new pg.Pool({ connectionString: url.toString(), max: opts.max ?? 5 });
37
58
  }
59
+ /** End a pool and resolve only once every client's connection has actually
60
+ * closed. pg-pool's own `end()` resolves as soon as it has *asked* its idle
61
+ * clients to close — their sockets are still open — so a caller that then
62
+ * drops the database `with (force)` can kill a backend mid-goodbye, and the
63
+ * FATAL it sends back surfaces as an unhandled pool 'error' (it took down a
64
+ * todo2 deploy with every test green, 2026-08-20). pg-pool emits 'remove' for
65
+ * each client once its socket has closed; wait for all of them. */
66
+ export async function endPool(pool) {
67
+ let open = pool.totalCount;
68
+ const allClosed = new Promise((resolve) => {
69
+ if (open === 0)
70
+ return resolve();
71
+ pool.on('remove', () => {
72
+ if (--open === 0)
73
+ resolve();
74
+ });
75
+ });
76
+ await pool.end();
77
+ await allClosed;
78
+ }
38
79
  // Plain-SQL migrations: numbered files (001-*.sql, 002-*.sql, …) applied in
39
80
  // order, recorded in schema_migrations, each inside a transaction, guarded by
40
81
  // an advisory lock so concurrent boots can't race. Applied at service boot.
@@ -52,7 +52,8 @@ export interface ShipperOptions {
52
52
  * fire at the next CPU window (next request or shutdown), not on the
53
53
  * wall clock. */
54
54
  retryDelaysMs?: number[];
55
- /** Per-attempt fetch timeout (default 3s). */
55
+ /** Per-attempt fetch timeout (default 10s; 3s aborted most of the fleet's
56
+ * attempts in the two weeks to 2026-08-28). */
56
57
  attemptTimeoutMs?: number;
57
58
  /** Caps across ALL undelivered batches; overflow drops oldest with an
58
59
  * onGiveUp('buffer-overflow'). Defaults 5000 events / 5 MB. */
package/dist/log-core.js CHANGED
@@ -70,7 +70,7 @@ export class LogShipper {
70
70
  this.intervalMs = opts.intervalMs ?? 2000;
71
71
  this.maxAttempts = opts.maxAttempts ?? 4;
72
72
  this.retryDelaysMs = opts.retryDelaysMs ?? [5_000, 15_000, 40_000];
73
- this.attemptTimeoutMs = opts.attemptTimeoutMs ?? 3_000;
73
+ this.attemptTimeoutMs = opts.attemptTimeoutMs ?? 10_000;
74
74
  this.maxBufferedEvents = opts.maxBufferedEvents ?? 5_000;
75
75
  this.maxBufferedBytes = opts.maxBufferedBytes ?? 5 * 1024 * 1024;
76
76
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steve31415/baselib",
3
- "version": "2.2.1",
3
+ "version": "2.3.0",
4
4
  "description": "Plasticine new-world shared platform library: logging, auth, service-to-service auth, db, HTTP, sync, app updates",
5
5
  "type": "module",
6
6
  "license": "MIT",