@kairos-es/store-postgres 0.0.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/LICENSE +28 -0
- package/README.md +167 -0
- package/dist/cjs/clients.js +62 -0
- package/dist/cjs/clients.js.map +1 -0
- package/dist/cjs/config.js +94 -0
- package/dist/cjs/config.js.map +1 -0
- package/dist/cjs/ensureSchema.js +53 -0
- package/dist/cjs/ensureSchema.js.map +1 -0
- package/dist/cjs/index.js +45 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/internal/ddl.js +436 -0
- package/dist/cjs/internal/ddl.js.map +1 -0
- package/dist/cjs/internal/matchSql.js +189 -0
- package/dist/cjs/internal/matchSql.js.map +1 -0
- package/dist/cjs/internal/readPlan.js +67 -0
- package/dist/cjs/internal/readPlan.js.map +1 -0
- package/dist/cjs/internal/subscribe.js +120 -0
- package/dist/cjs/internal/subscribe.js.map +1 -0
- package/dist/cjs/internal/transport.js +66 -0
- package/dist/cjs/internal/transport.js.map +1 -0
- package/dist/cjs/store.js +256 -0
- package/dist/cjs/store.js.map +1 -0
- package/dist/dts/clients.d.ts +34 -0
- package/dist/dts/clients.d.ts.map +1 -0
- package/dist/dts/config.d.ts +58 -0
- package/dist/dts/config.d.ts.map +1 -0
- package/dist/dts/ensureSchema.d.ts +35 -0
- package/dist/dts/ensureSchema.d.ts.map +1 -0
- package/dist/dts/index.d.ts +41 -0
- package/dist/dts/index.d.ts.map +1 -0
- package/dist/dts/internal/ddl.d.ts +174 -0
- package/dist/dts/internal/ddl.d.ts.map +1 -0
- package/dist/dts/internal/matchSql.d.ts +140 -0
- package/dist/dts/internal/matchSql.d.ts.map +1 -0
- package/dist/dts/internal/readPlan.d.ts +72 -0
- package/dist/dts/internal/readPlan.d.ts.map +1 -0
- package/dist/dts/internal/subscribe.d.ts +87 -0
- package/dist/dts/internal/subscribe.d.ts.map +1 -0
- package/dist/dts/internal/transport.d.ts +116 -0
- package/dist/dts/internal/transport.d.ts.map +1 -0
- package/dist/dts/store.d.ts +17 -0
- package/dist/dts/store.d.ts.map +1 -0
- package/dist/esm/clients.js +51 -0
- package/dist/esm/clients.js.map +1 -0
- package/dist/esm/config.js +87 -0
- package/dist/esm/config.js.map +1 -0
- package/dist/esm/ensureSchema.js +45 -0
- package/dist/esm/ensureSchema.js.map +1 -0
- package/dist/esm/index.js +40 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/ddl.js +424 -0
- package/dist/esm/internal/ddl.js.map +1 -0
- package/dist/esm/internal/matchSql.js +176 -0
- package/dist/esm/internal/matchSql.js.map +1 -0
- package/dist/esm/internal/readPlan.js +59 -0
- package/dist/esm/internal/readPlan.js.map +1 -0
- package/dist/esm/internal/subscribe.js +113 -0
- package/dist/esm/internal/subscribe.js.map +1 -0
- package/dist/esm/internal/transport.js +56 -0
- package/dist/esm/internal/transport.js.map +1 -0
- package/dist/esm/package.json +4 -0
- package/dist/esm/store.js +249 -0
- package/dist/esm/store.js.map +1 -0
- package/package.json +35 -0
- package/src/clients.ts +69 -0
- package/src/config.ts +92 -0
- package/src/ensureSchema.ts +46 -0
- package/src/index.ts +45 -0
- package/src/internal/ddl.ts +599 -0
- package/src/internal/matchSql.ts +219 -0
- package/src/internal/readPlan.ts +117 -0
- package/src/internal/transport.ts +141 -0
- package/src/store.ts +413 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Neverbland
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# @kairos-es/store-postgres
|
|
2
|
+
|
|
3
|
+
The Postgres `DcbEventStore` backend for kairos-es: the two-table B-tree +
|
|
4
|
+
exclusive-lock conditional append of [ADR-0002](../../docs/adr), behind the same
|
|
5
|
+
`DcbEventStore` contract as the in-memory store. It passes the shared
|
|
6
|
+
`@kairos-es/store-contract-tests` suite unchanged, with the in-memory store as
|
|
7
|
+
the parity oracle.
|
|
8
|
+
|
|
9
|
+
A store is defined entirely by its `{ schema, tablePrefix }` pair. Every object
|
|
10
|
+
it owns (both tables, both indexes, both plpgsql functions, and the NOTIFY
|
|
11
|
+
channel) is derived from that pair, so multiple isolated stores cohabit one
|
|
12
|
+
database without interference.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
`@kairos-es/core`, `effect`, and the Effect SQL/Postgres stack are peer
|
|
17
|
+
dependencies (so the app owns the single shared Effect + `SqlClient` instance,
|
|
18
|
+
per ADR-0003):
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
pnpm add @kairos-es/store-postgres@alpha @kairos-es/core@alpha \
|
|
22
|
+
@effect/sql @effect/sql-pg @effect/platform @effect/experimental effect
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`@effect/platform` and `@effect/experimental` are pulled in because
|
|
26
|
+
`@effect/sql-pg` requires them (the pooled client's `Reactivity` service lives in
|
|
27
|
+
`@effect/experimental`); declaring them here keeps the peer graph resolvable for
|
|
28
|
+
consumers.
|
|
29
|
+
|
|
30
|
+
## Schema: `ensureSchema` as a migration
|
|
31
|
+
|
|
32
|
+
The package ships **no migrator**. `ensureSchema(config)` is a single idempotent
|
|
33
|
+
`Effect<void, SqlError, SqlClient>` that creates (or refreshes) every object the
|
|
34
|
+
store owns. Every statement is `CREATE … IF NOT EXISTS` or `CREATE OR REPLACE
|
|
35
|
+
FUNCTION`, so re-running it is a no-op. Migration **orchestration** is yours; the
|
|
36
|
+
package only provides the idempotent **payload**.
|
|
37
|
+
|
|
38
|
+
Drop it straight into an `@effect/sql` migrator as a one-liner:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { Migrator } from '@effect/sql'
|
|
42
|
+
import { ensureSchema } from '@kairos-es/store-postgres'
|
|
43
|
+
|
|
44
|
+
const config = { schema: 'public', tablePrefix: 'dcb_events' }
|
|
45
|
+
|
|
46
|
+
const migrations = Migrator.fromRecord({
|
|
47
|
+
'0001_kairos_store_dcb_events': ensureSchema(config),
|
|
48
|
+
})
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Because `ensureSchema` requires the **generic** `SqlClient` tag (not the
|
|
52
|
+
store-owned client tags), it slots into any migrator or boot sequence that
|
|
53
|
+
already provides a `SqlClient`.
|
|
54
|
+
|
|
55
|
+
Or run it directly once at boot, over whatever `SqlClient` you have wired:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { SqlClient } from '@effect/sql'
|
|
59
|
+
import { PgClient } from '@effect/sql-pg'
|
|
60
|
+
import { ensureSchema } from '@kairos-es/store-postgres'
|
|
61
|
+
import { Effect, Layer, Redacted } from 'effect'
|
|
62
|
+
|
|
63
|
+
const config = { schema: 'public', tablePrefix: 'dcb_events' }
|
|
64
|
+
|
|
65
|
+
const program = ensureSchema(config)
|
|
66
|
+
|
|
67
|
+
const SqlLive = PgClient.layer({ url: Redacted.make(process.env.DATABASE_URL!) })
|
|
68
|
+
|
|
69
|
+
await Effect.runPromise(program.pipe(Effect.provide(SqlLive)))
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`ensureSchema` must have run before the store `layer` is used; the `layer` itself
|
|
73
|
+
performs no DDL and depends only on the two client tags.
|
|
74
|
+
|
|
75
|
+
## Two-endpoint wiring (Neon)
|
|
76
|
+
|
|
77
|
+
`@effect/sql-pg` exposes a single `PgClient`/`SqlClient` tag, so this package
|
|
78
|
+
defines **two distinct tags**, each holding a `PgClient` value:
|
|
79
|
+
|
|
80
|
+
- `PgClientPooled` — reads, the append function, and `NOTIFY`. Point it at your
|
|
81
|
+
pooled endpoint.
|
|
82
|
+
- `PgClientDirect` — used **only** by `subscribe`'s `LISTEN`. Point it at a
|
|
83
|
+
**direct** (non-pooler) endpoint.
|
|
84
|
+
|
|
85
|
+
Why the split matters on Neon: the built-in `listen` constructs a standalone
|
|
86
|
+
connection inheriting the pool's host. A Neon **pooled** endpoint
|
|
87
|
+
(`…-pooler…`) lands that connection on the PgBouncer front door, which
|
|
88
|
+
**silently drops `LISTEN`/`NOTIFY`** — the subscription would simply never wake.
|
|
89
|
+
Pointing `PgClientDirect` at Neon's direct endpoint fixes it. `subscribe` is the
|
|
90
|
+
only method that touches `PgClientDirect`.
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import {
|
|
94
|
+
directLayer,
|
|
95
|
+
layer,
|
|
96
|
+
pooledLayer,
|
|
97
|
+
} from '@kairos-es/store-postgres'
|
|
98
|
+
import { Layer, Redacted } from 'effect'
|
|
99
|
+
|
|
100
|
+
const config = { schema: 'public', tablePrefix: 'dcb_events' }
|
|
101
|
+
|
|
102
|
+
const clients = Layer.merge(
|
|
103
|
+
// The -pooler endpoint: pooled reads/append/notify.
|
|
104
|
+
pooledLayer({ url: Redacted.make(process.env.NEON_POOLED_URL!) }),
|
|
105
|
+
// The direct (non-pooler) endpoint: LISTEN/NOTIFY survives here.
|
|
106
|
+
directLayer({ url: Redacted.make(process.env.NEON_DIRECT_URL!) }),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
const store = layer(config).pipe(Layer.provide(clients))
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
On a plain (non-Neon) Postgres, point both tags at the **same** URL — the split
|
|
113
|
+
costs nothing there:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
const url = Redacted.make(process.env.DATABASE_URL!)
|
|
117
|
+
const clients = Layer.merge(pooledLayer({ url }), directLayer({ url }))
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Positions are strictly-increasing, not gapless
|
|
121
|
+
|
|
122
|
+
`id` is a `bigserial`, so positions are **strictly-increasing but NOT gapless**.
|
|
123
|
+
A rolled-back or timed-out insert permanently consumes its id, leaving a gap.
|
|
124
|
+
Ordering is by `id` alone; never assume positions are contiguous. `read`'s `head`
|
|
125
|
+
is the actual last position, not a count — treat it as an opaque cursor, compare
|
|
126
|
+
it, and never do arithmetic on it. (The shared contract suite is gap-tolerant for
|
|
127
|
+
exactly this reason.)
|
|
128
|
+
|
|
129
|
+
## `lockTimeout` defaults to `0`
|
|
130
|
+
|
|
131
|
+
`lockTimeout` (seconds) is passed to `set_config('lock_timeout', …, true)` (the
|
|
132
|
+
bound-parameter equivalent of `SET LOCAL`) inside every append, bounding how long
|
|
133
|
+
a blocked writer waits on the exclusive lock. It **defaults to `0` — wait
|
|
134
|
+
indefinitely**, matching the in-memory semaphore's unbounded wait so the shared
|
|
135
|
+
contract suite behaves identically across engines.
|
|
136
|
+
|
|
137
|
+
A non-zero timeout that fires raises SQLSTATE `55P03`. That is a Postgres-only
|
|
138
|
+
outcome and is surfaced **off** the shared `append` contract as a **defect**
|
|
139
|
+
(`Effect.die`), never as `AppendConditionFailed` — the in-memory oracle cannot
|
|
140
|
+
produce it, and a lock-timeout is infrastructure, not a consistency conflict.
|
|
141
|
+
|
|
142
|
+
## Configuration
|
|
143
|
+
|
|
144
|
+
`PostgresStoreConfig` is a `Schema` (exported), so a malformed config fails
|
|
145
|
+
loudly at construction (empty/dotted `schema`/`tablePrefix`, a
|
|
146
|
+
negative/non-finite `lockTimeout`, or any `{ schema, tablePrefix }` whose derived
|
|
147
|
+
identifiers would exceed Postgres's 63-byte limit and be silently truncated).
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
interface PostgresStoreConfig {
|
|
151
|
+
readonly schema?: string // default 'public'
|
|
152
|
+
readonly tablePrefix?: string // default 'dcb_events'
|
|
153
|
+
readonly lockTimeout?: number // seconds; default 0 (wait forever)
|
|
154
|
+
readonly pollInterval?: number // subscribe poll, ms; default 1000
|
|
155
|
+
readonly catchUpPageSize?: number // subscribe reconcile page; default 512
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
`pollInterval` bounds worst-case live-tail latency for `subscribe`: the periodic
|
|
160
|
+
position-based poll — not `NOTIFY` — is the delivery guarantee, so every
|
|
161
|
+
committed position is reconciled within this interval whether or not its `NOTIFY`
|
|
162
|
+
was heard. `catchUpPageSize` is how many events a single reconcile read fetches
|
|
163
|
+
before looping.
|
|
164
|
+
|
|
165
|
+
## Licence
|
|
166
|
+
|
|
167
|
+
BSD 3-Clause. See [`LICENSE`](./LICENSE). Copyright (c) 2026 Neverbland.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.pooledLayer = exports.directLayer = exports.PgClientPooled = exports.PgClientDirect = void 0;
|
|
7
|
+
var _experimental = require("@effect/experimental");
|
|
8
|
+
var _sqlPg = require("@effect/sql-pg");
|
|
9
|
+
var _effect = require("effect");
|
|
10
|
+
/**
|
|
11
|
+
* The TWO store-owned Postgres clients — the "Neon trap" mitigation (ADR-0002).
|
|
12
|
+
*
|
|
13
|
+
* `@effect/sql-pg` exposes a single `PgClient` (and `SqlClient`) tag; a
|
|
14
|
+
* `PgClient.layer` claims BOTH. If we built two `PgClient.layer`s they would
|
|
15
|
+
* collide on that one tag — the second would shadow the first. So instead we
|
|
16
|
+
* define two DISTINCT `Context.Tag`s, each HOLDING a `PgClient` value:
|
|
17
|
+
*
|
|
18
|
+
* - `PgClientPooled` — the pooled client used for reads, the append function,
|
|
19
|
+
* and `notify`. Backed by a `pg.Pool`; connections are recycled per query.
|
|
20
|
+
* - `PgClientDirect` — a client used ONLY by `subscribe`'s `listen`. Its
|
|
21
|
+
* built-in `listen` constructs a STANDALONE `pg.Client` from the pool's
|
|
22
|
+
* `options`, inheriting the pool's host. On a Neon POOLED endpoint
|
|
23
|
+
* (`…-pooler…`) that standalone connection lands on the PgBouncer front door,
|
|
24
|
+
* which SILENTLY DROPS `LISTEN`/`NOTIFY` — the subscription would simply never
|
|
25
|
+
* wake. Pointing `PgClientDirect` at Neon's DIRECT (non-pooler) endpoint fixes
|
|
26
|
+
* it. `subscribe` is the ONLY method that touches `PgClientDirect`.
|
|
27
|
+
*
|
|
28
|
+
* For a plain (non-Neon) Postgres both tags point at the same URL; the split
|
|
29
|
+
* costs nothing there. The two-tag design is what makes the Neon fix a
|
|
30
|
+
* deployment-time wiring choice rather than a code change.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The pooled client tag: reads, the append function, and `notify`. Holds a
|
|
35
|
+
* `PgClient` value rather than being a `PgClient.layer`, so it never collides
|
|
36
|
+
* with the direct client on the shared `PgClient`/`SqlClient` tag.
|
|
37
|
+
*/
|
|
38
|
+
class PgClientPooled extends /*#__PURE__*/_effect.Context.Tag('@kairos-es/store-postgres/PgClientPooled')() {}
|
|
39
|
+
/**
|
|
40
|
+
* The direct (non-pooler) client tag: the `subscribe` listener ONLY. Point this
|
|
41
|
+
* at a direct endpoint on Neon so `LISTEN`/`NOTIFY` is not dropped by the pooler.
|
|
42
|
+
*/
|
|
43
|
+
exports.PgClientPooled = PgClientPooled;
|
|
44
|
+
class PgClientDirect extends /*#__PURE__*/_effect.Context.Tag('@kairos-es/store-postgres/PgClientDirect')() {}
|
|
45
|
+
/**
|
|
46
|
+
* Build a `PgClientPooled` layer from a `PgClient` config. We construct the
|
|
47
|
+
* client via `PgClient.make` (scoped) and place it in our OWN tag, then provide
|
|
48
|
+
* `Reactivity.layer` (which `PgClient.make` requires) — exactly what the
|
|
49
|
+
* built-in `PgClient.layer` does internally, minus the collision-prone claim on
|
|
50
|
+
* the shared `PgClient` tag.
|
|
51
|
+
*/
|
|
52
|
+
exports.PgClientDirect = PgClientDirect;
|
|
53
|
+
const pooledLayer = config => _effect.Layer.scoped(PgClientPooled, _sqlPg.PgClient.make(config)).pipe(_effect.Layer.provide(_experimental.Reactivity.layer));
|
|
54
|
+
/**
|
|
55
|
+
* Build a `PgClientDirect` layer from a `PgClient` config. Same construction as
|
|
56
|
+
* `pooledLayer`; the two differ only in which endpoint the caller points them
|
|
57
|
+
* at (and that only matters on Neon).
|
|
58
|
+
*/
|
|
59
|
+
exports.pooledLayer = pooledLayer;
|
|
60
|
+
const directLayer = config => _effect.Layer.scoped(PgClientDirect, _sqlPg.PgClient.make(config)).pipe(_effect.Layer.provide(_experimental.Reactivity.layer));
|
|
61
|
+
exports.directLayer = directLayer;
|
|
62
|
+
//# sourceMappingURL=clients.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clients.js","names":["_experimental","require","_sqlPg","_effect","PgClientPooled","Context","Tag","exports","PgClientDirect","pooledLayer","config","Layer","scoped","PgClient","make","pipe","provide","Reactivity","layer","directLayer"],"sources":["../../src/clients.ts"],"sourcesContent":[null],"mappings":";;;;;;AAsBA,IAAAA,aAAA,GAAAC,OAAA;AAEA,IAAAC,MAAA,GAAAD,OAAA;AACA,IAAAE,OAAA,GAAAF,OAAA;AAzBA;;;;;;;;;;;;;;;;;;;;;;;AA2BA;;;;;AAKM,MAAOG,cAAe,sBAAQC,eAAO,CAACC,GAAG,CAC7C,0CAA0C,CAC3C,EAAqC;AAEtC;;;;AAAAC,OAAA,CAAAH,cAAA,GAAAA,cAAA;AAIM,MAAOI,cAAe,sBAAQH,eAAO,CAACC,GAAG,CAC7C,0CAA0C,CAC3C,EAAqC;AAEtC;;;;;;;AAAAC,OAAA,CAAAC,cAAA,GAAAA,cAAA;AAOO,MAAMC,WAAW,GACtBC,MAA+B,IAE/BC,aAAK,CAACC,MAAM,CAACR,cAAc,EAAES,eAAQ,CAACC,IAAI,CAACJ,MAAM,CAAC,CAAC,CAACK,IAAI,CACtDJ,aAAK,CAACK,OAAO,CAACC,wBAAU,CAACC,KAAK,CAAC,CAChC;AAEH;;;;;AAAAX,OAAA,CAAAE,WAAA,GAAAA,WAAA;AAKO,MAAMU,WAAW,GACtBT,MAA+B,IAE/BC,aAAK,CAACC,MAAM,CAACJ,cAAc,EAAEK,eAAQ,CAACC,IAAI,CAACJ,MAAM,CAAC,CAAC,CAACK,IAAI,CACtDJ,aAAK,CAACK,OAAO,CAACC,wBAAU,CAACC,KAAK,CAAC,CAChC;AAAAX,OAAA,CAAAY,WAAA,GAAAA,WAAA","ignoreList":[]}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.decodePostgresStoreConfig = exports.PostgresStoreConfig = void 0;
|
|
7
|
+
var _core = require("@kairos-es/core");
|
|
8
|
+
var _effect = require("effect");
|
|
9
|
+
var _ddl = require("./internal/ddl.js");
|
|
10
|
+
/**
|
|
11
|
+
* Public configuration for a Postgres `DcbEventStore`.
|
|
12
|
+
*
|
|
13
|
+
* A store is defined entirely by its `{ schema, tablePrefix }` naming pair plus
|
|
14
|
+
* a `lockTimeout`. Everything else — table/index/function names, the NOTIFY
|
|
15
|
+
* channel, the lock scope — is DERIVED from these (see `internal/ddl.ts`), so
|
|
16
|
+
* one config value is one completely isolated store. There is deliberately NO
|
|
17
|
+
* lockless option here: the exclusive-lock serialisation is not negotiable on the
|
|
18
|
+
* production surface — the lockless variant is a test-fixture-only seam, living in
|
|
19
|
+
* an internal module reachable only from the test graph.
|
|
20
|
+
*
|
|
21
|
+
* The config is a `Schema`, not a bare interface, so that malformed input fails
|
|
22
|
+
* LOUDLY at construction (`layer`/`ensureSchema` decode through it) rather than
|
|
23
|
+
* late and obscurely on the first DDL or append — the same fail-at-the-boundary
|
|
24
|
+
* discipline core applies to `Tag`/`ReadLimit`. It rejects: an empty or dotted
|
|
25
|
+
* `schema`/`tablePrefix` atom (a dot would break the qualified-name dot-split),
|
|
26
|
+
* a negative/non-finite `lockTimeout`, and — via `oversizedIdentifier` — any
|
|
27
|
+
* `{ schema, tablePrefix }` whose derived names would exceed Postgres's 63-byte
|
|
28
|
+
* identifier limit and be silently truncated.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A single Postgres identifier atom: non-empty and dot-free. Dots are barred
|
|
33
|
+
* because `schema`/`tablePrefix` are composed into qualified names that
|
|
34
|
+
* `sql(identifier)` dot-splits, so an embedded dot would silently re-partition
|
|
35
|
+
* the name into the wrong schema/object.
|
|
36
|
+
*/
|
|
37
|
+
const IdentifierAtom = /*#__PURE__*/_effect.Schema.String.pipe(/*#__PURE__*/_effect.Schema.nonEmptyString(), /*#__PURE__*/_effect.Schema.pattern(/^[^.]+$/, {
|
|
38
|
+
identifier: 'PostgresIdentifierAtom',
|
|
39
|
+
description: 'a non-empty identifier atom containing no "."'
|
|
40
|
+
}));
|
|
41
|
+
/**
|
|
42
|
+
* Configuration for a Postgres store instance.
|
|
43
|
+
*
|
|
44
|
+
* - `schema` — the Postgres schema that owns the store's objects (default
|
|
45
|
+
* `public`). `ensureSchema` creates it defensively.
|
|
46
|
+
* - `tablePrefix` — the base name for the store's tables/indexes/functions
|
|
47
|
+
* (default `dcb_events`). Two stores with different prefixes (or schemas)
|
|
48
|
+
* cohabit one database without interference.
|
|
49
|
+
* - `lockTimeout` — seconds passed to `set_config('lock_timeout', …, true)`
|
|
50
|
+
* inside every append, bounding how long a blocked writer waits on the
|
|
51
|
+
* exclusive lock. `0` (the default) means wait indefinitely — parity with the
|
|
52
|
+
* in-memory semaphore's unbounded wait, so the shared contract suite behaves
|
|
53
|
+
* identically across engines. A non-zero timeout that fires (`55P03`) is a
|
|
54
|
+
* Postgres-only outcome surfaced OFF the shared `append` contract (a defect),
|
|
55
|
+
* never as `AppendConditionFailed` — the in-memory oracle cannot produce it.
|
|
56
|
+
* - `pollInterval` — MILLISECONDS a live `subscribe` waits for a NOTIFY wake
|
|
57
|
+
* before re-reading from its cursor anyway, defaulting to core's
|
|
58
|
+
* `DEFAULT_POLL_INTERVAL_MILLIS`. Because this engine HAS a wake source, the
|
|
59
|
+
* value bounds worst-case live-tail latency rather than being it — only a
|
|
60
|
+
* subscriber no NOTIFY reaches waits the whole interval; lower it (e.g. in tests)
|
|
61
|
+
* to poll faster. See `packages/core/src/SubscribeMachine.ts` for what the poll
|
|
62
|
+
* guarantees and what a wake source is for.
|
|
63
|
+
* - `catchUpPageSize` — the `subscribe` reconcile page size, defaulting to core's
|
|
64
|
+
* `DEFAULT_CATCH_UP_PAGE_SIZE`: how many events a single catch-up/live read
|
|
65
|
+
* fetches before looping.
|
|
66
|
+
*/
|
|
67
|
+
const PostgresStoreConfig = exports.PostgresStoreConfig = /*#__PURE__*/_effect.Schema.Struct({
|
|
68
|
+
// `exact: true` keeps the decoded type at `key?: T` (never `T | undefined`),
|
|
69
|
+
// matching the repo's `exactOptionalPropertyTypes` discipline — an absent key,
|
|
70
|
+
// not an explicit `undefined`.
|
|
71
|
+
schema: _effect.Schema.optionalWith(IdentifierAtom, {
|
|
72
|
+
exact: true
|
|
73
|
+
}),
|
|
74
|
+
tablePrefix: _effect.Schema.optionalWith(IdentifierAtom, {
|
|
75
|
+
exact: true
|
|
76
|
+
}),
|
|
77
|
+
lockTimeout: _effect.Schema.optionalWith(_effect.Schema.Number.pipe(_effect.Schema.nonNegative(), _effect.Schema.finite()), {
|
|
78
|
+
exact: true
|
|
79
|
+
}),
|
|
80
|
+
// The two subscribe knobs are core's field definitions, not this engine's: see
|
|
81
|
+
// `subscriptionTuningFields`.
|
|
82
|
+
..._core.subscriptionTuningFields
|
|
83
|
+
}).pipe(/*#__PURE__*/
|
|
84
|
+
// Cross-field guard: the derived table/index/function/channel names must all
|
|
85
|
+
// fit Postgres's 63-byte identifier limit. Returning the message string fails
|
|
86
|
+
// the decode with it; `undefined`/`true` passes.
|
|
87
|
+
_effect.Schema.filter(config => (0, _ddl.oversizedIdentifier)(config) ?? true));
|
|
88
|
+
/**
|
|
89
|
+
* Decode an unknown value into a validated `PostgresStoreConfig`, throwing a
|
|
90
|
+
* `ParseError` on any violation. Used by `layer`/`ensureSchema` so a bad config
|
|
91
|
+
* fails at construction time.
|
|
92
|
+
*/
|
|
93
|
+
const decodePostgresStoreConfig = exports.decodePostgresStoreConfig = /*#__PURE__*/_effect.Schema.decodeUnknownSync(PostgresStoreConfig);
|
|
94
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","names":["_core","require","_effect","_ddl","IdentifierAtom","Schema","String","pipe","nonEmptyString","pattern","identifier","description","PostgresStoreConfig","exports","Struct","schema","optionalWith","exact","tablePrefix","lockTimeout","Number","nonNegative","finite","subscriptionTuningFields","filter","config","oversizedIdentifier","decodePostgresStoreConfig","decodeUnknownSync"],"sources":["../../src/config.ts"],"sourcesContent":[null],"mappings":";;;;;;AAoBA,IAAAA,KAAA,GAAAC,OAAA;AACA,IAAAC,OAAA,GAAAD,OAAA;AACA,IAAAE,IAAA,GAAAF,OAAA;AAtBA;;;;;;;;;;;;;;;;;;;;;AAwBA;;;;;;AAMA,MAAMG,cAAc,gBAAGC,cAAM,CAACC,MAAM,CAACC,IAAI,cACvCF,cAAM,CAACG,cAAc,EAAE,eACvBH,cAAM,CAACI,OAAO,CAAC,SAAS,EAAE;EACxBC,UAAU,EAAE,wBAAwB;EACpCC,WAAW,EAAE;CACd,CAAC,CACH;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;AA0BO,MAAMC,mBAAmB,GAAAC,OAAA,CAAAD,mBAAA,gBAAGP,cAAM,CAACS,MAAM,CAAC;EAC/C;EACA;EACA;EACAC,MAAM,EAAEV,cAAM,CAACW,YAAY,CAACZ,cAAc,EAAE;IAAEa,KAAK,EAAE;EAAI,CAAE,CAAC;EAC5DC,WAAW,EAAEb,cAAM,CAACW,YAAY,CAACZ,cAAc,EAAE;IAAEa,KAAK,EAAE;EAAI,CAAE,CAAC;EACjEE,WAAW,EAAEd,cAAM,CAACW,YAAY,CAC9BX,cAAM,CAACe,MAAM,CAACb,IAAI,CAACF,cAAM,CAACgB,WAAW,EAAE,EAAEhB,cAAM,CAACiB,MAAM,EAAE,CAAC,EACzD;IAAEL,KAAK,EAAE;EAAI,CAAE,CAChB;EACD;EACA;EACA,GAAGM;CACJ,CAAC,CAAChB,IAAI;AACL;AACA;AACA;AACAF,cAAM,CAACmB,MAAM,CAAEC,MAAM,IAAK,IAAAC,wBAAmB,EAACD,MAAM,CAAC,IAAI,IAAI,CAAC,CAC/D;AAGD;;;;;AAKO,MAAME,yBAAyB,GAAAd,OAAA,CAAAc,yBAAA,gBACpCtB,cAAM,CAACuB,iBAAiB,CAAChB,mBAAmB,CAAC","ignoreList":[]}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.ensureSchema = void 0;
|
|
7
|
+
var _sql = require("@effect/sql");
|
|
8
|
+
var _effect = require("effect");
|
|
9
|
+
var _config = require("./config.js");
|
|
10
|
+
var _ddl = require("./internal/ddl.js");
|
|
11
|
+
/**
|
|
12
|
+
* `ensureSchema` — the idempotent DDL migration building block (ADR-0002).
|
|
13
|
+
*
|
|
14
|
+
* This is deliberately NOT a migrator. It is a single `Effect<void, SqlError,
|
|
15
|
+
* SqlClient>` that creates (or refreshes) every object a store owns: both
|
|
16
|
+
* tables, both indexes, and both plpgsql functions. It requires the GENERIC
|
|
17
|
+
* `SqlClient` tag — NOT `PgClientPooled` — precisely so a consumer can drop it
|
|
18
|
+
* straight into THEIR OWN migrator, e.g.:
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* Migrator.fromRecord({
|
|
22
|
+
* '0001_kairos_store_dcb_events': ensureSchema(config),
|
|
23
|
+
* })
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* or simply run it once at boot. We ship no `FileSystem`/`Path`/`ChildProcess`
|
|
27
|
+
* dependency and no `PgMigrator`; migration ORCHESTRATION belongs to the
|
|
28
|
+
* consumer, this package only provides the idempotent PAYLOAD.
|
|
29
|
+
*
|
|
30
|
+
* Idempotence: every statement is `CREATE … IF NOT EXISTS` or `CREATE OR REPLACE
|
|
31
|
+
* FUNCTION`, so re-running against an existing store is a no-op. Names come from
|
|
32
|
+
* `sql(identifier)` (via the pre-quoted builder in `internal/ddl.ts`); the DDL
|
|
33
|
+
* bodies carry no bound values (pure schema), and the append functions keep
|
|
34
|
+
* their runtime VALUES as bound parameters, never interpolated.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Produce the migration payload for `config`. Resolves the object names once and
|
|
39
|
+
* runs the ordered idempotent DDL against whatever `SqlClient` the caller's
|
|
40
|
+
* migrator (or boot code) provides.
|
|
41
|
+
*/
|
|
42
|
+
const ensureSchema = config => _effect.Effect.gen(function* () {
|
|
43
|
+
const sql = yield* _sql.SqlClient.SqlClient;
|
|
44
|
+
// Decode through the config schema (same guard as `layer`) so a malformed
|
|
45
|
+
// config fails at construction rather than mid-migration.
|
|
46
|
+
const names = (0, _ddl.resolveNames)((0, _config.decodePostgresStoreConfig)(config));
|
|
47
|
+
// Sequentially, in dependency order (tables → indexes → FK → functions).
|
|
48
|
+
yield* _effect.Effect.all((0, _ddl.runDdl)(sql, names), {
|
|
49
|
+
discard: true
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
exports.ensureSchema = ensureSchema;
|
|
53
|
+
//# sourceMappingURL=ensureSchema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ensureSchema.js","names":["_sql","require","_effect","_config","_ddl","ensureSchema","config","Effect","gen","sql","SqlClient","names","resolveNames","decodePostgresStoreConfig","all","runDdl","discard","exports"],"sources":["../../src/ensureSchema.ts"],"sourcesContent":[null],"mappings":";;;;;;AAyBA,IAAAA,IAAA,GAAAC,OAAA;AACA,IAAAC,OAAA,GAAAD,OAAA;AACA,IAAAE,OAAA,GAAAF,OAAA;AACA,IAAAG,IAAA,GAAAH,OAAA;AA5BA;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA;;;;;AAKO,MAAMI,YAAY,GACvBC,MAA2B,IAE3BC,cAAM,CAACC,GAAG,CAAC,aAAS;EAClB,MAAMC,GAAG,GAAG,OAAOC,cAAS,CAACA,SAAS;EACtC;EACA;EACA,MAAMC,KAAK,GAAG,IAAAC,iBAAY,EAAC,IAAAC,iCAAyB,EAACP,MAAM,CAAC,CAAC;EAC7D;EACA,OAAOC,cAAM,CAACO,GAAG,CAAC,IAAAC,WAAM,EAACN,GAAG,EAAEE,KAAK,CAAC,EAAE;IAAEK,OAAO,EAAE;EAAI,CAAE,CAAC;AAC1D,CAAC,CAAC;AAAAC,OAAA,CAAAZ,YAAA,GAAAA,YAAA","ignoreList":[]}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
Object.defineProperty(exports, "PgClientDirect", {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: function () {
|
|
9
|
+
return _clients.PgClientDirect;
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
Object.defineProperty(exports, "PgClientPooled", {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
get: function () {
|
|
15
|
+
return _clients.PgClientPooled;
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
Object.defineProperty(exports, "directLayer", {
|
|
19
|
+
enumerable: true,
|
|
20
|
+
get: function () {
|
|
21
|
+
return _clients.directLayer;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
Object.defineProperty(exports, "ensureSchema", {
|
|
25
|
+
enumerable: true,
|
|
26
|
+
get: function () {
|
|
27
|
+
return _ensureSchema.ensureSchema;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
Object.defineProperty(exports, "layer", {
|
|
31
|
+
enumerable: true,
|
|
32
|
+
get: function () {
|
|
33
|
+
return _store.layer;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
Object.defineProperty(exports, "pooledLayer", {
|
|
37
|
+
enumerable: true,
|
|
38
|
+
get: function () {
|
|
39
|
+
return _clients.pooledLayer;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
var _clients = require("./clients.js");
|
|
43
|
+
var _ensureSchema = require("./ensureSchema.js");
|
|
44
|
+
var _store = require("./store.js");
|
|
45
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["_clients","require","_ensureSchema","_store"],"sources":["../../src/index.ts"],"sourcesContent":[null],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,IAAAA,QAAA,GAAAC,OAAA;AAOA,IAAAC,aAAA,GAAAD,OAAA;AACA,IAAAE,MAAA,GAAAF,OAAA","ignoreList":[]}
|