@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 +280 -4
- package/dist/browser-guard.d.ts.map +1 -1
- package/dist/browser-guard.js +6 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/package.json +2 -2
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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":"
|
|
1
|
+
{"version":3,"file":"browser-guard.d.ts","sourceRoot":"","sources":["../src/browser-guard.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAE,CAAA"}
|
package/dist/browser-guard.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Browser guard — `@molecule/api-instance-leadership-postgresql` is SERVER-ONLY.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
package/dist/index.d.ts.map
CHANGED
|
@@ -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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@molecule/api-instance-leadership-postgresql",
|
|
3
|
-
"version": "
|
|
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.
|
|
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",
|