@molecule/api-instance-leadership-postgresql 0.0.1-stage → 1.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.
package/README.md CHANGED
@@ -1,11 +1,287 @@
1
+ <!--
2
+ AUTO-GENERATED — DO NOT EDIT THIS FILE.
3
+ Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
4
+ Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
5
+ To change this document, edit the module-level JSDoc in src/index.ts.
6
+ Generated: 2026-10-19T22:02:10.972Z
7
+ -->
8
+
1
9
  # @molecule/api-instance-leadership-postgresql
2
10
 
3
- PostgreSQL provider for @molecule/api-instance-leadership: one leader across every API instance via a session advisory lock on a dedicated connection, with demotion when the connection is lost.
11
+ PostgreSQL provider for `@molecule/api-instance-leadership`: elect ONE API
12
+ instance to run platform-wide singleton work, using a PostgreSQL SESSION
13
+ advisory lock held on a dedicated connection.
14
+
15
+ ```typescript
16
+ import { acquireLeadership, setProvider } from '@molecule/api-instance-leadership'
17
+ import { provider } from '@molecule/api-instance-leadership-postgresql'
18
+
19
+ setProvider(provider) // reads DATABASE_URL lazily; nothing connects here
20
+ acquireLeadership({
21
+ onAcquired: () => startPoolManager(),
22
+ onLost: () => stopPoolManager(),
23
+ })
24
+ ```
25
+
26
+ Use this bond whenever the API runs as more than one process or Machine: a
27
+ host-local lock (a loopback port, a file) elects one leader PER HOST, so
28
+ every singleton — pool managers, hibernation, retention sweeps, status
29
+ recorders — runs once per Machine instead of once per platform.
30
+
31
+ > **Auto-generated, AI-first package reference** for the [molecule.dev](https://molecule.dev) ecosystem.
32
+ > It is written to be read by coding agents as much as by people, and is generated from this
33
+ > package's source — edit `src/index.ts` JSDoc, not this file.
34
+
35
+ ## Type
36
+
37
+ `provider`
38
+
39
+ ## Installation
40
+
41
+ ```bash
42
+ npm install @molecule/api-instance-leadership-postgresql @molecule/api-bond @molecule/api-instance-leadership pg
43
+ npm install -D @types/pg
44
+ ```
45
+
46
+ ## API
47
+
48
+ ### Interfaces
49
+
50
+ #### `PostgresLeaderLockOptions`
51
+
52
+ How to open the dedicated leadership connection.
53
+
54
+ ```typescript
55
+ interface PostgresLeaderLockOptions {
56
+ /**
57
+ * The lock id to hold. This is an arbitrary 64-bit integer that must be
58
+ * unique across everything sharing the database: leadership must never
59
+ * contend with a migration lock or another feature's advisory lock.
60
+ */
61
+ lockId: number
62
+
63
+ /** Connection string. Defaults to `process.env.DATABASE_URL`. */
64
+ connectionString?: string
4
65
 
66
+ /**
67
+ * TLS configuration passed to `pg`. Omit to let the driver decide from the
68
+ * connection string (`sslmode=require` still enables TLS).
69
+ */
70
+ ssl?: pg.ConnectionConfig['ssl']
71
+
72
+ /**
73
+ * Open a connected client. Injected by a host that owns its own connection
74
+ * settings (e.g. a platform API with a shared SSL derivation helper); the
75
+ * default creates a `pg.Client` from {@link PostgresLeaderLockOptions.connectionString}.
76
+ *
77
+ * @param connectionString - The resolved connection string.
78
+ * @param ssl - The configured TLS options.
79
+ * @returns A connected `pg.Client`.
80
+ */
81
+ connect?: (connectionString: string, ssl: pg.ConnectionConfig['ssl']) => Promise<pg.Client>
82
+ }
5
83
  ```
6
- npm install @molecule/api-instance-leadership-postgresql
84
+
85
+ #### `PostgresLeadershipProviderOptions`
86
+
87
+ How to build a leadership provider.
88
+
89
+ ```typescript
90
+ interface PostgresLeadershipProviderOptions {
91
+ /**
92
+ * Opens one dedicated lock session per attempt. Use
93
+ * {@link createPostgresLockFactory} for the production PostgreSQL session,
94
+ * or a fake that shares one "held by" flag in tests.
95
+ */
96
+ createSession: LeaderLockFactory
97
+
98
+ /**
99
+ * How often a follower retries, in milliseconds. A function keeps the value
100
+ * lazy so a host can read it from configuration after import. Defaults to
101
+ * 2000; values below 50 are clamped so a misconfiguration cannot spin.
102
+ */
103
+ retryMs?: number | (() => number)
104
+
105
+ /**
106
+ * The advisory lock id, used only in log lines (the factory owns the actual
107
+ * lock). Defaults to 0 when omitted.
108
+ */
109
+ lockId?: number
110
+ }
7
111
  ```
8
112
 
9
- Part of the [Molecule](https://www.molecule.dev) package fleet. This page carries the manifest summary; the full generated documentation ships with the package's first committed version.
113
+ ### Classes
114
+
115
+ #### `PgLeaderLockSession`
116
+
117
+ The production session: one `pg.Client`, one advisory lock.
118
+
119
+ Exported so a host that owns its own connection settings (the platform API's
120
+ SSL derivation, a Vault-backed URL resolver) can build a session directly and
121
+ pass it to the provider as its `createSession` factory.
122
+
123
+ ```typescript
124
+ class PgLeaderLockSession implements LeaderLockSession {
125
+ private lostHandler: (() => void) | null = null
126
+ private lost = false
127
+
128
+ /**
129
+ * Track the connection's lifecycle so a lost session demotes the leader.
130
+ *
131
+ * @param client - A connected client owned exclusively by this session.
132
+ * @param lockId - The advisory lock id to hold.
133
+ */
134
+ constructor(
135
+ private readonly client: pg.Client,
136
+ private readonly lockId: number,
137
+ ) {
138
+ const lost = (): void => {
139
+ if (this.lost) return
140
+ this.lost = true
141
+ this.lostHandler?.()
142
+ }
143
+ // `error` covers a socket/network failure; `end` covers the backend ending
144
+ // the session (a server restart, an idle-session kill). Either way the lock
145
+ // is gone.
146
+ this.client.on('error', (error) => {
147
+ getLogger().warn('Leadership lock connection error', { error })
148
+ lost()
149
+ })
150
+ this.client.on('end', lost)
151
+ }
152
+
153
+ /** {@inheritDoc LeaderLockSession.tryLock} */
154
+ async tryLock(): Promise<boolean> {
155
+ const { rows } = await this.client.query<{ locked: boolean }>(
156
+ 'SELECT pg_try_advisory_lock($1) AS locked',
157
+ [this.lockId],
158
+ )
159
+ return rows[0]?.locked === true
160
+ }
161
+
162
+ /** {@inheritDoc LeaderLockSession.release} */
163
+ async release(): Promise<void> {
164
+ if (!this.lost) {
165
+ try {
166
+ await this.client.query('SELECT pg_advisory_unlock($1)', [this.lockId])
167
+ } catch (error) {
168
+ // The session is ending either way, and a session lock is released by
169
+ // the backend when it ends — an unlock failure changes nothing.
170
+ getLogger().debug('Leadership advisory unlock failed; the session end releases it', {
171
+ error,
172
+ })
173
+ }
174
+ }
175
+ try {
176
+ await this.client.end()
177
+ } catch (_error) {
178
+ // Already ended.
179
+ }
180
+ }
181
+
182
+ /** {@inheritDoc LeaderLockSession.onLost} */
183
+ onLost(handler: () => void): void {
184
+ this.lostHandler = handler
185
+ }
186
+ }
187
+ ```
188
+
189
+ ### Functions
190
+
191
+ #### `createPostgresLockFactory(options)`
192
+
193
+ Builds a {@link LeaderLockFactory} that opens PostgreSQL session locks.
194
+
195
+ Each call to the factory opens a NEW dedicated connection — a follower
196
+ retries on its own connection, and a leader reconnects on a fresh one after
197
+ losing the old.
198
+
199
+ ```typescript
200
+ function createPostgresLockFactory(options: PostgresLeaderLockOptions): LeaderLockFactory
201
+ ```
202
+
203
+ - `options` — The lock id, connection settings and optional connect hook.
204
+
205
+ **Returns:** A factory the leadership provider calls to open each session.
206
+
207
+ #### `createProvider(options)`
208
+
209
+ Creates an instance-leadership provider.
210
+
211
+ ```typescript
212
+ function createProvider(options: PostgresLeadershipProviderOptions): InstanceLeadershipProvider
213
+ ```
214
+
215
+ - `options` — The lock-session factory and retry interval.
216
+
217
+ **Returns:** A provider implementing the core `InstanceLeadershipProvider` contract.
218
+
219
+ ### Constants
220
+
221
+ #### `provider`
222
+
223
+ A provider that reads its connection settings from `DATABASE_URL`.
224
+
225
+ Its lock session is opened lazily, so importing (and bonding) this provider
226
+ never connects. The lock id is the same one the API platform API has used
227
+ for its singleton work; an app that shares a database with another
228
+ leadership user must pass its OWN `lockId`.
229
+
230
+ ```typescript
231
+ const provider: InstanceLeadershipProvider
232
+ ```
233
+
234
+ ## Core Interface
235
+
236
+ Implements `@molecule/api-instance-leadership` interface.
237
+
238
+ ## Bond Wiring
239
+
240
+ Setup function to register this provider with the core interface:
241
+
242
+ ```typescript
243
+ import { setProvider } from '@molecule/api-instance-leadership'
244
+ import { provider } from '@molecule/api-instance-leadership-postgresql'
245
+
246
+ export function setupInstanceLeadershipPostgresql(): void {
247
+ setProvider(provider)
248
+ }
249
+ ```
250
+
251
+ ## Injection Notes
252
+
253
+ ### Requirements
254
+
255
+ Peer dependencies:
256
+
257
+ - `@molecule/api-bond` ^1.0.1
258
+ - `@molecule/api-instance-leadership` ^1.0.0
259
+
260
+ ### Environment Variables
261
+
262
+ - `DATABASE_URL` _(required)_ — PostgreSQL connection string
263
+ - Setup: Use a direct connection or a session-mode pooler; transaction-mode pooling cannot retain advisory locks.
264
+
265
+ ### Runtime Dependencies
266
+
267
+ - `@molecule/api-bond`
268
+ - `@molecule/api-instance-leadership`
269
+ - `pg`
10
270
 
11
- License: Apache-2.0
271
+ - **A session lock dies with its connection.** The holder's process exiting,
272
+ crashing, or losing the network ends the backend session and PostgreSQL
273
+ releases the lock — no TTL to tune, no lock row left stale.
274
+ - **A leader that loses its connection demotes itself.** The provider stops
275
+ treating itself as leader and calls `onLost`; a follower retries every
276
+ `retryMs` (default 2 s) and takes over within seconds.
277
+ - **`prepareThreshold`/pooling caveat.** The lock MUST be taken on a direct
278
+ client connection, never through a transaction-mode pooler (PgBouncer in
279
+ transaction mode), which multiplexes statements from many clients onto a
280
+ session: a lock "held" there is held by an unrelated session. Use a
281
+ session-mode pooler or a direct connection.
282
+ - **Pick a lock id nobody else uses.** Advisory locks share one namespace per
283
+ database. The exported `provider` uses `8675310`; a second application
284
+ sharing the same database must pass its own id to
285
+ `createPostgresLockFactory`.
286
+ - **This bond runs no singleton work itself.** It only tells the application
287
+ which instance leads; the loops belong to the app (`onAcquired`/`onLost`).
@@ -1 +1 @@
1
- {"version":3,"file":"browser-guard.d.ts","sourceRoot":"","sources":["../src/browser-guard.ts"],"names":[],"mappings":"AAqBA,OAAO,EAAE,CAAA"}
1
+ {"version":3,"file":"browser-guard.d.ts","sourceRoot":"","sources":["../src/browser-guard.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAE,CAAA"}
@@ -1,10 +1,12 @@
1
1
  /**
2
2
  * Browser guard — `@molecule/api-instance-leadership-postgresql` is SERVER-ONLY.
3
3
  *
4
- * Evaluating a server package in a browser bundle is always an import-graph
5
- * mistake (node APIs, secrets); without this guard it surfaces as a cryptic
6
- * downstream crash far from the culprit. Throwing here names the package and
7
- * the fix at the exact moment the client bundle evaluates it.
4
+ * Generated by scripts/gen-browser-guards.mjs (workspace root) — edit THAT, not this.
5
+ * Evaluating a server package in a browser bundle is always an import-graph mistake
6
+ * (node APIs, secrets); without this guard it surfaces as a cryptic downstream crash
7
+ * ("Buffer is not defined") far from the culprit. Throwing here names the package and
8
+ * the fix at the exact moment the client bundle evaluates it. jsdom tests and SSR are
9
+ * unaffected: the throw requires browser globals AND the absence of a node runtime.
8
10
  */
9
11
  const g = globalThis;
10
12
  if (g.window !== undefined && g.document !== undefined && !g.process?.versions?.node) {
package/dist/index.d.ts CHANGED
@@ -40,6 +40,7 @@
40
40
  *
41
41
  * @module
42
42
  */
43
+ export * from './browser-guard.js';
43
44
  export * from './lock.js';
44
45
  export * from './provider.js';
45
46
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH,cAAc,oBAAoB,CAAA;AAClC,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA"}
package/dist/index.js CHANGED
@@ -40,5 +40,6 @@
40
40
  *
41
41
  * @module
42
42
  */
43
+ export * from './browser-guard.js';
43
44
  export * from './lock.js';
44
45
  export * from './provider.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molecule/api-instance-leadership-postgresql",
3
- "version": "0.0.1-stage",
3
+ "version": "1.1.0",
4
4
  "description": "PostgreSQL provider for @molecule/api-instance-leadership: one leader across every API instance via a session advisory lock on a dedicated connection, with demotion when the connection is lost.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -44,7 +44,7 @@
44
44
  },
45
45
  "devDependencies": {
46
46
  "@molecule/api-bond": "1.0.2",
47
- "@molecule/api-instance-leadership": "1.0.0",
47
+ "@molecule/api-instance-leadership": "1.1.0",
48
48
  "@types/node": "26.1.2",
49
49
  "@types/pg": "8.20.0",
50
50
  "typescript": "6.0.3",