@effect/sql-pg 4.0.0-rc.111 → 4.0.0-rc.113

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 (60) hide show
  1. package/AGENTS.md +24 -9
  2. package/CLAUDE.md +24 -9
  3. package/ai-docs/package.json +2 -2
  4. package/ai-docs/src/01_effect/01_basics/02_effect-fn.ts +18 -5
  5. package/ai-docs/src/01_effect/01_basics/index.md +5 -3
  6. package/ai-docs/src/01_effect/03_services/20_layer-composition.ts +1 -1
  7. package/ai-docs/src/01_effect/03_services/20_layer-unwrap.ts +2 -2
  8. package/ai-docs/src/01_effect/05_resources/10_acquire-release.ts +2 -2
  9. package/ai-docs/src/03_stream/30_encoding.ts +5 -7
  10. package/ai-docs/src/08_observability/10_logging.ts +1 -1
  11. package/ai-docs/src/70_cli/10_basics.ts +7 -7
  12. package/ai-docs/src/71_ai/10_language-model.ts +2 -2
  13. package/ai-docs/src/71_ai/20_tools.ts +1 -1
  14. package/ai-docs/src/71_ai/30_chat.ts +1 -1
  15. package/dist/PgAuth.d.ts +93 -0
  16. package/dist/PgAuth.d.ts.map +1 -0
  17. package/dist/PgAuth.js +229 -0
  18. package/dist/PgAuth.js.map +1 -0
  19. package/dist/PgClient.d.ts +49 -82
  20. package/dist/PgClient.d.ts.map +1 -1
  21. package/dist/PgClient.js +58 -510
  22. package/dist/PgClient.js.map +1 -1
  23. package/dist/PgConnection.d.ts +177 -0
  24. package/dist/PgConnection.d.ts.map +1 -0
  25. package/dist/PgConnection.js +1786 -0
  26. package/dist/PgConnection.js.map +1 -0
  27. package/dist/PgPool.d.ts +107 -0
  28. package/dist/PgPool.d.ts.map +1 -0
  29. package/dist/PgPool.js +124 -0
  30. package/dist/PgPool.js.map +1 -0
  31. package/dist/PgProtocol.d.ts +783 -0
  32. package/dist/PgProtocol.d.ts.map +1 -0
  33. package/dist/PgProtocol.js +1202 -0
  34. package/dist/PgProtocol.js.map +1 -0
  35. package/dist/PgTypes.d.ts +429 -0
  36. package/dist/PgTypes.d.ts.map +1 -0
  37. package/dist/PgTypes.js +1511 -0
  38. package/dist/PgTypes.js.map +1 -0
  39. package/dist/index.d.ts +20 -0
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +20 -0
  42. package/dist/index.js.map +1 -1
  43. package/dist/internal/connection.d.ts +2 -0
  44. package/dist/internal/connection.d.ts.map +1 -0
  45. package/dist/internal/connection.js +5 -0
  46. package/dist/internal/connection.js.map +1 -0
  47. package/dist/internal/sqlError.d.ts +10 -0
  48. package/dist/internal/sqlError.d.ts.map +1 -0
  49. package/dist/internal/sqlError.js +57 -0
  50. package/dist/internal/sqlError.js.map +1 -0
  51. package/package.json +8 -12
  52. package/src/PgAuth.ts +318 -0
  53. package/src/PgClient.ts +138 -681
  54. package/src/PgConnection.ts +2294 -0
  55. package/src/PgPool.ts +231 -0
  56. package/src/PgProtocol.ts +1863 -0
  57. package/src/PgTypes.ts +1998 -0
  58. package/src/index.ts +25 -0
  59. package/src/internal/connection.ts +26 -0
  60. package/src/internal/sqlError.ts +75 -0
