@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.
Files changed (73) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +167 -0
  3. package/dist/cjs/clients.js +62 -0
  4. package/dist/cjs/clients.js.map +1 -0
  5. package/dist/cjs/config.js +94 -0
  6. package/dist/cjs/config.js.map +1 -0
  7. package/dist/cjs/ensureSchema.js +53 -0
  8. package/dist/cjs/ensureSchema.js.map +1 -0
  9. package/dist/cjs/index.js +45 -0
  10. package/dist/cjs/index.js.map +1 -0
  11. package/dist/cjs/internal/ddl.js +436 -0
  12. package/dist/cjs/internal/ddl.js.map +1 -0
  13. package/dist/cjs/internal/matchSql.js +189 -0
  14. package/dist/cjs/internal/matchSql.js.map +1 -0
  15. package/dist/cjs/internal/readPlan.js +67 -0
  16. package/dist/cjs/internal/readPlan.js.map +1 -0
  17. package/dist/cjs/internal/subscribe.js +120 -0
  18. package/dist/cjs/internal/subscribe.js.map +1 -0
  19. package/dist/cjs/internal/transport.js +66 -0
  20. package/dist/cjs/internal/transport.js.map +1 -0
  21. package/dist/cjs/store.js +256 -0
  22. package/dist/cjs/store.js.map +1 -0
  23. package/dist/dts/clients.d.ts +34 -0
  24. package/dist/dts/clients.d.ts.map +1 -0
  25. package/dist/dts/config.d.ts +58 -0
  26. package/dist/dts/config.d.ts.map +1 -0
  27. package/dist/dts/ensureSchema.d.ts +35 -0
  28. package/dist/dts/ensureSchema.d.ts.map +1 -0
  29. package/dist/dts/index.d.ts +41 -0
  30. package/dist/dts/index.d.ts.map +1 -0
  31. package/dist/dts/internal/ddl.d.ts +174 -0
  32. package/dist/dts/internal/ddl.d.ts.map +1 -0
  33. package/dist/dts/internal/matchSql.d.ts +140 -0
  34. package/dist/dts/internal/matchSql.d.ts.map +1 -0
  35. package/dist/dts/internal/readPlan.d.ts +72 -0
  36. package/dist/dts/internal/readPlan.d.ts.map +1 -0
  37. package/dist/dts/internal/subscribe.d.ts +87 -0
  38. package/dist/dts/internal/subscribe.d.ts.map +1 -0
  39. package/dist/dts/internal/transport.d.ts +116 -0
  40. package/dist/dts/internal/transport.d.ts.map +1 -0
  41. package/dist/dts/store.d.ts +17 -0
  42. package/dist/dts/store.d.ts.map +1 -0
  43. package/dist/esm/clients.js +51 -0
  44. package/dist/esm/clients.js.map +1 -0
  45. package/dist/esm/config.js +87 -0
  46. package/dist/esm/config.js.map +1 -0
  47. package/dist/esm/ensureSchema.js +45 -0
  48. package/dist/esm/ensureSchema.js.map +1 -0
  49. package/dist/esm/index.js +40 -0
  50. package/dist/esm/index.js.map +1 -0
  51. package/dist/esm/internal/ddl.js +424 -0
  52. package/dist/esm/internal/ddl.js.map +1 -0
  53. package/dist/esm/internal/matchSql.js +176 -0
  54. package/dist/esm/internal/matchSql.js.map +1 -0
  55. package/dist/esm/internal/readPlan.js +59 -0
  56. package/dist/esm/internal/readPlan.js.map +1 -0
  57. package/dist/esm/internal/subscribe.js +113 -0
  58. package/dist/esm/internal/subscribe.js.map +1 -0
  59. package/dist/esm/internal/transport.js +56 -0
  60. package/dist/esm/internal/transport.js.map +1 -0
  61. package/dist/esm/package.json +4 -0
  62. package/dist/esm/store.js +249 -0
  63. package/dist/esm/store.js.map +1 -0
  64. package/package.json +35 -0
  65. package/src/clients.ts +69 -0
  66. package/src/config.ts +92 -0
  67. package/src/ensureSchema.ts +46 -0
  68. package/src/index.ts +45 -0
  69. package/src/internal/ddl.ts +599 -0
  70. package/src/internal/matchSql.ts +219 -0
  71. package/src/internal/readPlan.ts +117 -0
  72. package/src/internal/transport.ts +141 -0
  73. 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":[]}