@porulle/adapter-neon 0.10.4 → 0.10.6

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
@@ -7,11 +7,11 @@ porulle's first adopter (a live iPad POS on Cloudflare Workers + Neon):
7
7
 
8
8
  1. **Plain queries** go through `@neondatabase/serverless` HTTP — stateless,
9
9
  no socket-reuse races across Workers isolates.
10
- 2. **`transaction()`** creates a fresh WebSocket `Pool` per call, runs the
11
- transaction, then ends the pool. `drizzle-orm/neon-http` cannot run
12
- transactions, and isolate-shared WebSocket pools flake when reused across
13
- requests; a short-lived pool per transaction gives atomicity without the
14
- flake.
10
+ 2. **`transaction()`** creates a fresh client per call and closes it before the
11
+ request completes. Direct Neon connections use a WebSocket `Pool`;
12
+ Hyperdrive connections use Postgres.js over Workers TCP. This distinction is
13
+ required because Neon WebSocket clients cannot speak to Hyperdrive's TCP
14
+ endpoint. `drizzle-orm/neon-http` cannot run interactive transactions.
15
15
 
16
16
  ## Usage
17
17
 
@@ -22,7 +22,7 @@ import { neonAdapter } from "@porulle/adapter-neon";
22
22
  export default defineConfig({
23
23
  databaseAdapter: neonAdapter({
24
24
  connectionString: env.DATABASE_URL, // direct Neon URL
25
- // Optional: route per-transaction pools through Hyperdrive
25
+ // Optional: route transactions through Hyperdrive using Postgres.js
26
26
  hyperdrive: env.HYPERDRIVE,
27
27
  }),
28
28
  // ...
@@ -33,8 +33,9 @@ export default defineConfig({
33
33
  Used by the HTTP driver, and by transaction pools when no Hyperdrive
34
34
  binding is given.
35
35
  - `hyperdrive` — optional Cloudflare Hyperdrive binding (any object exposing
36
- `connectionString`). When set, per-transaction pools connect through it;
37
- plain queries keep using the Neon HTTP driver against `connectionString`.
36
+ `connectionString`). When set, transactions use a fresh Postgres.js client
37
+ over Hyperdrive; plain queries keep using Neon HTTP against
38
+ `connectionString`.
38
39
 
39
40
  `.execute()` results are normalized to the postgres-js shape (an array of
40
41
  rows), matching what `@porulle/core` and custom routes expect.
package/dist/index.d.ts CHANGED
@@ -1,16 +1,18 @@
1
1
  import { type NeonHttpDatabase } from "drizzle-orm/neon-http";
2
2
  import { type NeonDatabase } from "drizzle-orm/neon-serverless";
3
+ import { type PostgresJsDatabase } from "drizzle-orm/postgres-js";
3
4
  import type { DatabaseAdapter } from "@porulle/core";
4
5
  type HttpClient = NeonHttpDatabase<Record<string, unknown>>;
5
6
  type WsClient = NeonDatabase<Record<string, unknown>>;
6
- type AnyDb = HttpClient | WsClient;
7
+ type PgClient = PostgresJsDatabase<Record<string, never>>;
8
+ type AnyDb = HttpClient | WsClient | PgClient;
7
9
  export interface NeonAdapterOptions {
8
10
  /** Direct Neon connection string (postgresql://...neon.tech/...). */
9
11
  connectionString: string;
10
12
  /**
11
13
  * Optional Cloudflare Hyperdrive binding (or any object exposing
12
- * `connectionString`). When set, per-transaction pools connect through it;
13
- * plain queries keep using the Neon HTTP driver against `connectionString`.
14
+ * `connectionString`). When set, transactions use a fresh Postgres.js TCP
15
+ * client through Hyperdrive; plain queries keep using Neon HTTP directly.
14
16
  */
15
17
  hyperdrive?: {
16
18
  connectionString: string;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAqBA,OAAO,EAA0B,KAAK,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACtF,OAAO,EAAwB,KAAK,YAAY,EAAE,MAAM,6BAA6B,CAAC;AACtF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAMrD,KAAK,UAAU,GAAG,gBAAgB,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAC5D,KAAK,QAAQ,GAAG,YAAY,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AACtD,KAAK,KAAK,GAAG,UAAU,GAAG,QAAQ,CAAC;AAEnC,MAAM,WAAW,kBAAkB;IACjC,qEAAqE;IACrE,gBAAgB,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,UAAU,CAAC,EAAE;QAAE,gBAAgB,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;CACvD;AAED,MAAM,MAAM,mBAAmB,GAAG,eAAe,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;AAEvE;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,SAAS,KAAK,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,CAsB/D;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,mBAAmB,CAsC5E"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAoBA,OAAO,EAA0B,KAAK,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACtF,OAAO,EAAwB,KAAK,YAAY,EAAE,MAAM,6BAA6B,CAAC;AACtF,OAAO,EAAwB,KAAK,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAExF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAMrD,KAAK,UAAU,GAAG,gBAAgB,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAC5D,KAAK,QAAQ,GAAG,YAAY,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AACtD,KAAK,QAAQ,GAAG,kBAAkB,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC;AAC1D,KAAK,KAAK,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE9C,MAAM,WAAW,kBAAkB;IACjC,qEAAqE;IACrE,gBAAgB,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,UAAU,CAAC,EAAE;QAAE,gBAAgB,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;CACvD;AAED,MAAM,MAAM,mBAAmB,GAAG,eAAe,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;AAEvE;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,SAAS,KAAK,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,CAsB/D;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,mBAAmB,CA0D5E"}
package/dist/index.js CHANGED
@@ -7,20 +7,21 @@
7
7
  * 1. Plain queries (select / insert / update / delete / raw execute) go
8
8
  * through `@neondatabase/serverless` HTTP — stateless, no socket-reuse
9
9
  * races across Workers isolates.
10
- * 2. `transaction()` creates a FRESH WebSocket `Pool` per call, runs the
11
- * transaction, then ends the pool. `drizzle-orm/neon-http` throws on
12
- * `db.transaction()` ("No transactions support"), and isolate-shared
13
- * WebSocket pools flake (~30% observed) when reused across requests —
14
- * a short-lived pool per transaction gives atomicity without the flake.
10
+ * 2. `transaction()` creates a FRESH client per call. Direct Neon uses its
11
+ * WebSocket `Pool`; Hyperdrive uses Postgres.js over Workers TCP. Both
12
+ * clients are closed before the request completes. `drizzle-orm/neon-http`
13
+ * throws on `db.transaction()` ("No transactions support").
15
14
  *
16
15
  * Hyperdrive-aware: pass the binding (`{ hyperdrive: env.HYPERDRIVE }`) and
17
- * its connection string is used for the per-transaction pools, keeping pool
18
- * setup on Cloudflare's fast path. The HTTP driver always speaks directly to
19
- * Neon, so a direct `connectionString` is still required alongside it.
16
+ * its TCP connection string is used by Postgres.js for transactions. A Neon
17
+ * WebSocket client cannot speak to Hyperdrive's TCP endpoint. The HTTP driver
18
+ * always speaks directly to Neon, so a direct `connectionString` is required.
20
19
  */
21
20
  import { Pool, neon, neonConfig } from "@neondatabase/serverless";
22
21
  import { drizzle as drizzleHttp } from "drizzle-orm/neon-http";
23
22
  import { drizzle as drizzleWs } from "drizzle-orm/neon-serverless";
23
+ import { drizzle as drizzlePg } from "drizzle-orm/postgres-js";
24
+ import postgres from "postgres";
24
25
  if (typeof WebSocket !== "undefined") {
25
26
  neonConfig.webSocketConstructor = WebSocket;
26
27
  }
@@ -52,11 +53,10 @@ export function normalizeExecuteShape(db) {
52
53
  }
53
54
  export function neonAdapter(options) {
54
55
  const httpConnectionString = options.connectionString;
55
- const poolConnectionString = options.hyperdrive?.connectionString ?? options.connectionString;
56
56
  const sql = neon(httpConnectionString);
57
57
  const httpDb = normalizeExecuteShape(drizzleHttp(sql));
58
- const runInFreshPool = async (fn) => {
59
- const pool = new Pool({ connectionString: poolConnectionString });
58
+ const runInFreshNeonPool = async (fn) => {
59
+ const pool = new Pool({ connectionString: options.connectionString });
60
60
  try {
61
61
  const wsDb = normalizeExecuteShape(drizzleWs(pool));
62
62
  return await wsDb.transaction(async (tx) => fn(normalizeExecuteShape(tx)));
@@ -67,13 +67,31 @@ export function neonAdapter(options) {
67
67
  await pool.end().catch(() => { });
68
68
  }
69
69
  };
70
+ const runInFreshHyperdriveClient = async (fn) => {
71
+ const client = postgres(options.hyperdrive.connectionString, {
72
+ max: 1,
73
+ prepare: false,
74
+ connect_timeout: 10,
75
+ idle_timeout: 5,
76
+ });
77
+ try {
78
+ const pgDb = normalizeExecuteShape(drizzlePg(client));
79
+ return await pgDb.transaction(async (tx) => fn(normalizeExecuteShape(tx)));
80
+ }
81
+ finally {
82
+ await client.end({ timeout: 1 }).catch(() => { });
83
+ }
84
+ };
85
+ const runTransaction = options.hyperdrive
86
+ ? runInFreshHyperdriveClient
87
+ : runInFreshNeonPool;
70
88
  // Some core paths call `kernel.database.db.transaction(...)` directly —
71
89
  // splice the pool-backed transaction onto the HTTP client so both entry
72
90
  // points behave identically.
73
91
  const dbWithTx = new Proxy(httpDb, {
74
92
  get(target, prop, receiver) {
75
93
  if (prop === "transaction") {
76
- return runInFreshPool;
94
+ return runTransaction;
77
95
  }
78
96
  return Reflect.get(target, prop, receiver);
79
97
  },
@@ -82,7 +100,7 @@ export function neonAdapter(options) {
82
100
  provider: "postgresql",
83
101
  db: dbWithTx,
84
102
  async transaction(fn) {
85
- return runInFreshPool(fn);
103
+ return runTransaction(fn);
86
104
  },
87
105
  };
88
106
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@porulle/adapter-neon",
3
- "version": "0.10.4",
3
+ "version": "0.10.6",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -13,7 +13,8 @@
13
13
  "dependencies": {
14
14
  "@neondatabase/serverless": "^1.0.1",
15
15
  "drizzle-orm": "^0.45.1",
16
- "@porulle/core": "0.10.4"
16
+ "postgres": "^3.4.7",
17
+ "@porulle/core": "0.10.6"
17
18
  },
18
19
  "devDependencies": {
19
20
  "@types/node": "^24.5.2",
@@ -31,7 +32,7 @@
31
32
  "dist",
32
33
  "README.md"
33
34
  ],
34
- "description": "Workers-grade Neon DatabaseAdapter for @porulle/core: HTTP driver for plain queries, a fresh WebSocket Pool per transaction, Hyperdrive-aware.",
35
+ "description": "Workers-grade Neon DatabaseAdapter for @porulle/core: Neon HTTP queries with request-scoped Neon or Hyperdrive transactions.",
35
36
  "homepage": "https://porulle-docs.vercel.app",
36
37
  "bugs": {
37
38
  "url": "https://github.com/asyncdotengineering/porulle/issues"
package/src/index.ts CHANGED
@@ -7,20 +7,21 @@
7
7
  * 1. Plain queries (select / insert / update / delete / raw execute) go
8
8
  * through `@neondatabase/serverless` HTTP — stateless, no socket-reuse
9
9
  * races across Workers isolates.
10
- * 2. `transaction()` creates a FRESH WebSocket `Pool` per call, runs the
11
- * transaction, then ends the pool. `drizzle-orm/neon-http` throws on
12
- * `db.transaction()` ("No transactions support"), and isolate-shared
13
- * WebSocket pools flake (~30% observed) when reused across requests —
14
- * a short-lived pool per transaction gives atomicity without the flake.
10
+ * 2. `transaction()` creates a FRESH client per call. Direct Neon uses its
11
+ * WebSocket `Pool`; Hyperdrive uses Postgres.js over Workers TCP. Both
12
+ * clients are closed before the request completes. `drizzle-orm/neon-http`
13
+ * throws on `db.transaction()` ("No transactions support").
15
14
  *
16
15
  * Hyperdrive-aware: pass the binding (`{ hyperdrive: env.HYPERDRIVE }`) and
17
- * its connection string is used for the per-transaction pools, keeping pool
18
- * setup on Cloudflare's fast path. The HTTP driver always speaks directly to
19
- * Neon, so a direct `connectionString` is still required alongside it.
16
+ * its TCP connection string is used by Postgres.js for transactions. A Neon
17
+ * WebSocket client cannot speak to Hyperdrive's TCP endpoint. The HTTP driver
18
+ * always speaks directly to Neon, so a direct `connectionString` is required.
20
19
  */
21
20
  import { Pool, neon, neonConfig } from "@neondatabase/serverless";
22
21
  import { drizzle as drizzleHttp, type NeonHttpDatabase } from "drizzle-orm/neon-http";
23
22
  import { drizzle as drizzleWs, type NeonDatabase } from "drizzle-orm/neon-serverless";
23
+ import { drizzle as drizzlePg, type PostgresJsDatabase } from "drizzle-orm/postgres-js";
24
+ import postgres from "postgres";
24
25
  import type { DatabaseAdapter } from "@porulle/core";
25
26
 
26
27
  if (typeof WebSocket !== "undefined") {
@@ -29,15 +30,16 @@ if (typeof WebSocket !== "undefined") {
29
30
 
30
31
  type HttpClient = NeonHttpDatabase<Record<string, unknown>>;
31
32
  type WsClient = NeonDatabase<Record<string, unknown>>;
32
- type AnyDb = HttpClient | WsClient;
33
+ type PgClient = PostgresJsDatabase<Record<string, never>>;
34
+ type AnyDb = HttpClient | WsClient | PgClient;
33
35
 
34
36
  export interface NeonAdapterOptions {
35
37
  /** Direct Neon connection string (postgresql://...neon.tech/...). */
36
38
  connectionString: string;
37
39
  /**
38
40
  * Optional Cloudflare Hyperdrive binding (or any object exposing
39
- * `connectionString`). When set, per-transaction pools connect through it;
40
- * plain queries keep using the Neon HTTP driver against `connectionString`.
41
+ * `connectionString`). When set, transactions use a fresh Postgres.js TCP
42
+ * client through Hyperdrive; plain queries keep using Neon HTTP directly.
41
43
  */
42
44
  hyperdrive?: { connectionString: string } | undefined;
43
45
  }
@@ -75,13 +77,12 @@ export function normalizeExecuteShape<T extends AnyDb>(db: T): T {
75
77
 
76
78
  export function neonAdapter(options: NeonAdapterOptions): NeonDatabaseAdapter {
77
79
  const httpConnectionString = options.connectionString;
78
- const poolConnectionString = options.hyperdrive?.connectionString ?? options.connectionString;
79
80
 
80
81
  const sql = neon(httpConnectionString);
81
82
  const httpDb = normalizeExecuteShape(drizzleHttp(sql) as HttpClient);
82
83
 
83
- const runInFreshPool = async (fn: (tx: unknown) => Promise<unknown>): Promise<unknown> => {
84
- const pool = new Pool({ connectionString: poolConnectionString });
84
+ const runInFreshNeonPool = async (fn: (tx: unknown) => Promise<unknown>): Promise<unknown> => {
85
+ const pool = new Pool({ connectionString: options.connectionString });
85
86
  try {
86
87
  const wsDb = normalizeExecuteShape(drizzleWs(pool) as WsClient);
87
88
  return await wsDb.transaction(async (tx) => fn(normalizeExecuteShape(tx as WsClient)));
@@ -92,13 +93,34 @@ export function neonAdapter(options: NeonAdapterOptions): NeonDatabaseAdapter {
92
93
  }
93
94
  };
94
95
 
96
+ const runInFreshHyperdriveClient = async (
97
+ fn: (tx: unknown) => Promise<unknown>,
98
+ ): Promise<unknown> => {
99
+ const client = postgres(options.hyperdrive!.connectionString, {
100
+ max: 1,
101
+ prepare: false,
102
+ connect_timeout: 10,
103
+ idle_timeout: 5,
104
+ });
105
+ try {
106
+ const pgDb = normalizeExecuteShape(drizzlePg(client) as PgClient);
107
+ return await pgDb.transaction(async (tx) => fn(normalizeExecuteShape(tx as PgClient)));
108
+ } finally {
109
+ await client.end({ timeout: 1 }).catch(() => {});
110
+ }
111
+ };
112
+
113
+ const runTransaction = options.hyperdrive
114
+ ? runInFreshHyperdriveClient
115
+ : runInFreshNeonPool;
116
+
95
117
  // Some core paths call `kernel.database.db.transaction(...)` directly —
96
118
  // splice the pool-backed transaction onto the HTTP client so both entry
97
119
  // points behave identically.
98
120
  const dbWithTx = new Proxy(httpDb, {
99
121
  get(target, prop, receiver) {
100
122
  if (prop === "transaction") {
101
- return runInFreshPool;
123
+ return runTransaction;
102
124
  }
103
125
  return Reflect.get(target, prop, receiver);
104
126
  },
@@ -108,7 +130,7 @@ export function neonAdapter(options: NeonAdapterOptions): NeonDatabaseAdapter {
108
130
  provider: "postgresql",
109
131
  db: dbWithTx,
110
132
  async transaction<T>(fn: (tx: unknown) => Promise<T>): Promise<T> {
111
- return runInFreshPool(fn) as Promise<T>;
133
+ return runTransaction(fn) as Promise<T>;
112
134
  },
113
135
  };
114
136
  }