lambda-pool 0.1.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.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +291 -0
  3. package/dist/adapters/health.d.ts +30 -0
  4. package/dist/adapters/health.d.ts.map +1 -0
  5. package/dist/adapters/health.js +36 -0
  6. package/dist/adapters/health.js.map +1 -0
  7. package/dist/adapters/mysql.d.ts +40 -0
  8. package/dist/adapters/mysql.d.ts.map +1 -0
  9. package/dist/adapters/mysql.js +34 -0
  10. package/dist/adapters/mysql.js.map +1 -0
  11. package/dist/adapters/pg.d.ts +46 -0
  12. package/dist/adapters/pg.d.ts.map +1 -0
  13. package/dist/adapters/pg.js +34 -0
  14. package/dist/adapters/pg.js.map +1 -0
  15. package/dist/application/diagnostics.d.ts +33 -0
  16. package/dist/application/diagnostics.d.ts.map +1 -0
  17. package/dist/application/diagnostics.js +94 -0
  18. package/dist/application/diagnostics.js.map +1 -0
  19. package/dist/application/inspect.d.ts +20 -0
  20. package/dist/application/inspect.d.ts.map +1 -0
  21. package/dist/application/inspect.js +31 -0
  22. package/dist/application/inspect.js.map +1 -0
  23. package/dist/application/preflight.d.ts +28 -0
  24. package/dist/application/preflight.d.ts.map +1 -0
  25. package/dist/application/preflight.js +49 -0
  26. package/dist/application/preflight.js.map +1 -0
  27. package/dist/application/recommend.d.ts +22 -0
  28. package/dist/application/recommend.d.ts.map +1 -0
  29. package/dist/application/recommend.js +30 -0
  30. package/dist/application/recommend.js.map +1 -0
  31. package/dist/core/budget.d.ts +35 -0
  32. package/dist/core/budget.d.ts.map +1 -0
  33. package/dist/core/budget.js +48 -0
  34. package/dist/core/budget.js.map +1 -0
  35. package/dist/core/config.d.ts +24 -0
  36. package/dist/core/config.d.ts.map +1 -0
  37. package/dist/core/config.js +32 -0
  38. package/dist/core/config.js.map +1 -0
  39. package/dist/core/dsn.d.ts +20 -0
  40. package/dist/core/dsn.d.ts.map +1 -0
  41. package/dist/core/dsn.js +38 -0
  42. package/dist/core/dsn.js.map +1 -0
  43. package/dist/core/env.d.ts +27 -0
  44. package/dist/core/env.d.ts.map +1 -0
  45. package/dist/core/env.js +52 -0
  46. package/dist/core/env.js.map +1 -0
  47. package/dist/core/providers.d.ts +29 -0
  48. package/dist/core/providers.d.ts.map +1 -0
  49. package/dist/core/providers.js +121 -0
  50. package/dist/core/providers.js.map +1 -0
  51. package/dist/core/redact.d.ts +20 -0
  52. package/dist/core/redact.d.ts.map +1 -0
  53. package/dist/core/redact.js +63 -0
  54. package/dist/core/redact.js.map +1 -0
  55. package/dist/core/result.d.ts +16 -0
  56. package/dist/core/result.d.ts.map +1 -0
  57. package/dist/core/result.js +32 -0
  58. package/dist/core/result.js.map +1 -0
  59. package/dist/core/retry.d.ts +31 -0
  60. package/dist/core/retry.d.ts.map +1 -0
  61. package/dist/core/retry.js +67 -0
  62. package/dist/core/retry.js.map +1 -0
  63. package/dist/core/url.d.ts +38 -0
  64. package/dist/core/url.d.ts.map +1 -0
  65. package/dist/core/url.js +88 -0
  66. package/dist/core/url.js.map +1 -0
  67. package/dist/index.d.ts +17 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +35 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/presentation/bin.d.ts +3 -0
  72. package/dist/presentation/bin.d.ts.map +1 -0
  73. package/dist/presentation/bin.js +11 -0
  74. package/dist/presentation/bin.js.map +1 -0
  75. package/dist/presentation/cli.d.ts +10 -0
  76. package/dist/presentation/cli.d.ts.map +1 -0
  77. package/dist/presentation/cli.js +124 -0
  78. package/dist/presentation/cli.js.map +1 -0
  79. package/package.json +133 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 owlCoder
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,291 @@
1
+ # lambda-pool
2
+
3
+ [![npm version](https://img.shields.io/npm/v/lambda-pool.svg)](https://www.npmjs.com/package/lambda-pool)
4
+ [![license](https://img.shields.io/npm/l/lambda-pool.svg)](./LICENSE)
5
+ [![types](https://img.shields.io/npm/types/lambda-pool.svg)](https://www.npmjs.com/package/lambda-pool)
6
+ [![zero deps](https://img.shields.io/badge/runtime%20deps-0-22c55e.svg)](#zero-runtime-dependencies)
7
+
8
+ Serverless-safe **connection-pool options** for **MySQL** (`mysql2`) and **Postgres** (`pg`)
9
+ on Vercel / AWS Lambda / Cloudflare-to-DB, where the database has a **small
10
+ `max_connections`** budget (Aiven free tier, Neon, Supabase, RDS micro, PlanetScale).
11
+
12
+ It returns a plain options object you pass to your own driver. **Zero runtime
13
+ dependencies.** It does not open connections, wrap your driver, or pin a version.
14
+
15
+ ```bash
16
+ npm install lambda-pool
17
+ ```
18
+
19
+ ## The bug this prevents
20
+
21
+ On Vercel/Lambda every **warm** function instance keeps **its own** pool. If each
22
+ pool opens `N` connections and the platform keeps `M` instances warm, your
23
+ database sees up to **`N × M`** connections.
24
+
25
+ Managed databases on small plans cap `max_connections` very low (often **10–25**).
26
+ So a perfectly reasonable-looking `connectionLimit: 10` blows the budget under
27
+ mild traffic and you get:
28
+
29
+ - MySQL: `ER_CON_COUNT_ERROR: Too many connections`
30
+ - Postgres: `sorry, too many clients already`
31
+
32
+ …intermittently, only in production, only under load. The classic "works on my
33
+ machine, dies on Black Friday" footgun.
34
+
35
+ ## The fix (and why it's counter-intuitive)
36
+
37
+ Make each pool **tiny** — default **1 connection per instance**. The platform's
38
+ horizontal scaling *is* your concurrency; the database stops melting. Idle
39
+ instances release their slot so they don't sit on the budget.
40
+
41
+ That's the whole idea. This package just encodes the right defaults so you don't
42
+ have to rediscover them in an incident.
43
+
44
+ ## Usage
45
+
46
+ ### MySQL (`mysql2`)
47
+
48
+ ```ts
49
+ import mysql from "mysql2/promise";
50
+ import { buildMysqlPoolOptions } from "lambda-pool/mysql";
51
+
52
+ const pool = mysql.createPool(buildMysqlPoolOptions(process.env));
53
+ ```
54
+
55
+ ### Postgres (`pg`)
56
+
57
+ ```ts
58
+ import { Pool } from "pg";
59
+ import { buildPgPoolOptions } from "lambda-pool/pg";
60
+
61
+ const pool = new Pool(buildPgPoolOptions(process.env));
62
+ ```
63
+
64
+ Both also re-exported from the root: `import { buildPgPoolOptions } from "lambda-pool"`.
65
+
66
+ ## Diagnostics — lint your connection for serverless
67
+
68
+ Beyond building options, `lambda-pool` can **analyze** a connection config and
69
+ tell you whether it will survive serverless fan-out. Pure function, no I/O:
70
+
71
+ ```ts
72
+ import { inspectEnv, formatReport } from "lambda-pool";
73
+
74
+ const report = inspectEnv(process.env);
75
+ console.log(formatReport(report));
76
+ // lambda-pool diagnostics for mysql://u:***@pg.aivencloud.com/db [aiven]
77
+ // ⚠ SMALL_MAX_CONNECTIONS: Aiven typically caps max_connections around 20; keep the per-instance pool at 1.
78
+ ```
79
+
80
+ It recognizes the provider from the host (Aiven, Neon, Supabase, PlanetScale,
81
+ RDS/Aurora, Railway, Render, Vercel Postgres) and warns about: pool sizes too
82
+ large for the provider's budget, SSL requested in the URL with no CA supplied,
83
+ and using a direct host when a pooled endpoint is available.
84
+
85
+ ## CLI
86
+
87
+ ```bash
88
+ # Lint the connection in your env (exit 1 on any warning — good as a CI gate)
89
+ DATABASE_URL=postgres://… npx lambda-pool inspect
90
+
91
+ # Full preflight check (config + diagnostics) — strict gate, exit 1 on any issue
92
+ DATABASE_URL=postgres://… npx lambda-pool doctor
93
+
94
+ # Recommend a pool size straight from a connection URL (detects the provider)
95
+ npx lambda-pool recommend "postgres://…@db.aivencloud.com/app" 10
96
+
97
+ # Recommend from raw numbers: max_connections, instances, [reserved], [other]
98
+ npx lambda-pool budget 100 20
99
+ # recommended pool limit: 4
100
+ # 97 usable connections (100 max − 3 reserved − 0 other) ÷ 20 instances → pool of 4 per instance (peak 80).
101
+
102
+ # List recognized providers
103
+ npx lambda-pool providers
104
+ ```
105
+
106
+ ## Budget calculator (programmatic)
107
+
108
+ ```ts
109
+ import { recommendPoolLimit } from "lambda-pool";
110
+
111
+ const { recommendedPoolLimit, exceedsBudget, rationale } = recommendPoolLimit({
112
+ maxConnections: 20, // your DB's max_connections
113
+ expectedInstances: 30, // peak warm serverless instances
114
+ });
115
+ // recommendedPoolLimit: 1, exceedsBudget: true → use a pooler
116
+ ```
117
+
118
+ ## Connection-string utilities
119
+
120
+ ```ts
121
+ import { parseConnectionString, redactUrl } from "lambda-pool";
122
+
123
+ redactUrl("postgres://u:secret@host/db"); // → "postgres://u:***@host/db"
124
+ parseConnectionString("mysql://root@127.0.0.1/test").port; // → 3306
125
+ ```
126
+
127
+ ## Health checks
128
+
129
+ Driver-agnostic readiness probe. Works with any pool that has a `query()`
130
+ method (both `mysql2` and `pg` do), so `lambda-pool` stays dependency-free:
131
+
132
+ ```ts
133
+ import { checkHealth } from "lambda-pool";
134
+
135
+ const { healthy, latencyMs, error } = await checkHealth(pool, { timeoutMs: 2000 });
136
+ // use in a /healthz endpoint — never throws
137
+ ```
138
+
139
+ ## Retry transient connect errors
140
+
141
+ Cold starts and brief failovers cause transient connect errors. `withRetry`
142
+ wraps an operation with jittered exponential backoff:
143
+
144
+ ```ts
145
+ import { withRetry, isTransientDbError } from "lambda-pool";
146
+
147
+ const client = await withRetry(() => pool.connect(), {
148
+ attempts: 4,
149
+ retryable: isTransientDbError, // don't retry auth failures
150
+ });
151
+ ```
152
+
153
+ ## Preflight (startup gate)
154
+
155
+ Run a single check at boot that combines config validation, diagnostics, and an
156
+ optional live reachability probe. The probe is **injected**, so the check stays
157
+ pure unless you opt into a real connection:
158
+
159
+ ```ts
160
+ import { preflight } from "lambda-pool";
161
+ import { isReachable } from "lambda-pool/health";
162
+
163
+ const result = await preflight(process.env, {
164
+ probe: () => isReachable(pool), // optional live check
165
+ });
166
+ if (!result.ok) {
167
+ console.error(result.diagnostics);
168
+ process.exit(1);
169
+ }
170
+ ```
171
+
172
+ ## Redact objects for logging
173
+
174
+ `redactUrl` masks a connection string; `redact` masks whole objects (config
175
+ dumps, error context) by key name, so secrets never reach your log aggregator:
176
+
177
+ ```ts
178
+ import { redact } from "lambda-pool";
179
+
180
+ redact({ host: "db", password: "p", nested: { token: "t" } });
181
+ // → { host: "db", password: "***", nested: { token: "***" } }
182
+ ```
183
+
184
+ ## Build a connection string from parts
185
+
186
+ ```ts
187
+ import { buildDsn } from "lambda-pool";
188
+
189
+ buildDsn({ engine: "postgres", host: "db", user: "u", password: "p/w", database: "app" });
190
+ // → "postgres://u:p%2Fw@db:5432/app" (credentials safely encoded)
191
+ ```
192
+
193
+ ## Result type
194
+
195
+ Non-throwing variants return a small `Result` you can branch on:
196
+
197
+ ```ts
198
+ import { safeParseConnectionString } from "lambda-pool";
199
+
200
+ const r = safeParseConnectionString(process.env.DATABASE_URL ?? "");
201
+ if (!r.ok) { /* handle r.error */ } else { /* use r.value */ }
202
+ ```
203
+
204
+ ## API surface
205
+
206
+ | Import | Exports |
207
+ |---|---|
208
+ | `lambda-pool/mysql` | `buildMysqlPoolOptions` |
209
+ | `lambda-pool/pg` | `buildPgPoolOptions` |
210
+ | `lambda-pool/url` | `parseConnectionString`, `safeParseConnectionString`, `redactUrl`, `urlRequestsSsl` |
211
+ | `lambda-pool/providers` | `detectProvider`, `listProviders`, `isPooledEndpoint` |
212
+ | `lambda-pool/budget` | `recommendPoolLimit` |
213
+ | `lambda-pool/diagnostics` | `diagnose`, `formatReport` |
214
+ | `lambda-pool/recommend` | `recommendForUrl` |
215
+ | `lambda-pool/preflight` | `preflight` |
216
+ | `lambda-pool/config` | `loadPoolConfig` |
217
+ | `lambda-pool/dsn` | `buildDsn` |
218
+ | `lambda-pool/redact` | `redact` |
219
+ | `lambda-pool/health` | `checkHealth`, `isReachable` |
220
+ | `lambda-pool/retry` | `withRetry`, `backoffDelay`, `isTransientDbError` |
221
+ | `lambda-pool/result` | `Result`, `ok`, `err`, `attempt`, `unwrap` |
222
+
223
+ All of the above are also re-exported from the package root.
224
+
225
+ ## Environment variables
226
+
227
+ | Variable | Purpose | Default |
228
+ |---|---|---|
229
+ | `DATABASE_URL` | Connection URI. Aliases: `MYSQL_URL` / `POSTGRES_URL` / `PG_URL`. | **required** |
230
+ | `DATABASE_POOL_LIMIT` | Per-instance pool size. Raise **only** behind a pooler (PgBouncer, Neon pooled endpoint) or on a bigger plan. | `1` |
231
+ | `DATABASE_SSL_CA_BASE64` | Base64 of your provider's CA cert → enables strict TLS. | off |
232
+
233
+ A non-positive or non-numeric `DATABASE_POOL_LIMIT` is ignored and falls back to
234
+ the default, so a bad env var can never silently widen the pool.
235
+
236
+ ## What the defaults are
237
+
238
+ **MySQL** (`mysql2` `PoolOptions`): `connectionLimit: 1`, `maxIdle: 1`,
239
+ `idleTimeout: 30000`, `enableKeepAlive: true`, `waitForConnections: true`,
240
+ `queueLimit: 0`.
241
+
242
+ **Postgres** (`pg` `PoolConfig`): `max: 1`, `idleTimeoutMillis: 30000`,
243
+ `connectionTimeoutMillis: 10000`, `maxUses: 7500`, `allowExitOnIdle: true`.
244
+
245
+ Override the default size in code without an env var:
246
+
247
+ ```ts
248
+ buildPgPoolOptions(process.env, { defaultPoolLimit: 2 }); // behind a pooler
249
+ ```
250
+
251
+ ### A note on TLS
252
+
253
+ Managed providers hand you a CA bundle, but the URI's `?ssl-mode=REQUIRED` /
254
+ `?sslmode=require` param is **not** interpreted by `mysql2` / `pg` the way people
255
+ expect. Pass the CA explicitly via `DATABASE_SSL_CA_BASE64` and the server is
256
+ verified (`rejectUnauthorized: true`).
257
+
258
+ ## Architecture
259
+
260
+ The source is organized into strict clean-architecture layers
261
+ (`core → application → adapters → presentation`) with the dependency rule
262
+ enforced in CI. See [ARCHITECTURE.md](./ARCHITECTURE.md).
263
+
264
+ ## Zero runtime dependencies
265
+
266
+ `lambda-pool` imports nothing at runtime. `mysql2` / `pg` are **your** dependency
267
+ and only `devDependencies` here (for tests/types). You stay in control of the
268
+ driver version.
269
+
270
+ ## Development
271
+
272
+ ```bash
273
+ npm install
274
+ npm run typecheck
275
+ npm test # hermetic unit tests, no DB needed
276
+ npm run build
277
+ ```
278
+
279
+ Integration tests run the option objects against real `mysql2` / `pg` pools,
280
+ using throwaway containers whose `max_connections` is pinned to 10 to mirror a
281
+ free tier:
282
+
283
+ ```bash
284
+ docker compose -f docker-compose.test.yml up -d
285
+ RUN_DB_TESTS=1 npm run test:integration
286
+ docker compose -f docker-compose.test.yml down -v
287
+ ```
288
+
289
+ ## License
290
+
291
+ MIT
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The minimal surface needed to run a trivial probe query. Both
3
+ * `mysql2`'s pool and `pg`'s pool structurally satisfy this.
4
+ */
5
+ export interface Queryable {
6
+ query(sql: string): Promise<unknown>;
7
+ }
8
+ export interface HealthResult {
9
+ healthy: boolean;
10
+ /** Round-trip latency of the probe query, in milliseconds. */
11
+ latencyMs: number;
12
+ /** Present when the probe failed. */
13
+ error?: Error;
14
+ }
15
+ export interface HealthOptions {
16
+ /** SQL used to probe. Defaults to `SELECT 1`. */
17
+ probeSql?: string;
18
+ /** Fail the probe if it takes longer than this many ms. */
19
+ timeoutMs?: number;
20
+ }
21
+ /**
22
+ * Run a probe query against any `Queryable` and report health + latency.
23
+ *
24
+ * Never throws — failures are returned in the result so callers can use it
25
+ * directly in a readiness endpoint.
26
+ */
27
+ export declare function checkHealth(db: Queryable, options?: HealthOptions): Promise<HealthResult>;
28
+ /** Convenience: true/false readiness, swallowing the detail. */
29
+ export declare function isReachable(db: Queryable, options?: HealthOptions): Promise<boolean>;
30
+ //# sourceMappingURL=health.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"health.d.ts","sourceRoot":"","sources":["../../src/adapters/health.ts"],"names":[],"mappings":"AASA;;;GAGG;AACH,MAAM,WAAW,SAAS;IACxB,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,8DAA8D;IAC9D,SAAS,EAAE,MAAM,CAAC;IAClB,qCAAqC;IACrC,KAAK,CAAC,EAAE,KAAK,CAAC;CACf;AAED,MAAM,WAAW,aAAa;IAC5B,iDAAiD;IACjD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAWD;;;;;GAKG;AACH,wBAAsB,WAAW,CAC/B,EAAE,EAAE,SAAS,EACb,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,YAAY,CAAC,CAwBvB;AAED,gEAAgE;AAChE,wBAAsB,WAAW,CAC/B,EAAE,EAAE,SAAS,EACb,OAAO,CAAC,EAAE,aAAa,GACtB,OAAO,CAAC,OAAO,CAAC,CAGlB"}
@@ -0,0 +1,36 @@
1
+ // Driver-agnostic health checks (Dependency Inversion + Interface Segregation).
2
+ //
3
+ // The package depends on a tiny `Queryable` port — the narrowest surface that
4
+ // both `mysql2` pools and `pg` pools already satisfy — not on a concrete driver.
5
+ // Adapters live next to the builders. This keeps `lambda-pool` dependency-free
6
+ // while still offering a "is the DB reachable?" helper.
7
+ function timeout(ms) {
8
+ return new Promise((_, reject) => setTimeout(() => reject(new Error(`lambda-pool: health probe timed out after ${ms}ms`)), ms));
9
+ }
10
+ /**
11
+ * Run a probe query against any `Queryable` and report health + latency.
12
+ *
13
+ * Never throws — failures are returned in the result so callers can use it
14
+ * directly in a readiness endpoint.
15
+ */
16
+ export async function checkHealth(db, options = {}) {
17
+ const sql = options.probeSql ?? "SELECT 1";
18
+ const started = performance.now();
19
+ const probe = (async () => {
20
+ await db.query(sql);
21
+ })();
22
+ const race = options.timeoutMs
23
+ ? Promise.race([probe, timeout(options.timeoutMs)]).then(() => ({ ok: true, value: undefined }), (e) => ({ ok: false, error: e }))
24
+ : probe.then(() => ({ ok: true, value: undefined }), (e) => ({ ok: false, error: e }));
25
+ const outcome = await race;
26
+ const latencyMs = Math.round(performance.now() - started);
27
+ return outcome.ok
28
+ ? { healthy: true, latencyMs }
29
+ : { healthy: false, latencyMs, error: outcome.error };
30
+ }
31
+ /** Convenience: true/false readiness, swallowing the detail. */
32
+ export async function isReachable(db, options) {
33
+ const r = await checkHealth(db, options);
34
+ return r.healthy;
35
+ }
36
+ //# sourceMappingURL=health.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"health.js","sourceRoot":"","sources":["../../src/adapters/health.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,EAAE;AACF,8EAA8E;AAC9E,iFAAiF;AACjF,+EAA+E;AAC/E,wDAAwD;AA2BxD,SAAS,OAAO,CAAC,EAAU;IACzB,OAAO,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,CAC/B,UAAU,CACR,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,6CAA6C,EAAE,IAAI,CAAC,CAAC,EAC5E,EAAE,CACH,CACF,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,EAAa,EACb,UAAyB,EAAE;IAE3B,MAAM,GAAG,GAAG,OAAO,CAAC,QAAQ,IAAI,UAAU,CAAC;IAC3C,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;IAElC,MAAM,KAAK,GAAG,CAAC,KAAK,IAAI,EAAE;QACxB,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACtB,CAAC,CAAC,EAAE,CAAC;IAEL,MAAM,IAAI,GAAiC,OAAO,CAAC,SAAS;QAC1D,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,CACpD,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,IAAa,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,EAC/C,CAAC,CAAQ,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,KAAc,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CACjD;QACH,CAAC,CAAC,KAAK,CAAC,IAAI,CACR,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,IAAa,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,EAC/C,CAAC,CAAQ,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,KAAc,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CACjD,CAAC;IAEN,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC;IAC3B,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,CAAC;IAE1D,OAAO,OAAO,CAAC,EAAE;QACf,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE;QAC9B,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC;AAC1D,CAAC;AAED,gEAAgE;AAChE,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,EAAa,EACb,OAAuB;IAEvB,MAAM,CAAC,GAAG,MAAM,WAAW,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;IACzC,OAAO,CAAC,CAAC,OAAO,CAAC;AACnB,CAAC"}
@@ -0,0 +1,40 @@
1
+ import { type Env } from "../core/env.ts";
2
+ export interface MysqlPoolEnv extends Env {
3
+ /** Connection URI (mysql://...). Alias: MYSQL_URL. */
4
+ DATABASE_URL?: string;
5
+ MYSQL_URL?: string;
6
+ /** Base64-encoded CA cert — when present, enables strict TLS. */
7
+ DATABASE_SSL_CA_BASE64?: string;
8
+ /** Per-instance pool size override; defaults to 1. */
9
+ DATABASE_POOL_LIMIT?: string;
10
+ }
11
+ /** Shape compatible with mysql2 `PoolOptions` (structural — no import needed). */
12
+ export interface MysqlPoolOptions {
13
+ uri: string;
14
+ connectionLimit: number;
15
+ maxIdle: number;
16
+ idleTimeout: number;
17
+ enableKeepAlive: boolean;
18
+ waitForConnections: boolean;
19
+ queueLimit: number;
20
+ ssl?: {
21
+ ca: string;
22
+ rejectUnauthorized: boolean;
23
+ };
24
+ }
25
+ export interface BuildMysqlOptions {
26
+ /** Override the default pool size of 1. */
27
+ defaultPoolLimit?: number;
28
+ }
29
+ /**
30
+ * Build serverless-safe mysql2 pool options.
31
+ *
32
+ * - `connectionLimit` defaults to 1 (one connection per warm lambda). Raise it
33
+ * only behind a connection pooler or on a plan with headroom, via
34
+ * `DATABASE_POOL_LIMIT`.
35
+ * - `maxIdle: 1` + `idleTimeout` lets idle lambdas release their connection so
36
+ * they don't sit on a slot of a tiny `max_connections` budget.
37
+ * - TLS is enabled with strict verification when `DATABASE_SSL_CA_BASE64` is set.
38
+ */
39
+ export declare function buildMysqlPoolOptions(env: MysqlPoolEnv, opts?: BuildMysqlOptions): MysqlPoolOptions;
40
+ //# sourceMappingURL=mysql.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mysql.d.ts","sourceRoot":"","sources":["../../src/adapters/mysql.ts"],"names":[],"mappings":"AAMA,OAAO,EACL,KAAK,GAAG,EAIT,MAAM,gBAAgB,CAAC;AAExB,MAAM,WAAW,YAAa,SAAQ,GAAG;IACvC,sDAAsD;IACtD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iEAAiE;IACjE,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,sDAAsD;IACtD,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,kFAAkF;AAClF,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,OAAO,CAAC;IACzB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,kBAAkB,EAAE,OAAO,CAAA;KAAE,CAAC;CACnD;AAED,MAAM,WAAW,iBAAiB;IAChC,2CAA2C;IAC3C,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,YAAY,EACjB,IAAI,GAAE,iBAAsB,GAC3B,gBAAgB,CAuBlB"}
@@ -0,0 +1,34 @@
1
+ // Serverless-safe pool options for mysql2.
2
+ //
3
+ // Returns a PLAIN options object — you pass it to your own
4
+ // `mysql.createPool(...)`, so this module has zero runtime dependencies and is
5
+ // not pinned to any mysql2 version.
6
+ import { decodeCaBase64, firstEnv, resolvePoolLimit, } from "../core/env.js";
7
+ /**
8
+ * Build serverless-safe mysql2 pool options.
9
+ *
10
+ * - `connectionLimit` defaults to 1 (one connection per warm lambda). Raise it
11
+ * only behind a connection pooler or on a plan with headroom, via
12
+ * `DATABASE_POOL_LIMIT`.
13
+ * - `maxIdle: 1` + `idleTimeout` lets idle lambdas release their connection so
14
+ * they don't sit on a slot of a tiny `max_connections` budget.
15
+ * - TLS is enabled with strict verification when `DATABASE_SSL_CA_BASE64` is set.
16
+ */
17
+ export function buildMysqlPoolOptions(env, opts = {}) {
18
+ const uri = firstEnv(env, "DATABASE_URL", "MYSQL_URL");
19
+ if (!uri) {
20
+ throw new Error("lambda-pool: DATABASE_URL (or MYSQL_URL) is not set — cannot build a MySQL pool.");
21
+ }
22
+ const ssl = decodeCaBase64(env.DATABASE_SSL_CA_BASE64);
23
+ return {
24
+ uri,
25
+ connectionLimit: resolvePoolLimit(env.DATABASE_POOL_LIMIT, opts.defaultPoolLimit ?? 1),
26
+ maxIdle: 1,
27
+ idleTimeout: 30_000,
28
+ enableKeepAlive: true,
29
+ waitForConnections: true,
30
+ queueLimit: 0,
31
+ ...(ssl ? { ssl } : {}),
32
+ };
33
+ }
34
+ //# sourceMappingURL=mysql.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mysql.js","sourceRoot":"","sources":["../../src/adapters/mysql.ts"],"names":[],"mappings":"AAAA,2CAA2C;AAC3C,EAAE;AACF,2DAA2D;AAC3D,+EAA+E;AAC/E,oCAAoC;AAEpC,OAAO,EAEL,cAAc,EACd,QAAQ,EACR,gBAAgB,GACjB,MAAM,gBAAgB,CAAC;AA6BxB;;;;;;;;;GASG;AACH,MAAM,UAAU,qBAAqB,CACnC,GAAiB,EACjB,OAA0B,EAAE;IAE5B,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,EAAE,cAAc,EAAE,WAAW,CAAC,CAAC;IACvD,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,KAAK,CACb,kFAAkF,CACnF,CAAC;IACJ,CAAC;IAED,MAAM,GAAG,GAAG,cAAc,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC;IAEvD,OAAO;QACL,GAAG;QACH,eAAe,EAAE,gBAAgB,CAC/B,GAAG,CAAC,mBAAmB,EACvB,IAAI,CAAC,gBAAgB,IAAI,CAAC,CAC3B;QACD,OAAO,EAAE,CAAC;QACV,WAAW,EAAE,MAAM;QACnB,eAAe,EAAE,IAAI;QACrB,kBAAkB,EAAE,IAAI;QACxB,UAAU,EAAE,CAAC;QACb,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACxB,CAAC;AACJ,CAAC"}
@@ -0,0 +1,46 @@
1
+ import { type Env } from "../core/env.ts";
2
+ export interface PgPoolEnv extends Env {
3
+ /** Connection URI (postgres://...). Aliases: POSTGRES_URL, PG_URL. */
4
+ DATABASE_URL?: string;
5
+ POSTGRES_URL?: string;
6
+ PG_URL?: string;
7
+ /** Base64-encoded CA cert — when present, enables strict TLS. */
8
+ DATABASE_SSL_CA_BASE64?: string;
9
+ /** Per-instance pool size override; defaults to 1. */
10
+ DATABASE_POOL_LIMIT?: string;
11
+ }
12
+ /** Shape compatible with pg `PoolConfig` (structural — no import needed). */
13
+ export interface PgPoolOptions {
14
+ connectionString: string;
15
+ /** pg calls this `max` (mysql2 calls it `connectionLimit`). */
16
+ max: number;
17
+ /** Close a connection after it sits idle this long (ms). 0 disables. */
18
+ idleTimeoutMillis: number;
19
+ /** Fail fast instead of hanging when the DB has no free slots (ms). */
20
+ connectionTimeoutMillis: number;
21
+ /** Recycle a connection after this many uses (0 = never). */
22
+ maxUses: number;
23
+ /** Don't keep the event loop alive for idle clients. */
24
+ allowExitOnIdle: boolean;
25
+ ssl?: {
26
+ ca: string;
27
+ rejectUnauthorized: boolean;
28
+ };
29
+ }
30
+ export interface BuildPgOptions {
31
+ /** Override the default pool size of 1. */
32
+ defaultPoolLimit?: number;
33
+ }
34
+ /**
35
+ * Build serverless-safe `pg` pool options.
36
+ *
37
+ * - `max` defaults to 1 (one connection per warm lambda). Raise it only behind
38
+ * a pooler (PgBouncer / Neon pooled endpoint) via `DATABASE_POOL_LIMIT`.
39
+ * - `idleTimeoutMillis` + `allowExitOnIdle` let an idle lambda release its slot
40
+ * of a tiny `max_connections` budget and not pin the process open.
41
+ * - `connectionTimeoutMillis` makes "too many clients" surface as a fast error
42
+ * rather than a hung request.
43
+ * - TLS is enabled with strict verification when `DATABASE_SSL_CA_BASE64` is set.
44
+ */
45
+ export declare function buildPgPoolOptions(env: PgPoolEnv, opts?: BuildPgOptions): PgPoolOptions;
46
+ //# sourceMappingURL=pg.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pg.d.ts","sourceRoot":"","sources":["../../src/adapters/pg.ts"],"names":[],"mappings":"AAMA,OAAO,EACL,KAAK,GAAG,EAIT,MAAM,gBAAgB,CAAC;AAExB,MAAM,WAAW,SAAU,SAAQ,GAAG;IACpC,sEAAsE;IACtE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,sDAAsD;IACtD,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,6EAA6E;AAC7E,MAAM,WAAW,aAAa;IAC5B,gBAAgB,EAAE,MAAM,CAAC;IACzB,+DAA+D;IAC/D,GAAG,EAAE,MAAM,CAAC;IACZ,wEAAwE;IACxE,iBAAiB,EAAE,MAAM,CAAC;IAC1B,uEAAuE;IACvE,uBAAuB,EAAE,MAAM,CAAC;IAChC,6DAA6D;IAC7D,OAAO,EAAE,MAAM,CAAC;IAChB,wDAAwD;IACxD,eAAe,EAAE,OAAO,CAAC;IACzB,GAAG,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,kBAAkB,EAAE,OAAO,CAAA;KAAE,CAAC;CACnD;AAED,MAAM,WAAW,cAAc;IAC7B,2CAA2C;IAC3C,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,SAAS,EACd,IAAI,GAAE,cAAmB,GACxB,aAAa,CAwBf"}
@@ -0,0 +1,34 @@
1
+ // Serverless-safe pool options for node-postgres (`pg`).
2
+ //
3
+ // Returns a PLAIN options object — you pass it to your own `new pg.Pool(...)`,
4
+ // so this module has zero runtime dependencies and is not pinned to any pg
5
+ // version.
6
+ import { decodeCaBase64, firstEnv, resolvePoolLimit, } from "../core/env.js";
7
+ /**
8
+ * Build serverless-safe `pg` pool options.
9
+ *
10
+ * - `max` defaults to 1 (one connection per warm lambda). Raise it only behind
11
+ * a pooler (PgBouncer / Neon pooled endpoint) via `DATABASE_POOL_LIMIT`.
12
+ * - `idleTimeoutMillis` + `allowExitOnIdle` let an idle lambda release its slot
13
+ * of a tiny `max_connections` budget and not pin the process open.
14
+ * - `connectionTimeoutMillis` makes "too many clients" surface as a fast error
15
+ * rather than a hung request.
16
+ * - TLS is enabled with strict verification when `DATABASE_SSL_CA_BASE64` is set.
17
+ */
18
+ export function buildPgPoolOptions(env, opts = {}) {
19
+ const connectionString = firstEnv(env, "DATABASE_URL", "POSTGRES_URL", "PG_URL");
20
+ if (!connectionString) {
21
+ throw new Error("lambda-pool: DATABASE_URL (or POSTGRES_URL / PG_URL) is not set — cannot build a Postgres pool.");
22
+ }
23
+ const ssl = decodeCaBase64(env.DATABASE_SSL_CA_BASE64);
24
+ return {
25
+ connectionString,
26
+ max: resolvePoolLimit(env.DATABASE_POOL_LIMIT, opts.defaultPoolLimit ?? 1),
27
+ idleTimeoutMillis: 30_000,
28
+ connectionTimeoutMillis: 10_000,
29
+ maxUses: 7_500,
30
+ allowExitOnIdle: true,
31
+ ...(ssl ? { ssl } : {}),
32
+ };
33
+ }
34
+ //# sourceMappingURL=pg.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pg.js","sourceRoot":"","sources":["../../src/adapters/pg.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,EAAE;AACF,+EAA+E;AAC/E,2EAA2E;AAC3E,WAAW;AAEX,OAAO,EAEL,cAAc,EACd,QAAQ,EACR,gBAAgB,GACjB,MAAM,gBAAgB,CAAC;AAkCxB;;;;;;;;;;GAUG;AACH,MAAM,UAAU,kBAAkB,CAChC,GAAc,EACd,OAAuB,EAAE;IAEzB,MAAM,gBAAgB,GAAG,QAAQ,CAC/B,GAAG,EACH,cAAc,EACd,cAAc,EACd,QAAQ,CACT,CAAC;IACF,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CACb,iGAAiG,CAClG,CAAC;IACJ,CAAC;IAED,MAAM,GAAG,GAAG,cAAc,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC;IAEvD,OAAO;QACL,gBAAgB;QAChB,GAAG,EAAE,gBAAgB,CAAC,GAAG,CAAC,mBAAmB,EAAE,IAAI,CAAC,gBAAgB,IAAI,CAAC,CAAC;QAC1E,iBAAiB,EAAE,MAAM;QACzB,uBAAuB,EAAE,MAAM;QAC/B,OAAO,EAAE,KAAK;QACd,eAAe,EAAE,IAAI;QACrB,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACxB,CAAC;AACJ,CAAC"}
@@ -0,0 +1,33 @@
1
+ import { type ProviderId } from "../core/providers.ts";
2
+ export type Severity = "error" | "warning" | "info";
3
+ export interface Diagnostic {
4
+ severity: Severity;
5
+ code: string;
6
+ message: string;
7
+ }
8
+ export interface DiagnoseInput {
9
+ /** The connection URI to analyze. */
10
+ url: string;
11
+ /** The pool size you intend to use (per warm instance). */
12
+ poolLimit: number;
13
+ /** Whether a CA cert was supplied out-of-band (DATABASE_SSL_CA_BASE64). */
14
+ hasCaCert?: boolean;
15
+ /** Peak warm instances, for budget checks. Defaults to a typical serverless fan-out. */
16
+ expectedInstances?: number;
17
+ }
18
+ export interface DiagnoseReport {
19
+ /** Password-redacted URL, safe to log alongside the report. */
20
+ safeUrl: string;
21
+ provider: ProviderId;
22
+ diagnostics: Diagnostic[];
23
+ /** True when no `error`-severity diagnostics are present. */
24
+ ok: boolean;
25
+ }
26
+ /**
27
+ * Analyze a connection config for serverless safety. Pure: returns a report,
28
+ * performs no I/O.
29
+ */
30
+ export declare function diagnose(input: DiagnoseInput): DiagnoseReport;
31
+ /** Format a report as plain text lines (for CLI / startup logs). */
32
+ export declare function formatReport(report: DiagnoseReport): string;
33
+ //# sourceMappingURL=diagnostics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diagnostics.d.ts","sourceRoot":"","sources":["../../src/application/diagnostics.ts"],"names":[],"mappings":"AAQA,OAAO,EAAoC,KAAK,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAGzF,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;AAEpD,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,QAAQ,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,qCAAqC;IACrC,GAAG,EAAE,MAAM,CAAC;IACZ,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB,2EAA2E;IAC3E,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,wFAAwF;IACxF,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,cAAc;IAC7B,+DAA+D;IAC/D,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,UAAU,CAAC;IACrB,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,6DAA6D;IAC7D,EAAE,EAAE,OAAO,CAAC;CACb;AAID;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,aAAa,GAAG,cAAc,CAiF7D;AAED,oEAAoE;AACpE,wBAAgB,YAAY,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAM3D"}