package/src/PgPool.ts ADDED
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Pools of native `PgConnection` sessions.
3
+ *
4
+ * @since 4.0.0
5
+ */
6
+ import * as Clock from "effect/Clock"
7
+ import * as Context from "effect/Context"
8
+ import * as Duration from "effect/Duration"
9
+ import * as Effect from "effect/Effect"
10
+ import * as Pool from "effect/Pool"
11
+ import type * as Scope from "effect/Scope"
12
+ import type { SqlError } from "effect/unstable/sql/SqlError"
13
+ import { connectionInternals } from "./internal/connection.ts"
14
+ import * as PgConnection from "./PgConnection.ts"
15
+
16
+ const defaultMultiplexConcurrency = 32
17
+
18
+ /**
19
+ * The runtime type identifier for `PgPool`.
20
+ *
21
+ * @category type IDs
22
+ * @since 4.0.0
23
+ */
24
+ export const TypeId: TypeId = "~@effect/sql-pg/PgPool"
25
+
26
+ /**
27
+ * The type-level identifier for `PgPool`.
28
+ *
29
+ * @category type IDs
30
+ * @since 4.0.0
31
+ */
32
+ export type TypeId = "~@effect/sql-pg/PgPool"
33
+
34
+ /**
35
+ * Connection and sizing settings for a PostgreSQL session pool.
36
+ *
37
+ * **Details**
38
+ *
39
+ * The defaults are 0 to 10 connections and a 10-second idle timeout.
40
+ * `connectionTTL` replaces connections that exceed the configured lifetime.
41
+ * Every connection is used at least once, so a TTL of zero disables reuse.
42
+ *
43
+ * With `multiplex` enabled, fibers may share pooled connections for pipelined
44
+ * queries. Reserved connections remain exclusive. Without multiplexing, every
45
+ * checkout is exclusive.
46
+ *
47
+ * @category models
48
+ * @since 4.0.0
49
+ */
50
+ export interface Config extends PgConnection.Config {
51
+ readonly idleTimeout?: Duration.Input | undefined
52
+ readonly maxConnections?: number | undefined
53
+ readonly minConnections?: number | undefined
54
+ readonly connectionTTL?: Duration.Input | undefined
55
+ /**
56
+ * How many statements may share one connection when `multiplex` is on.
57
+ * Defaults to `32`. Statements are pipelined into one write, so a higher
58
+ * number means fewer round trips, but it also means a slow statement holds
59
+ * up more of the statements queued behind it.
60
+ */
61
+ readonly multiplexConcurrency?: number | undefined
62
+ }
63
+
64
+ /**
65
+ * A PostgreSQL session pool.
66
+ *
67
+ * @category models
68
+ * @since 4.0.0
69
+ */
70
+ export interface PgPool {
71
+ readonly [TypeId]: TypeId
72
+ readonly config: Config
73
+ /**
74
+ * Checks out a session until the scope closes. Without multiplexing the
75
+ * checkout is exclusive. With multiplexing the
76
+ * session may be shared with other fibers, so multi-statement work should
77
+ * use `reserve` instead.
78
+ */
79
+ readonly get: Effect.Effect<PgConnection.PgConnection, SqlError, Scope.Scope>
80
+ /**
81
+ * Checks out a session for exclusive use until the scope closes. Use this for
82
+ * transactions and listeners on a multiplexed pool.
83
+ */
84
+ readonly reserve: Effect.Effect<PgConnection.PgConnection, SqlError, Scope.Scope>
85
+ /**
86
+ * Removes a session so the pool can replace it. Fatal protocol and socket
87
+ * errors invalidate sessions automatically.
88
+ */
89
+ /**
90
+ * Lends a session for the duration of one effect and takes it back on any
91
+ * exit, without opening a scope for it.
92
+ *
93
+ * **Details**
94
+ *
95
+ * For work that finishes with the effect that runs it. A lease that has to
96
+ * outlive its effect - a stream, a transaction - takes `get` or `reserve`.
97
+ */
98
+ readonly use: <A, E, R>(
99
+ f: (connection: PgConnection.PgConnection) => Effect.Effect<A, E, R>
100
+ ) => Effect.Effect<A, E | SqlError, R>
101
+ readonly invalidate: (connection: PgConnection.PgConnection) => Effect.Effect<void>
102
+ }
103
+
104
+ /**
105
+ * The service tag for `PgPool`.
106
+ *
107
+ * @category services
108
+ * @since 4.0.0
109
+ */
110
+ export const PgPool = Context.Service<PgPool>("@effect/sql-pg/PgPool")
111
+
112
+ /**
113
+ * Creates a scoped PostgreSQL session pool.
114
+ *
115
+ * **Details**
116
+ *
117
+ * Connections are opened lazily up to `maxConnections` and released down to
118
+ * `minConnections` after `idleTimeout` without use. Closing the scope shuts
119
+ * the pool down and releases every session.
120
+ *
121
+ * @category constructors
122
+ * @since 4.0.0
123
+ */
124
+ export const make = Effect.fnUntraced(function*(options: Config): Effect.fn.Return<PgPool, SqlError, Scope.Scope> {
125
+ const clock = yield* Clock.Clock
126
+ const runFork = Effect.runForkWith(yield* Effect.context())
127
+
128
+ const multiplex = options.multiplex ?? false
129
+ const connectionTTL = options.connectionTTL !== undefined
130
+ ? Duration.toMillis(Duration.fromInputUnsafe(options.connectionTTL))
131
+ : undefined
132
+ const deadConnections = new Set<PgConnection.PgConnection>()
133
+ const createdAt = new WeakMap<PgConnection.PgConnection, number>()
134
+ const checkedOut = new WeakSet<PgConnection.PgConnection>()
135
+
136
+ // Assigned below, once the pool exists. `acquire` only runs when the pool
137
+ // opens a connection, which is always after that.
138
+ let pool: Pool.Pool<PgConnection.PgConnection, SqlError>
139
+ const acquire = Effect.tap(PgConnection.make(options), (connection) =>
140
+ Effect.sync(() => {
141
+ createdAt.set(connection, clock.currentTimeMillisUnsafe())
142
+ const internals = connectionInternals(connection)
143
+ internals.fatalHooks.add(() => {
144
+ deadConnections.add(connection)
145
+ // `deadConnections` is only read by the next checkout, and a checkout
146
+ // already waiting for this connection would never get that far. Tell
147
+ // the pool now so it can replace the connection and admit them.
148
+ runFork(Pool.invalidate(pool, connection))
149
+ })
150
+ // Pinning a shared session has to take it out of circulation, or a
151
+ // second checkout lands on the connection its own stream is holding.
152
+ if (multiplex) internals.reserve = Pool.reserve(pool, connection)
153
+ }))
154
+
155
+ const maxConnections = options.maxConnections ?? 10
156
+ pool = yield* Pool.makeWithTTL({
157
+ acquire,
158
+ min: options.minConnections ?? 0,
159
+ max: maxConnections,
160
+ concurrency: multiplex
161
+ ? Math.max(1, options.multiplexConcurrency ?? defaultMultiplexConcurrency)
162
+ : 1,
163
+ timeToLive: options.idleTimeout ?? Duration.seconds(10),
164
+ timeToLiveStrategy: "usage"
165
+ })
166
+
167
+ const expired = (connection: PgConnection.PgConnection): boolean => {
168
+ if (connectionInternals(connection).deadError() !== undefined) return true
169
+ if (connectionTTL === undefined) return false
170
+ if (!checkedOut.has(connection)) {
171
+ checkedOut.add(connection)
172
+ return false
173
+ }
174
+ const openedAt = createdAt.get(connection)
175
+ return openedAt !== undefined && clock.currentTimeMillisUnsafe() - openedAt >= connectionTTL
176
+ }
177
+
178
+ // A checkout runs per statement, so the case where nothing has died and the
179
+ // first connection is usable stays a plain flatMap; retrying is the
180
+ // exception and pays for the loop.
181
+ const retry: Effect.Effect<PgConnection.PgConnection, SqlError, Scope.Scope> = Effect.gen(function*() {
182
+ while (true) {
183
+ if (deadConnections.size > 0) {
184
+ const dead = Array.from(deadConnections)
185
+ deadConnections.clear()
186
+ yield* Effect.forEach(dead, (connection) => Pool.invalidate(pool, connection), { discard: true })
187
+ }
188
+ const connection = yield* Pool.get(pool)
189
+ if (expired(connection)) {
190
+ yield* Pool.invalidate(pool, connection)
191
+ continue
192
+ }
193
+ return connection
194
+ }
195
+ })
196
+
197
+ const get: Effect.Effect<PgConnection.PgConnection, SqlError, Scope.Scope> = Effect.suspend(() =>
198
+ deadConnections.size > 0 ? retry : Effect.flatMap(Pool.get(pool), (connection) =>
199
+ expired(connection)
200
+ ? Effect.andThen(Pool.invalidate(pool, connection), retry)
201
+ : Effect.succeed(connection))
202
+ )
203
+
204
+ // `pin` reserves the pool item itself, so this needs no help.
205
+ const reserve = Effect.flatMap(get, (connection) => connection.pin)
206
+
207
+ // `Pool.use` cannot check the session it hands over before running the
208
+ // effect, so it is only taken when there is nothing to check: no session is
209
+ // known dead, and no lifetime can have run out. Otherwise the scoped
210
+ // checkout does its replacement pass first. Checking inside the callback
211
+ // instead would hold one lease while acquiring another, which deadlocks a
212
+ // pool of one.
213
+ const use = <A, E, R>(
214
+ f: (connection: PgConnection.PgConnection) => Effect.Effect<A, E, R>
215
+ ): Effect.Effect<A, E | SqlError, R> =>
216
+ Effect.suspend(() =>
217
+ connectionTTL === undefined && deadConnections.size === 0
218
+ ? Pool.use(pool, f)
219
+ : Effect.scoped(Effect.flatMap(get, f))
220
+ )
221
+
222
+ const pgPool: PgPool = {
223
+ [TypeId]: TypeId,
224
+ config: options,
225
+ get,
226
+ reserve,
227
+ use,
228
+ invalidate: (connection) => Pool.invalidate(pool, connectionInternals(connection).base as PgConnection.PgConnection)
229
+ }
230
+ return pgPool
231
+ })