@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
@@ -0,0 +1,116 @@
1
+ /**
2
+ * The JSON/JSONB transport for the Postgres engine: the wire shapes and the
3
+ * (de)serialisers that carry an event batch / query items ACROSS the boundary into
4
+ * the append functions, and map a raw driver row BACK to a core `SequencedEvent`.
5
+ * Extracted from `store.ts` (the write side) and `internal/readPlan.ts` (the read
6
+ * side) so the transport concept lives in ONE module rather than being split
7
+ * across the write path and the read-plan compiler — the module name now predicts
8
+ * where transport code lives, and the two per-column type maps (`EventJson`,
9
+ * `EventRow`) sit next to each other rather than in two files.
10
+ *
11
+ * The 5-column event contract stays single-sourced on `EventColumnName`
12
+ * (`internal/matchSql.ts`), the one column-NAME descriptor: both type maps here
13
+ * are mapped types over it, so a column rename in `EVENT_COLUMNS` is a COMPILE
14
+ * error in this module, never a silent transport drift the lax read boundary
15
+ * (ADR-0006) would only surface as corrupt data under test.
16
+ *
17
+ * The read boundary (`rowToSequenced`) is the trusted, deliberately-lax
18
+ * deserialisation point (ADR-0006 store-laxness); the write boundary
19
+ * (`toEventJson`) serialises `data` as base64 text and domain time as an ISO
20
+ * string, both restored in SQL.
21
+ */
22
+ import type { DcbEvent, Query, SequencedEvent } from '@kairos-es/core';
23
+ import type { EventColumnName } from './matchSql.js';
24
+ /**
25
+ * Serialise a value to a JSON STRING for transport as a `::jsonb`-cast bound
26
+ * parameter.
27
+ *
28
+ * WHY not `sql.json(value)`: `@effect/sql-pg`'s `json` helper binds the RAW
29
+ * value, and node-postgres serialises a JS *array* bound param as a Postgres
30
+ * ARRAY literal (`{…}`), not JSON — so `sql.json([...])::jsonb` fails with a
31
+ * `22P02` invalid-JSON error (an array literal is not valid JSON). Passing a
32
+ * pre-stringified JSON string and casting `::jsonb` sends plain text that
33
+ * Postgres parses as JSON, which round-trips reliably for both objects and
34
+ * arrays. Kept as a named helper so every JSON-transport call site is consistent
35
+ * and the reasoning lives in one place.
36
+ */
37
+ export declare const jsonbText: (value: unknown) => string;
38
+ /** The JSON shape of one query item: types OR-filter, tags AND-superset. */
39
+ export interface QueryItemJson {
40
+ readonly types: ReadonlyArray<string>;
41
+ readonly tags: ReadonlyArray<string>;
42
+ }
43
+ /** Serialise a `Query`'s items into their transport JSON shape. */
44
+ export declare const toQueryItemsJson: (query: Query) => ReadonlyArray<QueryItemJson>;
45
+ /**
46
+ * The per-column TS types of the transport JSON shape — one entry per
47
+ * `EVENT_COLUMNS` name. `data` is base64 TEXT and `occurred_at` an ISO string
48
+ * (both restored in SQL: `decode(…, 'base64')` / `timestamptz`); the rest map
49
+ * straight across. Kept as a plain map so `EventJson` below is a mapped type over
50
+ * the ONE column descriptor rather than a hand-restated column list.
51
+ */
52
+ interface EventJsonColumns {
53
+ readonly type: string;
54
+ readonly data: string;
55
+ readonly tags: ReadonlyArray<string>;
56
+ readonly uuid: string;
57
+ readonly occurred_at: string;
58
+ }
59
+ /**
60
+ * The JSON shape of one event as sent to the append functions, as a mapped type
61
+ * over the shared `EventColumnName` descriptor (`internal/matchSql.ts`). `data`
62
+ * is base64 text (restored to `bytea` by `decode(…, 'base64')` in plpgsql);
63
+ * `occurred_at` is an ISO string parsed by `timestamptz`. Because the keys come
64
+ * from the same descriptor that builds the `jsonb_to_recordset(new_events)`
65
+ * column list, the two cannot drift — rename a column in `EVENT_COLUMNS` and this
66
+ * mapped type stops compiling (the old key is gone from `EventJsonColumns`).
67
+ */
68
+ export type EventJson = {
69
+ readonly [K in EventColumnName]: EventJsonColumns[K];
70
+ };
71
+ /** Serialise one core `DcbEvent` into its transport JSON shape. */
72
+ export declare const toEventJson: (event: DcbEvent) => EventJson;
73
+ /**
74
+ * The per-column TS types of a raw driver row — one entry per `EVENT_COLUMNS`
75
+ * name. These are node-postgres's decoded JS types: `data` a `Buffer` (or null
76
+ * for a payload-less event), `occurred_at` a JS `Date`, `tags` a `string[]`. `id`
77
+ * (the `bigserial` position) is NOT here — it is prepended by the read projection
78
+ * and absent from the events recordset, so it sits outside the shared descriptor.
79
+ */
80
+ interface EventRowColumns {
81
+ readonly type: string;
82
+ readonly data: Uint8Array | null;
83
+ readonly tags: ReadonlyArray<string>;
84
+ readonly uuid: string;
85
+ readonly occurred_at: Date;
86
+ }
87
+ /**
88
+ * A raw row from the main table as node-postgres returns it, as `id` plus a
89
+ * mapped type over the shared `EventColumnName` descriptor. `id` is `int8`, which
90
+ * the driver hands back as a decimal STRING (JS `number` is unsafe past 2^53).
91
+ * The payload columns cannot drift from the read projection: a rename in
92
+ * `EVENT_COLUMNS` breaks this mapped type.
93
+ */
94
+ export type EventRow = {
95
+ readonly id: string;
96
+ } & {
97
+ readonly [K in EventColumnName]: EventRowColumns[K];
98
+ };
99
+ /**
100
+ * Map a raw main-table row to a core `SequencedEvent`. `data` NULL → empty bytes
101
+ * (a payload-less event round-trips to a zero-length `Uint8Array`); `occurred_at`
102
+ * Date → `DateTime.Utc`; `id` string → branded `Position`.
103
+ *
104
+ * The branded scalars (`type`, `tags`, `uuid`, `position`) are minted by trusted
105
+ * `as`-cast, NOT by re-running `EventType.make` / `Tag.make` etc. This is
106
+ * deliberate: these bytes were validated at write time, and the store schema is
107
+ * intentionally LAX on read (ADR-0006 store-laxness — hand-assembled or pre-ADR
108
+ * events must remain readable), so re-validating here could REJECT a legitimately
109
+ * stored value and break that contract. It mirrors the in-memory oracle, which
110
+ * likewise mints branded positions by cast at its trusted allocation boundary
111
+ * (`inMemory.ts`). The read boundary is the analogous trusted deserialisation
112
+ * point.
113
+ */
114
+ export declare const rowToSequenced: (row: EventRow) => SequencedEvent;
115
+ export {};
116
+ //# sourceMappingURL=transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../../src/internal/transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAY,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAEjD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,SAAS,GAAI,OAAO,OAAO,KAAG,MAA+B,CAAA;AAE1E,4EAA4E;AAC5E,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IACrC,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;CACrC;AAED,mEAAmE;AACnE,eAAO,MAAM,gBAAgB,GAAI,OAAO,KAAK,KAAG,aAAa,CAAC,aAAa,CAItE,CAAA;AAEL;;;;;;GAMG;AACH,UAAU,gBAAgB;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAC7B;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,SAAS,GAAG;IAAE,QAAQ,EAAE,CAAC,IAAI,eAAe,GAAG,gBAAgB,CAAC,CAAC,CAAC;CAAE,CAAA;AAEhF,mEAAmE;AACnE,eAAO,MAAM,WAAW,GAAI,OAAO,QAAQ,KAAG,SAQ5C,CAAA;AAEF;;;;;;GAMG;AACH,UAAU,eAAe;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,CAAA;IAChC,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,WAAW,EAAE,IAAI,CAAA;CAC3B;AAED;;;;;;GAMG;AACH,MAAM,MAAM,QAAQ,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG;IAC/C,QAAQ,EAAE,CAAC,IAAI,eAAe,GAAG,eAAe,CAAC,CAAC,CAAC;CACpD,CAAA;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,QAAQ,KAAG,cAS7C,CAAA"}
@@ -0,0 +1,17 @@
1
+ import { DcbEventStore } from '@kairos-es/core';
2
+ import { Layer } from 'effect';
3
+ import { PgClientDirect, PgClientPooled } from './clients.js';
4
+ import { type PostgresStoreConfig } from './config.js';
5
+ /**
6
+ * The Postgres `DcbEventStore` layer factory. Builds the engine over
7
+ * `PgClientPooled` (reads/append/notify) and `PgClientDirect` (the subscribe
8
+ * listener). The caller wires those two client layers — pointing `PgClientDirect`
9
+ * at a direct (non-pooler) endpoint on Neon, or at the same URL as `PgClientPooled`
10
+ * everywhere else.
11
+ *
12
+ * `ensureSchema(config)` must have been run (by the caller's migrator or at
13
+ * boot) before the layer is used; the layer itself performs no DDL, so it stays
14
+ * a pure runtime dependency requiring only the two clients.
15
+ */
16
+ export declare const layer: (config?: PostgresStoreConfig) => Layer.Layer<DcbEventStore, never, PgClientPooled | PgClientDirect>;
17
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/store.ts"],"names":[],"mappings":"AAoDA,OAAO,EAGL,aAAa,EAQd,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAA6B,KAAK,EAAsB,MAAM,QAAQ,CAAA;AAC7E,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAC1D,OAAO,EAA6B,KAAK,mBAAmB,EAAE,MAAM,UAAU,CAAA;AAoU9E;;;;;;;;;;GAUG;AACH,eAAO,MAAM,KAAK,GAChB,SAAQ,mBAAwB,KAC/B,KAAK,CAAC,KAAK,CAAC,aAAa,EAAE,KAAK,EAAE,cAAc,GAAG,cAAc,CASnE,CAAA"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The TWO store-owned Postgres clients — the "Neon trap" mitigation (ADR-0002).
3
+ *
4
+ * `@effect/sql-pg` exposes a single `PgClient` (and `SqlClient`) tag; a
5
+ * `PgClient.layer` claims BOTH. If we built two `PgClient.layer`s they would
6
+ * collide on that one tag — the second would shadow the first. So instead we
7
+ * define two DISTINCT `Context.Tag`s, each HOLDING a `PgClient` value:
8
+ *
9
+ * - `PgClientPooled` — the pooled client used for reads, the append function,
10
+ * and `notify`. Backed by a `pg.Pool`; connections are recycled per query.
11
+ * - `PgClientDirect` — a client used ONLY by `subscribe`'s `listen`. Its
12
+ * built-in `listen` constructs a STANDALONE `pg.Client` from the pool's
13
+ * `options`, inheriting the pool's host. On a Neon POOLED endpoint
14
+ * (`…-pooler…`) that standalone connection lands on the PgBouncer front door,
15
+ * which SILENTLY DROPS `LISTEN`/`NOTIFY` — the subscription would simply never
16
+ * wake. Pointing `PgClientDirect` at Neon's DIRECT (non-pooler) endpoint fixes
17
+ * it. `subscribe` is the ONLY method that touches `PgClientDirect`.
18
+ *
19
+ * For a plain (non-Neon) Postgres both tags point at the same URL; the split
20
+ * costs nothing there. The two-tag design is what makes the Neon fix a
21
+ * deployment-time wiring choice rather than a code change.
22
+ */
23
+ import { Reactivity } from '@effect/experimental';
24
+ import { PgClient } from '@effect/sql-pg';
25
+ import { Context, Layer } from 'effect';
26
+ /**
27
+ * The pooled client tag: reads, the append function, and `notify`. Holds a
28
+ * `PgClient` value rather than being a `PgClient.layer`, so it never collides
29
+ * with the direct client on the shared `PgClient`/`SqlClient` tag.
30
+ */
31
+ export class PgClientPooled extends /*#__PURE__*/Context.Tag('@kairos-es/store-postgres/PgClientPooled')() {}
32
+ /**
33
+ * The direct (non-pooler) client tag: the `subscribe` listener ONLY. Point this
34
+ * at a direct endpoint on Neon so `LISTEN`/`NOTIFY` is not dropped by the pooler.
35
+ */
36
+ export class PgClientDirect extends /*#__PURE__*/Context.Tag('@kairos-es/store-postgres/PgClientDirect')() {}
37
+ /**
38
+ * Build a `PgClientPooled` layer from a `PgClient` config. We construct the
39
+ * client via `PgClient.make` (scoped) and place it in our OWN tag, then provide
40
+ * `Reactivity.layer` (which `PgClient.make` requires) — exactly what the
41
+ * built-in `PgClient.layer` does internally, minus the collision-prone claim on
42
+ * the shared `PgClient` tag.
43
+ */
44
+ export const pooledLayer = config => Layer.scoped(PgClientPooled, PgClient.make(config)).pipe(Layer.provide(Reactivity.layer));
45
+ /**
46
+ * Build a `PgClientDirect` layer from a `PgClient` config. Same construction as
47
+ * `pooledLayer`; the two differ only in which endpoint the caller points them
48
+ * at (and that only matters on Neon).
49
+ */
50
+ export const directLayer = config => Layer.scoped(PgClientDirect, PgClient.make(config)).pipe(Layer.provide(Reactivity.layer));
51
+ //# sourceMappingURL=clients.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clients.js","names":["Reactivity","PgClient","Context","Layer","PgClientPooled","Tag","PgClientDirect","pooledLayer","config","scoped","make","pipe","provide","layer","directLayer"],"sources":["../../src/clients.ts"],"sourcesContent":[null],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;AAsBA,SAASA,UAAU,QAAQ,sBAAsB;AAEjD,SAASC,QAAQ,QAAQ,gBAAgB;AACzC,SAASC,OAAO,EAAEC,KAAK,QAAQ,QAAQ;AAEvC;;;;;AAKA,OAAM,MAAOC,cAAe,sBAAQF,OAAO,CAACG,GAAG,CAC7C,0CAA0C,CAC3C,EAAqC;AAEtC;;;;AAIA,OAAM,MAAOC,cAAe,sBAAQJ,OAAO,CAACG,GAAG,CAC7C,0CAA0C,CAC3C,EAAqC;AAEtC;;;;;;;AAOA,OAAO,MAAME,WAAW,GACtBC,MAA+B,IAE/BL,KAAK,CAACM,MAAM,CAACL,cAAc,EAAEH,QAAQ,CAACS,IAAI,CAACF,MAAM,CAAC,CAAC,CAACG,IAAI,CACtDR,KAAK,CAACS,OAAO,CAACZ,UAAU,CAACa,KAAK,CAAC,CAChC;AAEH;;;;;AAKA,OAAO,MAAMC,WAAW,GACtBN,MAA+B,IAE/BL,KAAK,CAACM,MAAM,CAACH,cAAc,EAAEL,QAAQ,CAACS,IAAI,CAACF,MAAM,CAAC,CAAC,CAACG,IAAI,CACtDR,KAAK,CAACS,OAAO,CAACZ,UAAU,CAACa,KAAK,CAAC,CAChC","ignoreList":[]}
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Public configuration for a Postgres `DcbEventStore`.
3
+ *
4
+ * A store is defined entirely by its `{ schema, tablePrefix }` naming pair plus
5
+ * a `lockTimeout`. Everything else — table/index/function names, the NOTIFY
6
+ * channel, the lock scope — is DERIVED from these (see `internal/ddl.ts`), so
7
+ * one config value is one completely isolated store. There is deliberately NO
8
+ * lockless option here: the exclusive-lock serialisation is not negotiable on the
9
+ * production surface — the lockless variant is a test-fixture-only seam, living in
10
+ * an internal module reachable only from the test graph.
11
+ *
12
+ * The config is a `Schema`, not a bare interface, so that malformed input fails
13
+ * LOUDLY at construction (`layer`/`ensureSchema` decode through it) rather than
14
+ * late and obscurely on the first DDL or append — the same fail-at-the-boundary
15
+ * discipline core applies to `Tag`/`ReadLimit`. It rejects: an empty or dotted
16
+ * `schema`/`tablePrefix` atom (a dot would break the qualified-name dot-split),
17
+ * a negative/non-finite `lockTimeout`, and — via `oversizedIdentifier` — any
18
+ * `{ schema, tablePrefix }` whose derived names would exceed Postgres's 63-byte
19
+ * identifier limit and be silently truncated.
20
+ */
21
+ import { subscriptionTuningFields } from '@kairos-es/core';
22
+ import { Schema } from 'effect';
23
+ import { oversizedIdentifier } from "./internal/ddl.js";
24
+ /**
25
+ * A single Postgres identifier atom: non-empty and dot-free. Dots are barred
26
+ * because `schema`/`tablePrefix` are composed into qualified names that
27
+ * `sql(identifier)` dot-splits, so an embedded dot would silently re-partition
28
+ * the name into the wrong schema/object.
29
+ */
30
+ const IdentifierAtom = /*#__PURE__*/Schema.String.pipe(/*#__PURE__*/Schema.nonEmptyString(), /*#__PURE__*/Schema.pattern(/^[^.]+$/, {
31
+ identifier: 'PostgresIdentifierAtom',
32
+ description: 'a non-empty identifier atom containing no "."'
33
+ }));
34
+ /**
35
+ * Configuration for a Postgres store instance.
36
+ *
37
+ * - `schema` — the Postgres schema that owns the store's objects (default
38
+ * `public`). `ensureSchema` creates it defensively.
39
+ * - `tablePrefix` — the base name for the store's tables/indexes/functions
40
+ * (default `dcb_events`). Two stores with different prefixes (or schemas)
41
+ * cohabit one database without interference.
42
+ * - `lockTimeout` — seconds passed to `set_config('lock_timeout', …, true)`
43
+ * inside every append, bounding how long a blocked writer waits on the
44
+ * exclusive lock. `0` (the default) means wait indefinitely — parity with the
45
+ * in-memory semaphore's unbounded wait, so the shared contract suite behaves
46
+ * identically across engines. A non-zero timeout that fires (`55P03`) is a
47
+ * Postgres-only outcome surfaced OFF the shared `append` contract (a defect),
48
+ * never as `AppendConditionFailed` — the in-memory oracle cannot produce it.
49
+ * - `pollInterval` — MILLISECONDS a live `subscribe` waits for a NOTIFY wake
50
+ * before re-reading from its cursor anyway, defaulting to core's
51
+ * `DEFAULT_POLL_INTERVAL_MILLIS`. Because this engine HAS a wake source, the
52
+ * value bounds worst-case live-tail latency rather than being it — only a
53
+ * subscriber no NOTIFY reaches waits the whole interval; lower it (e.g. in tests)
54
+ * to poll faster. See `packages/core/src/SubscribeMachine.ts` for what the poll
55
+ * guarantees and what a wake source is for.
56
+ * - `catchUpPageSize` — the `subscribe` reconcile page size, defaulting to core's
57
+ * `DEFAULT_CATCH_UP_PAGE_SIZE`: how many events a single catch-up/live read
58
+ * fetches before looping.
59
+ */
60
+ export const PostgresStoreConfig = /*#__PURE__*/Schema.Struct({
61
+ // `exact: true` keeps the decoded type at `key?: T` (never `T | undefined`),
62
+ // matching the repo's `exactOptionalPropertyTypes` discipline — an absent key,
63
+ // not an explicit `undefined`.
64
+ schema: Schema.optionalWith(IdentifierAtom, {
65
+ exact: true
66
+ }),
67
+ tablePrefix: Schema.optionalWith(IdentifierAtom, {
68
+ exact: true
69
+ }),
70
+ lockTimeout: Schema.optionalWith(Schema.Number.pipe(Schema.nonNegative(), Schema.finite()), {
71
+ exact: true
72
+ }),
73
+ // The two subscribe knobs are core's field definitions, not this engine's: see
74
+ // `subscriptionTuningFields`.
75
+ ...subscriptionTuningFields
76
+ }).pipe(/*#__PURE__*/
77
+ // Cross-field guard: the derived table/index/function/channel names must all
78
+ // fit Postgres's 63-byte identifier limit. Returning the message string fails
79
+ // the decode with it; `undefined`/`true` passes.
80
+ Schema.filter(config => oversizedIdentifier(config) ?? true));
81
+ /**
82
+ * Decode an unknown value into a validated `PostgresStoreConfig`, throwing a
83
+ * `ParseError` on any violation. Used by `layer`/`ensureSchema` so a bad config
84
+ * fails at construction time.
85
+ */
86
+ export const decodePostgresStoreConfig = /*#__PURE__*/Schema.decodeUnknownSync(PostgresStoreConfig);
87
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","names":["subscriptionTuningFields","Schema","oversizedIdentifier","IdentifierAtom","String","pipe","nonEmptyString","pattern","identifier","description","PostgresStoreConfig","Struct","schema","optionalWith","exact","tablePrefix","lockTimeout","Number","nonNegative","finite","filter","config","decodePostgresStoreConfig","decodeUnknownSync"],"sources":["../../src/config.ts"],"sourcesContent":[null],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;AAoBA,SAASA,wBAAwB,QAAQ,iBAAiB;AAC1D,SAASC,MAAM,QAAQ,QAAQ;AAC/B,SAASC,mBAAmB,QAAQ,mBAAgB;AAEpD;;;;;;AAMA,MAAMC,cAAc,gBAAGF,MAAM,CAACG,MAAM,CAACC,IAAI,cACvCJ,MAAM,CAACK,cAAc,EAAE,eACvBL,MAAM,CAACM,OAAO,CAAC,SAAS,EAAE;EACxBC,UAAU,EAAE,wBAAwB;EACpCC,WAAW,EAAE;CACd,CAAC,CACH;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,OAAO,MAAMC,mBAAmB,gBAAGT,MAAM,CAACU,MAAM,CAAC;EAC/C;EACA;EACA;EACAC,MAAM,EAAEX,MAAM,CAACY,YAAY,CAACV,cAAc,EAAE;IAAEW,KAAK,EAAE;EAAI,CAAE,CAAC;EAC5DC,WAAW,EAAEd,MAAM,CAACY,YAAY,CAACV,cAAc,EAAE;IAAEW,KAAK,EAAE;EAAI,CAAE,CAAC;EACjEE,WAAW,EAAEf,MAAM,CAACY,YAAY,CAC9BZ,MAAM,CAACgB,MAAM,CAACZ,IAAI,CAACJ,MAAM,CAACiB,WAAW,EAAE,EAAEjB,MAAM,CAACkB,MAAM,EAAE,CAAC,EACzD;IAAEL,KAAK,EAAE;EAAI,CAAE,CAChB;EACD;EACA;EACA,GAAGd;CACJ,CAAC,CAACK,IAAI;AACL;AACA;AACA;AACAJ,MAAM,CAACmB,MAAM,CAAEC,MAAM,IAAKnB,mBAAmB,CAACmB,MAAM,CAAC,IAAI,IAAI,CAAC,CAC/D;AAGD;;;;;AAKA,OAAO,MAAMC,yBAAyB,gBACpCrB,MAAM,CAACsB,iBAAiB,CAACb,mBAAmB,CAAC","ignoreList":[]}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * `ensureSchema` — the idempotent DDL migration building block (ADR-0002).
3
+ *
4
+ * This is deliberately NOT a migrator. It is a single `Effect<void, SqlError,
5
+ * SqlClient>` that creates (or refreshes) every object a store owns: both
6
+ * tables, both indexes, and both plpgsql functions. It requires the GENERIC
7
+ * `SqlClient` tag — NOT `PgClientPooled` — precisely so a consumer can drop it
8
+ * straight into THEIR OWN migrator, e.g.:
9
+ *
10
+ * ```ts
11
+ * Migrator.fromRecord({
12
+ * '0001_kairos_store_dcb_events': ensureSchema(config),
13
+ * })
14
+ * ```
15
+ *
16
+ * or simply run it once at boot. We ship no `FileSystem`/`Path`/`ChildProcess`
17
+ * dependency and no `PgMigrator`; migration ORCHESTRATION belongs to the
18
+ * consumer, this package only provides the idempotent PAYLOAD.
19
+ *
20
+ * Idempotence: every statement is `CREATE … IF NOT EXISTS` or `CREATE OR REPLACE
21
+ * FUNCTION`, so re-running against an existing store is a no-op. Names come from
22
+ * `sql(identifier)` (via the pre-quoted builder in `internal/ddl.ts`); the DDL
23
+ * bodies carry no bound values (pure schema), and the append functions keep
24
+ * their runtime VALUES as bound parameters, never interpolated.
25
+ */
26
+ import { SqlClient } from '@effect/sql';
27
+ import { Effect } from 'effect';
28
+ import { decodePostgresStoreConfig } from "./config.js";
29
+ import { resolveNames, runDdl } from "./internal/ddl.js";
30
+ /**
31
+ * Produce the migration payload for `config`. Resolves the object names once and
32
+ * runs the ordered idempotent DDL against whatever `SqlClient` the caller's
33
+ * migrator (or boot code) provides.
34
+ */
35
+ export const ensureSchema = config => Effect.gen(function* () {
36
+ const sql = yield* SqlClient.SqlClient;
37
+ // Decode through the config schema (same guard as `layer`) so a malformed
38
+ // config fails at construction rather than mid-migration.
39
+ const names = resolveNames(decodePostgresStoreConfig(config));
40
+ // Sequentially, in dependency order (tables → indexes → FK → functions).
41
+ yield* Effect.all(runDdl(sql, names), {
42
+ discard: true
43
+ });
44
+ });
45
+ //# sourceMappingURL=ensureSchema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ensureSchema.js","names":["SqlClient","Effect","decodePostgresStoreConfig","resolveNames","runDdl","ensureSchema","config","gen","sql","names","all","discard"],"sources":["../../src/ensureSchema.ts"],"sourcesContent":[null],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,SAASA,SAAS,QAAuB,aAAa;AACtD,SAASC,MAAM,QAAQ,QAAQ;AAC/B,SAASC,yBAAyB,QAAkC,aAAU;AAC9E,SAASC,YAAY,EAAEC,MAAM,QAAQ,mBAAgB;AAErD;;;;;AAKA,OAAO,MAAMC,YAAY,GACvBC,MAA2B,IAE3BL,MAAM,CAACM,GAAG,CAAC,aAAS;EAClB,MAAMC,GAAG,GAAG,OAAOR,SAAS,CAACA,SAAS;EACtC;EACA;EACA,MAAMS,KAAK,GAAGN,YAAY,CAACD,yBAAyB,CAACI,MAAM,CAAC,CAAC;EAC7D;EACA,OAAOL,MAAM,CAACS,GAAG,CAACN,MAAM,CAACI,GAAG,EAAEC,KAAK,CAAC,EAAE;IAAEE,OAAO,EAAE;EAAI,CAAE,CAAC;AAC1D,CAAC,CAAC","ignoreList":[]}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @kairos-es/store-postgres — the Postgres `DcbEventStore` backend.
3
+ *
4
+ * A single store is one `{ schema, tablePrefix }` naming pair (`PostgresStoreConfig`);
5
+ * every object it owns is derived from that pair, so multiple isolated stores
6
+ * cohabit one database. The engine is the two-table B-tree + exclusive-lock
7
+ * conditional append of ADR-0002, with JSON transport and a two-client Layer to
8
+ * survive Neon's pooled LISTEN/NOTIFY drop.
9
+ *
10
+ * Wiring, end to end:
11
+ *
12
+ * ```ts
13
+ * import { PgClient } from '@effect/sql-pg'
14
+ * import {
15
+ * ensureSchema,
16
+ * layer,
17
+ * pooledLayer,
18
+ * directLayer,
19
+ * } from '@kairos-es/store-postgres'
20
+ *
21
+ * const config = { schema: 'public', tablePrefix: 'dcb_events' }
22
+ *
23
+ * // 1. Register `ensureSchema(config)` in YOUR migrator (or run it at boot).
24
+ * // 2. Provide the two clients (same URL off Neon; a direct endpoint for
25
+ * // `directLayer` on Neon), then the store layer over them.
26
+ * const clients = Layer.merge(
27
+ * pooledLayer({ url: Redacted.make(poolUrl) }),
28
+ * directLayer({ url: Redacted.make(directUrl) }),
29
+ * )
30
+ * const store = layer(config).pipe(Layer.provide(clients))
31
+ * ```
32
+ *
33
+ * The store `layer` performs NO DDL: it depends only on the two client tags and
34
+ * assumes `ensureSchema` has run. `ensureSchema` requires the generic `SqlClient`
35
+ * tag so it slots into any migrator; the runtime uses the store-owned tags.
36
+ */
37
+ export { directLayer, PgClientDirect, PgClientPooled, pooledLayer } from "./clients.js";
38
+ export { ensureSchema } from "./ensureSchema.js";
39
+ export { layer } from "./store.js";
40
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["directLayer","PgClientDirect","PgClientPooled","pooledLayer","ensureSchema","layer"],"sources":["../../src/index.ts"],"sourcesContent":[null],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,SACEA,WAAW,EACXC,cAAc,EACdC,cAAc,EACdC,WAAW,QACN,cAAW;AAElB,SAASC,YAAY,QAAQ,mBAAgB;AAC7C,SAASC,KAAK,QAAQ,YAAS","ignoreList":[]}