@rotorsoft/act-pg 0.23.0 → 0.25.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 +91 -119
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/postgres-store.d.ts +43 -2
- package/dist/@types/postgres-store.d.ts.map +1 -1
- package/dist/index.cjs +215 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +215 -18
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,28 +4,27 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/@rotorsoft/act-pg)
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
_PostgreSQL event store for [@rotorsoft/act](https://www.npmjs.com/package/@rotorsoft/act). ACID, connection-pooled, multi-process — production default for Act deployments. Lane-aware claim/ack via `streams.lane` + `streams_lane_ix` since v0.25.0 ([ACT-1103](https://github.com/Rotorsoft/act-root/issues/733))._
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## Why this package
|
|
10
|
+
|
|
11
|
+
Act's in-memory store is fine for development and tests, but production needs durable events, cross-process coordination, and a query path that scales past a single Node process. `PostgresStore` is the canonical production implementation of Act's `Store` port: full ACID guarantees from PG, atomic stream claiming via `FOR UPDATE SKIP LOCKED` (no application-layer locking required), optional `LISTEN`/`NOTIFY` for sub-poll cross-process wakeup, and auto-managed schema via `seed()`.
|
|
12
|
+
|
|
13
|
+
The adapter passes the same conformance suite (`@rotorsoft/act-tck`) as InMemoryStore and SqliteStore, so swapping it in is a one-line bootstrap change.
|
|
10
14
|
|
|
11
15
|
## Installation
|
|
12
16
|
|
|
13
|
-
```
|
|
14
|
-
npm install @rotorsoft/act @rotorsoft/act-pg
|
|
15
|
-
# or
|
|
17
|
+
```bash
|
|
16
18
|
pnpm add @rotorsoft/act @rotorsoft/act-pg
|
|
17
19
|
```
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
## Quick start
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
```typescript
|
|
23
|
+
```ts
|
|
24
24
|
import { act, state, store } from "@rotorsoft/act";
|
|
25
25
|
import { PostgresStore } from "@rotorsoft/act-pg";
|
|
26
26
|
import { z } from "zod";
|
|
27
27
|
|
|
28
|
-
// Inject the PostgreSQL store before building your app
|
|
29
28
|
store(new PostgresStore({
|
|
30
29
|
host: "localhost",
|
|
31
30
|
port: 5432,
|
|
@@ -34,84 +33,67 @@ store(new PostgresStore({
|
|
|
34
33
|
password: "secret",
|
|
35
34
|
}));
|
|
36
35
|
|
|
37
|
-
//
|
|
36
|
+
// One-time schema setup (idempotent — safe to leave in your bootstrap).
|
|
38
37
|
await store().seed();
|
|
39
38
|
|
|
40
|
-
//
|
|
39
|
+
// From here, the framework is identical to the InMemory version.
|
|
41
40
|
const Counter = state({ Counter: z.object({ count: z.number() }) })
|
|
42
41
|
.init(() => ({ count: 0 }))
|
|
43
42
|
.emits({ Incremented: z.object({ amount: z.number() }) })
|
|
44
|
-
.patch({ Incremented: ({ data }, s) => ({ count: s.count + data.amount }) })
|
|
43
|
+
.patch({ Incremented: ({ data }, s) => ({ count: s.count + data.amount }) })
|
|
45
44
|
.on({ increment: z.object({ by: z.number() }) })
|
|
46
|
-
|
|
45
|
+
.emit((a) => ["Incremented", { amount: a.by }])
|
|
47
46
|
.build();
|
|
48
47
|
|
|
49
48
|
const app = act().withState(Counter).build();
|
|
50
|
-
await app.do("increment", { stream: "
|
|
49
|
+
await app.do("increment", { stream: "c1", actor: { id: "1", name: "u" } }, { by: 1 });
|
|
51
50
|
```
|
|
52
51
|
|
|
52
|
+
## API
|
|
53
|
+
|
|
54
|
+
- **`PostgresStore`** — class implementing Act's `Store` port. Construct once, pass to `store()`.
|
|
55
|
+
- **`PostgresConfig`** — constructor options (host/port/db/user/password/schema/table/notify).
|
|
56
|
+
|
|
57
|
+
Full type reference: [typedoc](https://github.com/Rotorsoft/act-root/blob/master/docs/docs/api/act-pg/src/README.md).
|
|
58
|
+
|
|
53
59
|
## Configuration
|
|
54
60
|
|
|
55
|
-
All
|
|
61
|
+
All fields are optional and have sensible defaults:
|
|
56
62
|
|
|
57
63
|
| Option | Default | Description |
|
|
58
|
-
|
|
64
|
+
|---|---|---|
|
|
59
65
|
| `host` | `localhost` | PostgreSQL host |
|
|
60
66
|
| `port` | `5432` | PostgreSQL port |
|
|
61
67
|
| `database` | `postgres` | Database name |
|
|
62
68
|
| `user` | `postgres` | Database user |
|
|
63
69
|
| `password` | `postgres` | Database password |
|
|
64
|
-
| `schema` | `public` | Schema for event tables |
|
|
65
|
-
| `table` | `events` | Base name for
|
|
70
|
+
| `schema` | `public` | Schema for event + streams tables |
|
|
71
|
+
| `table` | `events` | Base name (`<table>` for events, `<table>_streams` for subscriptions) |
|
|
72
|
+
| `notify` | `false` | Opt-in `LISTEN`/`NOTIFY` for cross-process commit wakeup (see below) |
|
|
73
|
+
| `max`, `idleTimeoutMillis`, …pg.PoolConfig | (pg defaults) | Pass-through to node-postgres pool config |
|
|
66
74
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
database:
|
|
73
|
-
user:
|
|
75
|
+
```ts
|
|
76
|
+
// Production deployment via env vars
|
|
77
|
+
store(new PostgresStore({
|
|
78
|
+
host: process.env.DB_HOST,
|
|
79
|
+
port: Number(process.env.DB_PORT ?? 5432),
|
|
80
|
+
database: process.env.DB_NAME,
|
|
81
|
+
user: process.env.DB_USER,
|
|
74
82
|
password: process.env.DB_PASSWORD,
|
|
75
|
-
schema: "
|
|
76
|
-
|
|
77
|
-
});
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
### Environment-Based Configuration
|
|
81
|
-
|
|
82
|
-
```typescript
|
|
83
|
-
if (process.env.NODE_ENV === "production") {
|
|
84
|
-
store(new PostgresStore({
|
|
85
|
-
host: process.env.DB_HOST,
|
|
86
|
-
port: parseInt(process.env.DB_PORT || "5432"),
|
|
87
|
-
database: process.env.DB_NAME,
|
|
88
|
-
user: process.env.DB_USER,
|
|
89
|
-
password: process.env.DB_PASSWORD,
|
|
90
|
-
}));
|
|
91
|
-
}
|
|
92
|
-
// In development, the default InMemoryStore is used
|
|
83
|
+
schema: process.env.DB_SCHEMA ?? "public",
|
|
84
|
+
max: 20, // pool size — raise for drain-heavy workloads
|
|
85
|
+
}));
|
|
93
86
|
```
|
|
94
87
|
|
|
95
|
-
|
|
88
|
+
Multi-tenant deployments often want one schema per tenant. The store accepts both — use them rather than namespacing stream IDs.
|
|
96
89
|
|
|
97
|
-
|
|
98
|
-
- **Optimistic Concurrency** - Version-based conflict detection prevents lost updates
|
|
99
|
-
- **Connection Pooling** - Uses [node-postgres](https://node-postgres.com/) Pool for efficient connection management
|
|
100
|
-
- **Atomic Stream Claiming** - Zero-contention competing consumers via `FOR UPDATE SKIP LOCKED`
|
|
101
|
-
- **Auto Schema Setup** - `seed()` creates all required tables, indexes, and schema
|
|
102
|
-
- **Cross-Process `LISTEN`/`NOTIFY`** (opt-in) - Set `notify: true` to wake `settle()` immediately on remote commits — no polling lag for horizontally-scaled deployments. Off by default. See [PERFORMANCE.md](./PERFORMANCE.md) for the latency benchmark.
|
|
103
|
-
- **Multi-Tenant** - Isolate tenants using separate schemas
|
|
90
|
+
## Common patterns
|
|
104
91
|
|
|
105
|
-
|
|
92
|
+
### Cross-process `LISTEN`/`NOTIFY` (opt-in)
|
|
106
93
|
|
|
107
|
-
For multi-instance deployments, `PostgresStore` implements the optional `Store.notify` hook via `LISTEN`/`NOTIFY` so the orchestrator wakes `settle()` immediately on commits from other processes — no polling delay.
|
|
108
|
-
|
|
109
|
-
**Opt-in via the `notify: true` config flag.** The cost (per-commit `pg_notify`, dedicated `LISTEN` client per process) is wasted in single-instance deployments, so it defaults to **off** — existing callers see zero behavior change after upgrading. Multi-process apps that need sub-poll wakeup enable it on every store instance involved (writers and listeners both):
|
|
94
|
+
For multi-instance deployments, `PostgresStore` implements the optional `Store.notify` hook via `LISTEN`/`NOTIFY` so the orchestrator wakes `settle()` immediately on commits from other processes — no polling delay. Off by default to keep single-instance deployments allocation-free; enable on every store instance in a multi-process app:
|
|
110
95
|
|
|
111
96
|
```ts
|
|
112
|
-
import { act, store } from "@rotorsoft/act";
|
|
113
|
-
import { PostgresStore } from "@rotorsoft/act-pg";
|
|
114
|
-
|
|
115
97
|
const config = { schema: "myapp", table: "events", notify: true };
|
|
116
98
|
|
|
117
99
|
// Worker A (writer)
|
|
@@ -119,91 +101,81 @@ store(new PostgresStore(config));
|
|
|
119
101
|
const app = act().withState(Order).build();
|
|
120
102
|
await app.do("placeOrder", { stream: "order-1", actor }, payload);
|
|
121
103
|
|
|
122
|
-
// Worker B (reactions, separate process
|
|
123
|
-
store(new PostgresStore(config));
|
|
104
|
+
// Worker B (reactions, separate process)
|
|
105
|
+
store(new PostgresStore(config));
|
|
124
106
|
const app = act()
|
|
125
107
|
.withState(Order)
|
|
126
108
|
.on("OrderPlaced").do(reduceInventory).to("inventory-1")
|
|
127
109
|
.build();
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
app.on("notified", (n) => sse.broadcast(n));
|
|
110
|
+
// Worker B wakes within ~10ms of Worker A's commit (vs. ≥ poll interval).
|
|
111
|
+
app.on("notified", (n) => sse.broadcast(n)); // optional fan-out
|
|
131
112
|
```
|
|
132
113
|
|
|
133
|
-
When `notify: true`:
|
|
134
|
-
- `commit()` issues one `NOTIFY act_commit_<schema>_<table>` per transaction with the full event batch as a JSON payload.
|
|
135
|
-
- The orchestrator auto-subscribes once at `build()` (one dedicated PG client per process — size your pool accordingly).
|
|
136
|
-
- The store self-filters its own commits (per-instance UUID in the payload), so the `notified` lifecycle event surfaces only **cross-process** activity. Local commits already arm drain via `do()`.
|
|
114
|
+
When `notify: true`: `commit()` issues one `NOTIFY act_commit_<schema>_<table>` per transaction with the full event batch as JSON. The store self-filters its own commits (per-instance UUID), so the `"notified"` lifecycle event surfaces only cross-process activity. Size your pool to account for one extra dedicated LISTEN client per process.
|
|
137
115
|
|
|
138
|
-
|
|
116
|
+
`notify` is a hint, not a contract — lost notifications fall back to the existing debounce/poll path. Correctness is preserved.
|
|
139
117
|
|
|
140
|
-
|
|
118
|
+
**Build-time contract:** call `store(adapter)` *before* `act()…build()`. The orchestrator binds notify to whichever store is current at construction time.
|
|
141
119
|
|
|
142
|
-
|
|
120
|
+
### Competing consumer (free horizontal scaling)
|
|
143
121
|
|
|
144
|
-
|
|
122
|
+
`claim()` uses `FOR UPDATE SKIP LOCKED` — the idiomatic Postgres competing-consumer pattern. Workers never block each other; locked rows are silently skipped. Same approach as pgBoss and Graphile Worker.
|
|
145
123
|
|
|
146
|
-
|
|
124
|
+
Add a second pod, run the same Act app — drain workload splits with zero application-layer coordination. No external job queue, no Redis lock.
|
|
147
125
|
|
|
148
|
-
|
|
149
|
-
- `id` (serial) - global event sequence
|
|
150
|
-
- `name` - event type name
|
|
151
|
-
- `data` (jsonb) - event payload
|
|
152
|
-
- `stream` - stream identifier
|
|
153
|
-
- `version` - per-stream sequence number
|
|
154
|
-
- `created` (timestamptz) - event timestamp
|
|
155
|
-
- `meta` (jsonb) - correlation, causation, and actor metadata
|
|
126
|
+
### Schema setup
|
|
156
127
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
- `leased_by` / `leased_until` - distributed processing claim info
|
|
161
|
-
- `blocked` / `error` - error tracking for failed streams
|
|
162
|
-
- `priority` - scheduling priority (default 0; higher wins lagging-frontier ties — see [Priority lanes](https://rotorsoft.github.io/act-root/docs/architecture/priority-lanes))
|
|
128
|
+
```ts
|
|
129
|
+
await store().seed();
|
|
130
|
+
```
|
|
163
131
|
|
|
164
|
-
|
|
132
|
+
Idempotent. Creates the events table, the streams (subscription) table, and the indexes that support the claim and notify paths. Safe to leave in your bootstrap. The store transparently runs `ADD COLUMN IF NOT EXISTS` migrations for new optional columns (e.g. `priority` for [priority lanes](https://rotorsoft.github.io/act-root/docs/architecture/priority-lanes)), so existing deployments upgrade in place.
|
|
165
133
|
|
|
166
|
-
|
|
134
|
+
### Database schema reference
|
|
167
135
|
|
|
168
|
-
|
|
136
|
+
Created by `seed()`:
|
|
169
137
|
|
|
170
|
-
-
|
|
171
|
-
-
|
|
172
|
-
- Same pattern used by pgBoss, Graphile Worker, and other production job queues
|
|
173
|
-
- Enables horizontal scaling by simply adding more workers
|
|
138
|
+
- **Events** (`{schema}.{table}`): `id` (serial PK), `name`, `data` (jsonb), `stream`, `version`, `created` (timestamptz), `meta` (jsonb). Unique index on `(stream, version)`.
|
|
139
|
+
- **Streams** (`{schema}.{table}_streams`): `stream` (PK), `source`, `at`, `retry`, `blocked`, `error`, `leased_by`, `leased_until`, `priority`. Composite index on `(blocked, priority DESC, at)` for the saturated-claim ordering.
|
|
174
140
|
|
|
175
|
-
|
|
141
|
+
## When to use this vs `act-sqlite`
|
|
176
142
|
|
|
177
|
-
|
|
143
|
+
| You want… | Use |
|
|
144
|
+
|---|---|
|
|
145
|
+
| Multi-server deployment, distributed processing | `act-pg` |
|
|
146
|
+
| Sub-poll cross-process reaction latency | `act-pg` (with `notify: true`) |
|
|
147
|
+
| Embedded / single-server / edge | `act-sqlite` |
|
|
148
|
+
| Zero-config local dev / tests | The default `InMemoryStore` |
|
|
178
149
|
|
|
179
|
-
|
|
150
|
+
Both adapters pass the same conformance suite — your application code doesn't change.
|
|
180
151
|
|
|
181
|
-
|
|
182
|
-
import { runStoreTck } from "@rotorsoft/act-tck";
|
|
183
|
-
import { PostgresStore } from "@rotorsoft/act-pg";
|
|
152
|
+
## Compatibility
|
|
184
153
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
```
|
|
154
|
+
- **Node**: >=22.18.0
|
|
155
|
+
- **PostgreSQL**: >=14 (uses `FOR UPDATE SKIP LOCKED`, `LISTEN`/`NOTIFY`, JSONB)
|
|
156
|
+
- **Peer**: `@rotorsoft/act` >=0.39.0, `zod` ^4.4.3
|
|
157
|
+
- **Bundled deps**: `pg` ^8.20.0
|
|
158
|
+
- **Module formats**: ESM + CJS
|
|
159
|
+
|
|
160
|
+
## Stability
|
|
161
|
+
|
|
162
|
+
Public API governed by the [Act Stability Charter](../../STABILITY.md). Charter takes effect at 1.0 (gated on [milestone 1.0](https://github.com/Rotorsoft/act-root/milestone/1)).
|
|
163
|
+
|
|
164
|
+
## Related packages
|
|
197
165
|
|
|
198
|
-
|
|
166
|
+
- **[@rotorsoft/act](https://www.npmjs.com/package/@rotorsoft/act)** — the framework whose `Store` port this implements.
|
|
167
|
+
- **[@rotorsoft/act-sqlite](https://www.npmjs.com/package/@rotorsoft/act-sqlite)** — sibling store adapter for single-node / edge deployments.
|
|
168
|
+
- **[@rotorsoft/act-tck](https://www.npmjs.com/package/@rotorsoft/act-tck)** — conformance suite. `PostgresStore` passes `runStoreTck` with `capabilities: { notify: true }`.
|
|
169
|
+
- **[@rotorsoft/act-pino](https://www.npmjs.com/package/@rotorsoft/act-pino)** — pino logger adapter, common pairing for production deployments.
|
|
199
170
|
|
|
200
|
-
##
|
|
171
|
+
## Documentation
|
|
201
172
|
|
|
202
|
-
- [
|
|
203
|
-
- [
|
|
204
|
-
- [
|
|
205
|
-
- [
|
|
173
|
+
- **[Production checklist](https://rotorsoft.github.io/act-root/docs/guides/production-checklist)** — operator-facing guide for taking an Act app to production with this store.
|
|
174
|
+
- **[Cross-process reactions](https://rotorsoft.github.io/act-root/docs/architecture/cross-process-reactions)** — when to enable `notify`, what the latency looks like.
|
|
175
|
+
- **[Concurrency model](https://rotorsoft.github.io/act-root/docs/architecture/concurrency-model)** — lease lifecycle, `claim`/`ack`/`block`/timeout, optimistic concurrency.
|
|
176
|
+
- **[Writing a custom Store adapter](https://rotorsoft.github.io/act-root/docs/guides/writing-a-store)** — the recipe `PostgresStore` itself follows, for authors building against other databases.
|
|
177
|
+
- **[PERFORMANCE.md](./PERFORMANCE.md)** — measured throughput numbers, including the `notify` latency benchmark.
|
|
206
178
|
|
|
207
179
|
## License
|
|
208
180
|
|
|
209
|
-
|
|
181
|
+
MIT
|