@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,67 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.runFetch = exports.compileFetchPlan = void 0;
7
+ var _core = require("@kairos-es/core");
8
+ var _matchSql = require("./matchSql.js");
9
+ var _transport = require("./transport.js");
10
+ /**
11
+ * Compile a servable query into a `FetchPlan`. The SQL comes from the shared
12
+ * `matchSql` builders (the tagged path composes the same emitted match chain as the
13
+ * conflict check, which binds its own placeholder tokens and wraps it differently),
14
+ * with the pre-quoted names embedded and the VALUES bound as `$n` parameters
15
+ * (never interpolated).
16
+ *
17
+ * The `switch` is exhaustive over core's `ServableQueryShape` with NO `default`
18
+ * and no trailing fall-through arm, and what that buys is a COMPILE error where
19
+ * there would otherwise be a silent mis-serve. Core owns that union and this
20
+ * module only consumes it, so a fourth member added there is a change this file
21
+ * cannot see coming: a cascade ending in an unguarded tagged branch would have
22
+ * SERVED that shape as tagged, and both `read` and the `subscribe` poll sharing
23
+ * its plan would have returned the wrong rows — no error raised, nothing failing
24
+ * to typecheck. Every arm returns instead, so the added member leaves this
25
+ * function with a reachable end and a return type that does not admit
26
+ * `undefined`, and the module stops compiling.
27
+ */
28
+ const compileFetchPlan = (tables, query) => {
29
+ const shape = (0, _core.classifyServableQuery)(query);
30
+ switch (shape.kind) {
31
+ case 'wildcard':
32
+ return {
33
+ text: (0, _matchSql.wildcardReadSql)(tables.main),
34
+ bind: (after, limit) => [after, limit]
35
+ };
36
+ case 'typeOnly':
37
+ {
38
+ // node-postgres binds a JS array param as a Postgres array literal, so
39
+ // `type = ANY($2)` matches any of the item's types. The item's own list is
40
+ // bound as it stands: a branded `EventType` IS its string to the driver, so
41
+ // nothing here has to unwrap or copy it.
42
+ const types = shape.types;
43
+ return {
44
+ text: (0, _matchSql.typeOnlyReadSql)(tables.main),
45
+ bind: (after, limit) => [after, types, limit]
46
+ };
47
+ }
48
+ case 'tagged':
49
+ {
50
+ // `after` ($2) is applied at BOTH CTE stages; `limit` ($3) inside the
51
+ // ordered filtered_ids so it caps by position, not on the outer join.
52
+ const itemsJson = (0, _transport.jsonbText)((0, _transport.toQueryItemsJson)(query));
53
+ return {
54
+ text: (0, _matchSql.taggedReadSql)(tables.main, tables.tag),
55
+ bind: (after, limit) => [itemsJson, after, limit]
56
+ };
57
+ }
58
+ }
59
+ };
60
+ /**
61
+ * Run a compiled plan for a given `after`/`limit`, honouring `after` (exclusive;
62
+ * absent → `0`) and `limit` (`NULL` = no limit), ordered by `id ASC`.
63
+ */
64
+ exports.compileFetchPlan = compileFetchPlan;
65
+ const runFetch = (sql, plan, after, limit) => sql.unsafe(plan.text, plan.bind(after ?? _core.ORIGIN, limit ?? null));
66
+ exports.runFetch = runFetch;
67
+ //# sourceMappingURL=readPlan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"readPlan.js","names":["_core","require","_matchSql","_transport","compileFetchPlan","tables","query","shape","classifyServableQuery","kind","text","wildcardReadSql","main","bind","after","limit","types","typeOnlyReadSql","itemsJson","jsonbText","toQueryItemsJson","taggedReadSql","tag","exports","runFetch","sql","plan","unsafe","ORIGIN"],"sources":["../../../src/internal/readPlan.ts"],"sourcesContent":[null],"mappings":";;;;;;AAyBA,IAAAA,KAAA,GAAAC,OAAA;AAEA,IAAAC,SAAA,GAAAD,OAAA;AACA,IAAAE,UAAA,GAAAF,OAAA;AA0BA;;;;;;;;;;;;;;;;;;AAkBO,MAAMG,gBAAgB,GAAGA,CAC9BC,MAAkB,EAClBC,KAAY,KACC;EACb,MAAMC,KAAK,GAAG,IAAAC,2BAAqB,EAACF,KAAK,CAAC;EAC1C,QAAQC,KAAK,CAACE,IAAI;IAChB,KAAK,UAAU;MACb,OAAO;QACLC,IAAI,EAAE,IAAAC,yBAAe,EAACN,MAAM,CAACO,IAAI,CAAC;QAClCC,IAAI,EAAEA,CAACC,KAAK,EAAEC,KAAK,KAAK,CAACD,KAAK,EAAEC,KAAK;OACtC;IACH,KAAK,UAAU;MAAE;QACf;QACA;QACA;QACA;QACA,MAAMC,KAAK,GAAGT,KAAK,CAACS,KAAK;QACzB,OAAO;UACLN,IAAI,EAAE,IAAAO,yBAAe,EAACZ,MAAM,CAACO,IAAI,CAAC;UAClCC,IAAI,EAAEA,CAACC,KAAK,EAAEC,KAAK,KAAK,CAACD,KAAK,EAAEE,KAAK,EAAED,KAAK;SAC7C;MACH;IACA,KAAK,QAAQ;MAAE;QACb;QACA;QACA,MAAMG,SAAS,GAAG,IAAAC,oBAAS,EAAC,IAAAC,2BAAgB,EAACd,KAAK,CAAC,CAAC;QACpD,OAAO;UACLI,IAAI,EAAE,IAAAW,uBAAa,EAAChB,MAAM,CAACO,IAAI,EAAEP,MAAM,CAACiB,GAAG,CAAC;UAC5CT,IAAI,EAAEA,CAACC,KAAK,EAAEC,KAAK,KAAK,CAACG,SAAS,EAAEJ,KAAK,EAAEC,KAAK;SACjD;MACH;EACF;AACF,CAAC;AAED;;;;AAAAQ,OAAA,CAAAnB,gBAAA,GAAAA,gBAAA;AAIO,MAAMoB,QAAQ,GAAGA,CACtBC,GAAwB,EACxBC,IAAe,EACfZ,KAA2B,EAC3BC,KAA4B,KAE5BU,GAAG,CAACE,MAAM,CAAWD,IAAI,CAAChB,IAAI,EAAEgB,IAAI,CAACb,IAAI,CAACC,KAAK,IAAIc,YAAM,EAAEb,KAAK,IAAI,IAAI,CAAC,CAAC;AAAAQ,OAAA,CAAAC,QAAA,GAAAA,QAAA","ignoreList":[]}
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.subscribeStream = exports.DEFAULT_POLL_INTERVAL_MILLIS = exports.DEFAULT_CATCH_UP_PAGE = void 0;
7
+ var _core = require("@kairos-es/core");
8
+ var _effect = require("effect");
9
+ /**
10
+ * A short catch-up page size default for the reconcile loop. Reads page by page
11
+ * until a short page signals the tail is drained, so a large backlog is streamed
12
+ * in bounded chunks rather than one huge SELECT. Overridable via
13
+ * `config.catchUpPageSize`.
14
+ */
15
+ const DEFAULT_CATCH_UP_PAGE = exports.DEFAULT_CATCH_UP_PAGE = 512;
16
+ /**
17
+ * The default upper bound a live subscriber waits for a wake before re-reading
18
+ * from its cursor ANYWAY (milliseconds; overridable via `config.pollInterval`).
19
+ * This periodic position-based poll — not `NOTIFY` — is the correctness
20
+ * guarantee: every committed position is reconciled within this interval
21
+ * regardless of whether its `NOTIFY` was heard, which covers a `NOTIFY` fired
22
+ * before `LISTEN` was fully established, a coalesced/lost `NOTIFY`, and a listen
23
+ * connection that dropped SILENTLY (the `@effect/sql-pg` listen client swallows
24
+ * connection errors and does not auto-reconnect). `NOTIFY` only lowers delivery
25
+ * latency below this interval. Upstream `postgres_tt.py` uses the same 1-second
26
+ * fallback poll (`conn.wait(..., interval=1)`).
27
+ */
28
+ const DEFAULT_POLL_INTERVAL_MILLIS = exports.DEFAULT_POLL_INTERVAL_MILLIS = 1000;
29
+ /**
30
+ * Retry a TRANSIENT poll fault (a `SqlError` from one reconcile read) on a capped
31
+ * exponential backoff rather than letting `Stream.orDie` kill a subscription that
32
+ * may have been alive for days over a single connection blip. A fault that
33
+ * PERSISTS past the cap propagates (the decorator dies it) — infrastructure stays
34
+ * off the contract channel while the stream survives ordinary transients.
35
+ */
36
+ const POLL_RETRY_SCHEDULE = /*#__PURE__*/_effect.Schedule.exponential('100 millis', 2).pipe(/*#__PURE__*/_effect.Schedule.union(/*#__PURE__*/_effect.Schedule.spaced('2 seconds')), /*#__PURE__*/_effect.Schedule.intersect(/*#__PURE__*/_effect.Schedule.recurs(20)));
37
+ /**
38
+ * Build the subscription `Stream`: catch up from `after`, then track the live
39
+ * tail. The cursor threads through the `unfoldChunkEffect` unfold state, starting
40
+ * at `after` and advancing to the last delivered position each batch; the stream
41
+ * never completes on its own (always `Option.some`), so the enclosing scope
42
+ * interrupts it on close. A `SqlError` mid-stream stays on the channel;
43
+ * `makeContractStore` dies it (subscribe is `E = never` on the contract).
44
+ */
45
+ const subscribeStream = (deps, after) => _effect.Stream.unwrapScoped(_effect.Effect.gen(function* () {
46
+ // The wake primitive: a sliding(1) queue holding at most one pending
47
+ // signal. The listener `offer`s an opaque wake on every NOTIFY; the poll
48
+ // loop `take`s (with a timeout). The lost-wakeup guard is STRUCTURAL —
49
+ // drain the queue BEFORE reading, so a NOTIFY that lands mid-read leaves
50
+ // a fresh signal that makes the next `take` return at once rather than
51
+ // sleeping through a real change.
52
+ const wake = yield* _effect.Queue.sliding(1);
53
+ // Tap every notification into the wake queue. If the listen stream DOES
54
+ // surface an error, re-subscribe with capped-exponential backoff. In
55
+ // practice the @effect/sql-pg listen client usually swallows a dropped
56
+ // connection SILENTLY rather than erroring, so this retry rarely fires —
57
+ // the poll fallback below, not this retry, is the real reconnect guarantee:
58
+ // the reconcile loop re-reads from the cursor on every poll, so a silently-
59
+ // dropped (and later re-established) listener loses no positions.
60
+ const listener = deps.listen.pipe(_effect.Stream.tap(() => _effect.Queue.offer(wake, undefined)), _effect.Stream.retry(_effect.Schedule.exponential('50 millis').pipe(_effect.Schedule.union(_effect.Schedule.spaced('1 seconds')))));
61
+ // Drain the listener into the wake queue on a background fibre; the scope
62
+ // releases it. We never read its elements directly — every notification is
63
+ // an opaque wake signal.
64
+ yield* listener.pipe(_effect.Stream.runDrain, _effect.Effect.forkScoped);
65
+ // Reconcile: page-read from `from` until a short page, advancing to the last
66
+ // delivered position. Retries a TRANSIENT poll fault on a capped schedule so
67
+ // a single connection blip does not kill a days-long subscription; a
68
+ // persistent fault propagates and dies.
69
+ const drainFrom = from => _effect.Effect.gen(function* () {
70
+ const collected = [];
71
+ let cursor = from;
72
+ // Loop pages until a short page signals the tail is drained.
73
+ while (true) {
74
+ const events = yield* deps.fetch(cursor, _core.ReadLimit.make(deps.catchUpPage));
75
+ for (const event of events) {
76
+ collected.push(event);
77
+ }
78
+ const lastEvent = events.at(-1);
79
+ if (lastEvent === undefined || events.length < deps.catchUpPage) {
80
+ break;
81
+ }
82
+ cursor = lastEvent.position;
83
+ }
84
+ const last = collected.at(-1)?.position ?? from;
85
+ return {
86
+ events: collected,
87
+ last
88
+ };
89
+ }).pipe(_effect.Effect.retry(POLL_RETRY_SCHEDULE));
90
+ /**
91
+ * Produce the NEXT non-empty batch from a cursor position, together with
92
+ * the advanced cursor — a pure `Position → Effect<[Chunk, next]>`. The
93
+ * `> boundary` de-dup is enforced by `fetch`'s exclusive lower bound, so a
94
+ * live re-read after a wake never re-emits an already-delivered position.
95
+ *
96
+ * Drain the wake queue BEFORE reading (lost-wakeup guard), then drain from
97
+ * the cursor. If a drain yields nothing, wait for a wake but only up to the
98
+ * poll interval, then loop and re-read ANYWAY — the periodic position-based
99
+ * poll that makes delivery correct independently of NOTIFY. This effect only
100
+ * completes by returning a non-empty chunk; the scope interrupts it on close.
101
+ */
102
+ const nextBatch = from => _effect.Effect.gen(function* () {
103
+ while (true) {
104
+ yield* _effect.Queue.takeAll(wake);
105
+ const {
106
+ events,
107
+ last
108
+ } = yield* drainFrom(from);
109
+ if (events.length > 0) {
110
+ return [_effect.Chunk.fromIterable(events), last];
111
+ }
112
+ // Nothing new: block on a wake, but only up to the poll interval — on
113
+ // timeout we fall through and the loop re-reads regardless.
114
+ yield* _effect.Effect.ignore(_effect.Effect.timeout(_effect.Queue.take(wake), deps.pollInterval));
115
+ }
116
+ });
117
+ return _effect.Stream.unfoldChunkEffect(after ?? _core.ORIGIN, from => _effect.Effect.map(nextBatch(from), _effect.Option.some));
118
+ }));
119
+ exports.subscribeStream = subscribeStream;
120
+ //# sourceMappingURL=subscribe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subscribe.js","names":["_core","require","_effect","DEFAULT_CATCH_UP_PAGE","exports","DEFAULT_POLL_INTERVAL_MILLIS","POLL_RETRY_SCHEDULE","Schedule","exponential","pipe","union","spaced","intersect","recurs","subscribeStream","deps","after","Stream","unwrapScoped","Effect","gen","wake","Queue","sliding","listener","listen","tap","offer","undefined","retry","runDrain","forkScoped","drainFrom","from","collected","cursor","events","fetch","ReadLimit","make","catchUpPage","event","push","lastEvent","at","length","position","last","nextBatch","takeAll","Chunk","fromIterable","ignore","timeout","take","pollInterval","unfoldChunkEffect","ORIGIN","map","Option","some"],"sources":["../../../src/internal/subscribe.ts"],"sourcesContent":[null],"mappings":";;;;;;AAgCA,IAAAA,KAAA,GAAAC,OAAA;AACA,IAAAC,OAAA,GAAAD,OAAA;AAUA;;;;;;AAMO,MAAME,qBAAqB,GAAAC,OAAA,CAAAD,qBAAA,GAAG,GAAG;AAExC;;;;;;;;;;;;AAYO,MAAME,4BAA4B,GAAAD,OAAA,CAAAC,4BAAA,GAAG,IAAI;AAEhD;;;;;;;AAOA,MAAMC,mBAAmB,gBAAGC,gBAAQ,CAACC,WAAW,CAAC,YAAY,EAAE,CAAC,CAAC,CAACC,IAAI,cACpEF,gBAAQ,CAACG,KAAK,cAACH,gBAAQ,CAACI,MAAM,CAAC,WAAW,CAAC,CAAC,eAC5CJ,gBAAQ,CAACK,SAAS,cAACL,gBAAQ,CAACM,MAAM,CAAC,EAAE,CAAC,CAAC,CACxC;AA6BD;;;;;;;;AAQO,MAAMC,eAAe,GAAGA,CAC7BC,IAAsB,EACtBC,KAA2B,KAE3BC,cAAM,CAACC,YAAY,CACjBC,cAAM,CAACC,GAAG,CAAC,aAAS;EAClB;EACA;EACA;EACA;EACA;EACA;EACA,MAAMC,IAAI,GAAG,OAAOC,aAAK,CAACC,OAAO,CAAO,CAAC,CAAC;EAE1C;EACA;EACA;EACA;EACA;EACA;EACA;EACA,MAAMC,QAAQ,GAAGT,IAAI,CAACU,MAAM,CAAChB,IAAI,CAC/BQ,cAAM,CAACS,GAAG,CAAC,MAAMJ,aAAK,CAACK,KAAK,CAACN,IAAI,EAAEO,SAAS,CAAC,CAAC,EAC9CX,cAAM,CAACY,KAAK,CACVtB,gBAAQ,CAACC,WAAW,CAAC,WAAW,CAAC,CAACC,IAAI,CACpCF,gBAAQ,CAACG,KAAK,CAACH,gBAAQ,CAACI,MAAM,CAAC,WAAW,CAAC,CAAC,CAC7C,CACF,CACF;EACD;EACA;EACA;EACA,OAAOa,QAAQ,CAACf,IAAI,CAACQ,cAAM,CAACa,QAAQ,EAAEX,cAAM,CAACY,UAAU,CAAC;EAExD;EACA;EACA;EACA;EACA,MAAMC,SAAS,GACbC,IAAc,IAQdd,cAAM,CAACC,GAAG,CAAC,aAAS;IAClB,MAAMc,SAAS,GAA0B,EAAE;IAC3C,IAAIC,MAAM,GAAGF,IAAI;IACjB;IACA,OAAO,IAAI,EAAE;MACX,MAAMG,MAAM,GAAG,OAAOrB,IAAI,CAACsB,KAAK,CAC9BF,MAAM,EACNG,eAAS,CAACC,IAAI,CAACxB,IAAI,CAACyB,WAAW,CAAC,CACjC;MACD,KAAK,MAAMC,KAAK,IAAIL,MAAM,EAAE;QAC1BF,SAAS,CAACQ,IAAI,CAACD,KAAK,CAAC;MACvB;MACA,MAAME,SAAS,GAAGP,MAAM,CAACQ,EAAE,CAAC,CAAC,CAAC,CAAC;MAC/B,IAAID,SAAS,KAAKf,SAAS,IAAIQ,MAAM,CAACS,MAAM,GAAG9B,IAAI,CAACyB,WAAW,EAAE;QAC/D;MACF;MACAL,MAAM,GAAGQ,SAAS,CAACG,QAAQ;IAC7B;IACA,MAAMC,IAAI,GAAGb,SAAS,CAACU,EAAE,CAAC,CAAC,CAAC,CAAC,EAAEE,QAAQ,IAAIb,IAAI;IAC/C,OAAO;MAAEG,MAAM,EAAEF,SAAS;MAAEa;IAAI,CAAE;EACpC,CAAC,CAAC,CAACtC,IAAI,CAACU,cAAM,CAACU,KAAK,CAACvB,mBAAmB,CAAC,CAAC;EAE5C;;;;;;;;;;;;EAYA,MAAM0C,SAAS,GACbf,IAAc,IAKdd,cAAM,CAACC,GAAG,CAAC,aAAS;IAClB,OAAO,IAAI,EAAE;MACX,OAAOE,aAAK,CAAC2B,OAAO,CAAC5B,IAAI,CAAC;MAC1B,MAAM;QAAEe,MAAM;QAAEW;MAAI,CAAE,GAAG,OAAOf,SAAS,CAACC,IAAI,CAAC;MAC/C,IAAIG,MAAM,CAACS,MAAM,GAAG,CAAC,EAAE;QACrB,OAAO,CAACK,aAAK,CAACC,YAAY,CAACf,MAAM,CAAC,EAAEW,IAAI,CAAU;MACpD;MACA;MACA;MACA,OAAO5B,cAAM,CAACiC,MAAM,CAClBjC,cAAM,CAACkC,OAAO,CAAC/B,aAAK,CAACgC,IAAI,CAACjC,IAAI,CAAC,EAAEN,IAAI,CAACwC,YAAY,CAAC,CACpD;IACH;EACF,CAAC,CAAC;EAEJ,OAAOtC,cAAM,CAACuC,iBAAiB,CAACxC,KAAK,IAAIyC,YAAM,EAAGxB,IAAI,IACpDd,cAAM,CAACuC,GAAG,CAACV,SAAS,CAACf,IAAI,CAAC,EAAE0B,cAAM,CAACC,IAAI,CAAC,CACzC;AACH,CAAC,CAAC,CACH;AAAAxD,OAAA,CAAAU,eAAA,GAAAA,eAAA","ignoreList":[]}
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.toQueryItemsJson = exports.toEventJson = exports.rowToSequenced = exports.jsonbText = void 0;
7
+ var _effect = require("effect");
8
+ /**
9
+ * Serialise a value to a JSON STRING for transport as a `::jsonb`-cast bound
10
+ * parameter.
11
+ *
12
+ * WHY not `sql.json(value)`: `@effect/sql-pg`'s `json` helper binds the RAW
13
+ * value, and node-postgres serialises a JS *array* bound param as a Postgres
14
+ * ARRAY literal (`{…}`), not JSON — so `sql.json([...])::jsonb` fails with a
15
+ * `22P02` invalid-JSON error (an array literal is not valid JSON). Passing a
16
+ * pre-stringified JSON string and casting `::jsonb` sends plain text that
17
+ * Postgres parses as JSON, which round-trips reliably for both objects and
18
+ * arrays. Kept as a named helper so every JSON-transport call site is consistent
19
+ * and the reasoning lives in one place.
20
+ */
21
+ const jsonbText = value => JSON.stringify(value);
22
+ /** Serialise a `Query`'s items into their transport JSON shape. */
23
+ exports.jsonbText = jsonbText;
24
+ const toQueryItemsJson = query => query.items.map(item => ({
25
+ types: [...item.types],
26
+ tags: [...item.tags]
27
+ }));
28
+ /** Serialise one core `DcbEvent` into its transport JSON shape. */
29
+ exports.toQueryItemsJson = toQueryItemsJson;
30
+ const toEventJson = event => ({
31
+ type: event.type,
32
+ // `Encoding.encodeBase64` (from `effect`) keeps this dependency-free and pure
33
+ // — no `Buffer`, so no `@types/node` and no browser/runtime coupling.
34
+ data: _effect.Encoding.encodeBase64(event.data),
35
+ tags: [...event.tags],
36
+ uuid: event.uuid,
37
+ occurred_at: _effect.DateTime.formatIso(event.occurredAt)
38
+ });
39
+ /**
40
+ * Map a raw main-table row to a core `SequencedEvent`. `data` NULL → empty bytes
41
+ * (a payload-less event round-trips to a zero-length `Uint8Array`); `occurred_at`
42
+ * Date → `DateTime.Utc`; `id` string → branded `Position`.
43
+ *
44
+ * The branded scalars (`type`, `tags`, `uuid`, `position`) are minted by trusted
45
+ * `as`-cast, NOT by re-running `EventType.make` / `Tag.make` etc. This is
46
+ * deliberate: these bytes were validated at write time, and the store schema is
47
+ * intentionally LAX on read (ADR-0006 store-laxness — hand-assembled or pre-ADR
48
+ * events must remain readable), so re-validating here could REJECT a legitimately
49
+ * stored value and break that contract. It mirrors the in-memory oracle, which
50
+ * likewise mints branded positions by cast at its trusted allocation boundary
51
+ * (`inMemory.ts`). The read boundary is the analogous trusted deserialisation
52
+ * point.
53
+ */
54
+ exports.toEventJson = toEventJson;
55
+ const rowToSequenced = row => ({
56
+ event: {
57
+ type: row.type,
58
+ data: row.data ?? new Uint8Array(0),
59
+ tags: row.tags,
60
+ uuid: row.uuid,
61
+ occurredAt: _effect.DateTime.unsafeFromDate(row.occurred_at)
62
+ },
63
+ position: BigInt(row.id)
64
+ });
65
+ exports.rowToSequenced = rowToSequenced;
66
+ //# sourceMappingURL=transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.js","names":["_effect","require","jsonbText","value","JSON","stringify","exports","toQueryItemsJson","query","items","map","item","types","tags","toEventJson","event","type","data","Encoding","encodeBase64","uuid","occurred_at","DateTime","formatIso","occurredAt","rowToSequenced","row","Uint8Array","unsafeFromDate","position","BigInt","id"],"sources":["../../../src/internal/transport.ts"],"sourcesContent":[null],"mappings":";;;;;;AAsBA,IAAAA,OAAA,GAAAC,OAAA;AAGA;;;;;;;;;;;;;AAaO,MAAMC,SAAS,GAAIC,KAAc,IAAaC,IAAI,CAACC,SAAS,CAACF,KAAK,CAAC;AAQ1E;AAAAG,OAAA,CAAAJ,SAAA,GAAAA,SAAA;AACO,MAAMK,gBAAgB,GAAIC,KAAY,IAC3CA,KAAK,CAACC,KAAK,CAACC,GAAG,CAAEC,IAAI,KAAM;EACzBC,KAAK,EAAE,CAAC,GAAGD,IAAI,CAACC,KAAK,CAAC;EACtBC,IAAI,EAAE,CAAC,GAAGF,IAAI,CAACE,IAAI;CACpB,CAAC,CAAC;AA4BL;AAAAP,OAAA,CAAAC,gBAAA,GAAAA,gBAAA;AACO,MAAMO,WAAW,GAAIC,KAAe,KAAiB;EAC1DC,IAAI,EAAED,KAAK,CAACC,IAAI;EAChB;EACA;EACAC,IAAI,EAAEC,gBAAQ,CAACC,YAAY,CAACJ,KAAK,CAACE,IAAI,CAAC;EACvCJ,IAAI,EAAE,CAAC,GAAGE,KAAK,CAACF,IAAI,CAAC;EACrBO,IAAI,EAAEL,KAAK,CAACK,IAAI;EAChBC,WAAW,EAAEC,gBAAQ,CAACC,SAAS,CAACR,KAAK,CAACS,UAAU;CACjD,CAAC;AA4BF;;;;;;;;;;;;;;;AAAAlB,OAAA,CAAAQ,WAAA,GAAAA,WAAA;AAeO,MAAMW,cAAc,GAAIC,GAAa,KAAsB;EAChEX,KAAK,EAAE;IACLC,IAAI,EAAEU,GAAG,CAACV,IAAwB;IAClCC,IAAI,EAAES,GAAG,CAACT,IAAI,IAAI,IAAIU,UAAU,CAAC,CAAC,CAAC;IACnCd,IAAI,EAAEa,GAAG,CAACb,IAAmC;IAC7CO,IAAI,EAAEM,GAAG,CAACN,IAAwB;IAClCI,UAAU,EAAEF,gBAAQ,CAACM,cAAc,CAACF,GAAG,CAACL,WAAW;GACpD;EACDQ,QAAQ,EAAEC,MAAM,CAACJ,GAAG,CAACK,EAAE;CACxB,CAAC;AAAAzB,OAAA,CAAAmB,cAAA,GAAAA,cAAA","ignoreList":[]}
@@ -0,0 +1,256 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.layer = void 0;
7
+ var _core = require("@kairos-es/core");
8
+ var _effect = require("effect");
9
+ var _clients = require("./clients.js");
10
+ var _config = require("./config.js");
11
+ var _ddl = require("./internal/ddl.js");
12
+ var _readPlan = require("./internal/readPlan.js");
13
+ var _transport = require("./internal/transport.js");
14
+ /**
15
+ * Set `lock_timeout` for the current transaction via a BOUND parameter.
16
+ *
17
+ * `SET LOCAL` cannot take a bound parameter, but `set_config(setting, value,
18
+ * is_local)` is its exact bound-parameter equivalent (`is_local = true` ≡ `SET
19
+ * LOCAL`) — so the timeout value is a parameter, never interpolated into SQL
20
+ * text. The value is `'<seconds>s'` (Postgres parses the `s` unit); `'0s'` = no
21
+ * timeout. Runs inside the append transaction so it scopes to that call only.
22
+ */
23
+ const setLocalLockTimeout = (sql, lockTimeout) => sql`SELECT set_config('lock_timeout', ${`${lockTimeout}s`}, true)`;
24
+ /**
25
+ * Build the engine MECHANICS (a `RawDcbEventStore`) over the two clients and a
26
+ * resolved name set. Contract policy (`assertServableQuery`, `ReadLimit` decode,
27
+ * error-vs-defect classification) is NOT here — `makeContractStore` adds it
28
+ * uniformly, so this engine surfaces raw `SqlError`s and the contract conflict.
29
+ */
30
+ const make = (config, names) => _effect.Effect.gen(function* () {
31
+ const pooled = yield* _clients.PgClientPooled;
32
+ const direct = yield* _clients.PgClientDirect;
33
+ const lockTimeout = config.lockTimeout ?? 0;
34
+ const pollInterval = config.pollInterval ?? _core.DEFAULT_POLL_INTERVAL_MILLIS;
35
+ const catchUpPageSize = config.catchUpPageSize ?? _core.DEFAULT_CATCH_UP_PAGE_SIZE;
36
+ // The pre-quoted table names every read plan embeds (resolved once).
37
+ const tables = {
38
+ main: (0, _ddl.quoteQualified)(names.mainTable),
39
+ tag: (0, _ddl.quoteQualified)(names.tagTable)
40
+ };
41
+ // --- read -------------------------------------------------------------
42
+ /**
43
+ * The global last position ignoring any filter — the head of a no-limit
44
+ * read. Empty store → ORIGIN.
45
+ *
46
+ * A VALUE over the resolved `pooled`, and deliberately NOT a function of a
47
+ * client — do not re-introduce the parameter. There is only one client it
48
+ * could take: `direct` exists solely for `subscribe`'s `listen` (see
49
+ * `clients.ts`), so a client parameter would advertise a selection seam the
50
+ * engine does not have.
51
+ *
52
+ * Nor would such a parameter be load-bearing for the REPEATABLE READ
53
+ * transaction this runs inside, which is the trap worth naming, since a
54
+ * closed-over client LOOKS like it must escape to its own snapshot.
55
+ * `@effect/sql` resolves a statement's connection when the statement RUNS,
56
+ * from the FIBRE CONTEXT rather than from the client value: `withTransaction`
57
+ * acquires one connection and `Effect.locally`s it into
58
+ * `FiberRef.currentContext` under its `TransactionConnection` tag, and every
59
+ * statement a client builds resolves through a `getConnection` that reads
60
+ * that tag first, falling back to the pool acquirer only when it is absent
61
+ * (`@effect/sql@0.52` `src/internal/client.ts`). A statement therefore joins
62
+ * a transaction by being RUN inside it, and passing the client along the call
63
+ * chain neither adds to that nor is required by it. `read-postgres`'s
64
+ * `pinnedConnection` test observes the same fact from the SERVER: two
65
+ * statements issued inside one transaction report one `pg_backend_pid()`.
66
+ *
67
+ * Built once at layer construction and re-run per read, which a statement
68
+ * supports: it carries no mutable state and recompiles on every execution.
69
+ */
70
+ const globalHead = pooled`
71
+ SELECT MAX(id) AS max FROM ${pooled(names.mainTable)}
72
+ `.pipe(_effect.Effect.map(rows => {
73
+ const max = rows[0]?.max;
74
+ return max === null || max === undefined ? _core.ORIGIN : BigInt(max);
75
+ }));
76
+ /**
77
+ * The read MECHANICS: the rows a query selects, plus the head a decision
78
+ * model may then append under. `read` below only shapes the pair into a
79
+ * `ReadResponse`, so the two snapshot regimes argued for here stay in one
80
+ * place.
81
+ *
82
+ * Named for what it returns rather than for what it reads through: it closes
83
+ * over `pooled`, and takes no client, for the reason given on `globalHead`.
84
+ */
85
+ const readRowsAndHead = (query, options) => _effect.Effect.gen(function* () {
86
+ // No validation here — the query and limit were validated once at the
87
+ // `makeContractStore` boundary, so the per-read re-validation is gone.
88
+ const plan = (0, _readPlan.compileFetchPlan)(tables, query);
89
+ // Limited read: `head` is the last RETURNED position, taken from the
90
+ // SAME single statement, so rows and head are already one consistent
91
+ // snapshot (or ORIGIN when nothing is returned). No transaction needed —
92
+ // a single statement sees a single snapshot.
93
+ if (options?.limit !== undefined) {
94
+ const rows = yield* (0, _readPlan.runFetch)(pooled, plan, options.after, options.limit);
95
+ const lastRow = rows.at(-1);
96
+ const head = lastRow === undefined ? _core.ORIGIN : BigInt(lastRow.id);
97
+ return {
98
+ rows,
99
+ head
100
+ };
101
+ }
102
+ // No-limit read: `head` is the GLOBAL last position (ignoring the query
103
+ // filter), which needs a SEPARATE `MAX(id)` statement. Rows and head
104
+ // MUST share one MVCC snapshot: under the default READ COMMITTED each
105
+ // statement snapshots independently, so a concurrent append committing
106
+ // between the row fetch and the `MAX(id)` would push `head` PAST an
107
+ // event absent from `events` — and a decision model appending under
108
+ // `{ after: head }` would then silently miss that event as a conflict,
109
+ // breaking optimistic concurrency. A REPEATABLE READ, READ ONLY
110
+ // transaction gives both statements a single snapshot, matching the
111
+ // in-memory oracle's atomic (mutex-guarded) read. Readers take only
112
+ // ACCESS SHARE, so this never contends with the exclusive-lock append.
113
+ return yield* pooled.withTransaction(_effect.Effect.gen(function* () {
114
+ yield* pooled.unsafe('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ, READ ONLY');
115
+ const rows = yield* (0, _readPlan.runFetch)(pooled, plan, options?.after, undefined);
116
+ const head = yield* globalHead;
117
+ return {
118
+ rows,
119
+ head
120
+ };
121
+ }));
122
+ });
123
+ const read = (query, options) => readRowsAndHead(query, options).pipe(_effect.Effect.map(({
124
+ rows,
125
+ head
126
+ }) => ({
127
+ events: _effect.Stream.fromIterable(rows.map(_transport.rowToSequenced)),
128
+ head
129
+ })));
130
+ /**
131
+ * The ONE transaction frame every append rides: open a transaction, apply the
132
+ * per-call `set_config('lock_timeout', …, true)`, then run the caller's
133
+ * `SELECT <fn>(…)`. Both the conditional and unconditional paths go through
134
+ * here, so the lock-timeout frame can never diverge between them — the
135
+ * function's LOCK/check/insert and the timeout are one atomic unit, and the
136
+ * conflict (zero rows) vs head (`MAX(id)`) is read from the returned rows by
137
+ * the caller, never from a caught error.
138
+ */
139
+ const callAppendFn = select => pooled.withTransaction(setLocalLockTimeout(pooled, lockTimeout).pipe(_effect.Effect.zipRight(select)));
140
+ const append = (events, condition) => {
141
+ const eventsJson = events.map(_transport.toEventJson);
142
+ // Raw mechanics: a CONFLICT is `AppendConditionFailed`; a `SqlError` (incl.
143
+ // a `55P03` lock-timeout abort) rides the channel too. `makeContractStore`
144
+ // classifies — conflict stays, everything else becomes a defect.
145
+ return _effect.Effect.gen(function* () {
146
+ if (condition === undefined) {
147
+ // No guard, but the unconditional twin STILL takes the exclusive lock
148
+ // (see `unconditionalAppendBody`). That lock on BOTH append paths is how
149
+ // this engine discharges the subscribe machine's ascending-commit-order
150
+ // precondition (`SubscribeMachine.ts` argues it): without it two
151
+ // concurrent unconditional writers can commit bigserial ids out of
152
+ // order, letting `subscribe` skip the later-committing lower id and
153
+ // `head` overtake an in-flight position. Sharing `callAppendFn` with the
154
+ // conditional path guarantees the lock is scoped to the call and the
155
+ // configured timeout applies identically to both append paths.
156
+ const rows = yield* callAppendFn(pooled`
157
+ SELECT ${pooled(names.appendUnconditionalFn)}(
158
+ ${(0, _transport.jsonbText)(eventsJson)}::jsonb
159
+ ) AS max
160
+ `);
161
+ return headFromRows(rows);
162
+ }
163
+ const queryItems = (0, _transport.toQueryItemsJson)(condition.failIfEventsMatch);
164
+ const afterId = condition.after === undefined ? null : condition.after;
165
+ // The function returns MAX(id) on success, ZERO rows on conflict — the
166
+ // conflict is the row count, never a caught error.
167
+ const rows = yield* callAppendFn(pooled`
168
+ SELECT ${pooled(names.appendFn)}(
169
+ ${(0, _transport.jsonbText)(queryItems)}::jsonb,
170
+ ${afterId},
171
+ ${(0, _transport.jsonbText)(eventsJson)}::jsonb
172
+ ) AS max
173
+ `);
174
+ // Empty result set = conflict.
175
+ if (rows.length === 0) {
176
+ return yield* new _core.AppendConditionFailed({
177
+ condition
178
+ });
179
+ }
180
+ return headFromRows(rows);
181
+ });
182
+ };
183
+ /**
184
+ * Extract the new head `Position` from the function's single result row. The
185
+ * function returns exactly one row (`MAX(id)`) on a successful insert; that
186
+ * value is the last new position (read-your-writes).
187
+ */
188
+ const headFromRows = rows => {
189
+ const max = rows[0]?.max;
190
+ if (max === null || max === undefined) {
191
+ // Defensive: a successful unconditional/`NOT conflict` insert always
192
+ // yields a non-null MAX. A null here means nothing was inserted, which
193
+ // for an append (always ≥1 event) is a broken invariant → die.
194
+ throw new Error('kairos-es/store-postgres: append returned no head position');
195
+ }
196
+ return BigInt(max);
197
+ };
198
+ // --- subscribe --------------------------------------------------------
199
+ /**
200
+ * Wire core's subscribe machine to the live clients: `fetch` is the
201
+ * per-subscription compiled read plan run against the pooled client and mapped
202
+ * to `SequencedEvent`s; `listen` is the direct client's ref-counted, non-pooled
203
+ * LISTEN on this store's channel (the Neon-safe path — `clients.ts` owns why
204
+ * there are two clients at all). The plan is compiled ONCE here so the poll
205
+ * loop re-binds only the cursor.
206
+ *
207
+ * The wake source is `NOTIFY`, and it is a LATENCY optimisation only: the
208
+ * machine's periodic position-based poll is what makes delivery correct, which
209
+ * matters more here than the general argument for it suggests. The
210
+ * `@effect/sql-pg` listen client SWALLOWS connection errors and does not
211
+ * auto-reconnect, so a dropped LISTEN typically goes quiet rather than
212
+ * erroring — the machine's listen-side retry then never fires, and the poll is
213
+ * the ONLY thing that recovers the subscription. The same silence is what a
214
+ * Neon pooled endpoint produces if `PgClientDirect` is misconfigured: the
215
+ * pooler drops `LISTEN`/`NOTIFY` without complaint and delivery degrades to
216
+ * poll-rate rather than failing, which is why that trap is a deployment-time
217
+ * wiring choice and not a runtime error.
218
+ *
219
+ * `E` is `SqlError.SqlError` — both seams fail with it, so the machine's single
220
+ * error parameter carries it straight through to `makeContractStore`.
221
+ */
222
+ const subscribe = (query, after) => {
223
+ const plan = (0, _readPlan.compileFetchPlan)(tables, query);
224
+ return (0, _core.subscribeStream)({
225
+ fetch: (from, limit) => (0, _readPlan.runFetch)(pooled, plan, from, limit).pipe(_effect.Effect.map(rows => rows.map(_transport.rowToSequenced))),
226
+ listen: direct.listen(names.channel),
227
+ pollInterval,
228
+ catchUpPageSize
229
+ }, after);
230
+ };
231
+ return {
232
+ read,
233
+ append,
234
+ subscribe
235
+ };
236
+ });
237
+ /**
238
+ * The Postgres `DcbEventStore` layer factory. Builds the engine over
239
+ * `PgClientPooled` (reads/append/notify) and `PgClientDirect` (the subscribe
240
+ * listener). The caller wires those two client layers — pointing `PgClientDirect`
241
+ * at a direct (non-pooler) endpoint on Neon, or at the same URL as `PgClientPooled`
242
+ * everywhere else.
243
+ *
244
+ * `ensureSchema(config)` must have been run (by the caller's migrator or at
245
+ * boot) before the layer is used; the layer itself performs no DDL, so it stays
246
+ * a pure runtime dependency requiring only the two clients.
247
+ */
248
+ const layer = (config = {}) => {
249
+ // Decode through the config schema so a malformed config (empty/dotted atom,
250
+ // negative/non-finite timeout, over-63-byte derived name) fails LOUDLY at
251
+ // construction rather than late on the first append.
252
+ const decoded = (0, _config.decodePostgresStoreConfig)(config);
253
+ return _effect.Layer.effect(_core.DcbEventStore, _effect.Effect.map(make(decoded, (0, _ddl.resolveNames)(decoded)), _core.makeContractStore));
254
+ };
255
+ exports.layer = layer;
256
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","names":["_core","require","_effect","_clients","_config","_ddl","_readPlan","_transport","setLocalLockTimeout","sql","lockTimeout","make","config","names","Effect","gen","pooled","PgClientPooled","direct","PgClientDirect","pollInterval","DEFAULT_POLL_INTERVAL_MILLIS","catchUpPageSize","DEFAULT_CATCH_UP_PAGE_SIZE","tables","main","quoteQualified","mainTable","tag","tagTable","globalHead","pipe","map","rows","max","undefined","ORIGIN","BigInt","readRowsAndHead","query","options","plan","compileFetchPlan","limit","runFetch","after","lastRow","at","head","id","withTransaction","unsafe","read","events","Stream","fromIterable","rowToSequenced","callAppendFn","select","zipRight","append","condition","eventsJson","toEventJson","appendUnconditionalFn","jsonbText","headFromRows","queryItems","toQueryItemsJson","failIfEventsMatch","afterId","appendFn","length","AppendConditionFailed","Error","subscribe","subscribeStream","fetch","from","listen","channel","layer","decoded","decodePostgresStoreConfig","Layer","effect","DcbEventStore","resolveNames","makeContractStore","exports"],"sources":["../../src/store.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,34 @@
1
+ import type { SqlError } from '@effect/sql';
2
+ import { PgClient } from '@effect/sql-pg';
3
+ import { Context, Layer } from 'effect';
4
+ declare const PgClientPooled_base: Context.TagClass<PgClientPooled, "@kairos-es/store-postgres/PgClientPooled", PgClient.PgClient>;
5
+ /**
6
+ * The pooled client tag: reads, the append function, and `notify`. Holds a
7
+ * `PgClient` value rather than being a `PgClient.layer`, so it never collides
8
+ * with the direct client on the shared `PgClient`/`SqlClient` tag.
9
+ */
10
+ export declare class PgClientPooled extends PgClientPooled_base {
11
+ }
12
+ declare const PgClientDirect_base: Context.TagClass<PgClientDirect, "@kairos-es/store-postgres/PgClientDirect", PgClient.PgClient>;
13
+ /**
14
+ * The direct (non-pooler) client tag: the `subscribe` listener ONLY. Point this
15
+ * at a direct endpoint on Neon so `LISTEN`/`NOTIFY` is not dropped by the pooler.
16
+ */
17
+ export declare class PgClientDirect extends PgClientDirect_base {
18
+ }
19
+ /**
20
+ * Build a `PgClientPooled` layer from a `PgClient` config. We construct the
21
+ * client via `PgClient.make` (scoped) and place it in our OWN tag, then provide
22
+ * `Reactivity.layer` (which `PgClient.make` requires) — exactly what the
23
+ * built-in `PgClient.layer` does internally, minus the collision-prone claim on
24
+ * the shared `PgClient` tag.
25
+ */
26
+ export declare const pooledLayer: (config: PgClient.PgClientConfig) => Layer.Layer<PgClientPooled, SqlError.SqlError>;
27
+ /**
28
+ * Build a `PgClientDirect` layer from a `PgClient` config. Same construction as
29
+ * `pooledLayer`; the two differ only in which endpoint the caller points them
30
+ * at (and that only matters on Neon).
31
+ */
32
+ export declare const directLayer: (config: PgClient.PgClientConfig) => Layer.Layer<PgClientDirect, SqlError.SqlError>;
33
+ export {};
34
+ //# sourceMappingURL=clients.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clients.d.ts","sourceRoot":"","sources":["../../src/clients.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AACzC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAA;;AAEvC;;;;GAIG;AACH,qBAAa,cAAe,SAAQ,mBAEE;CAAG;;AAEzC;;;GAGG;AACH,qBAAa,cAAe,SAAQ,mBAEE;CAAG;AAEzC;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,GACtB,QAAQ,QAAQ,CAAC,cAAc,KAC9B,KAAK,CAAC,KAAK,CAAC,cAAc,EAAE,QAAQ,CAAC,QAAQ,CAG7C,CAAA;AAEH;;;;GAIG;AACH,eAAO,MAAM,WAAW,GACtB,QAAQ,QAAQ,CAAC,cAAc,KAC9B,KAAK,CAAC,KAAK,CAAC,cAAc,EAAE,QAAQ,CAAC,QAAQ,CAG7C,CAAA"}
@@ -0,0 +1,58 @@
1
+ import { Schema } from 'effect';
2
+ /**
3
+ * Configuration for a Postgres store instance.
4
+ *
5
+ * - `schema` — the Postgres schema that owns the store's objects (default
6
+ * `public`). `ensureSchema` creates it defensively.
7
+ * - `tablePrefix` — the base name for the store's tables/indexes/functions
8
+ * (default `dcb_events`). Two stores with different prefixes (or schemas)
9
+ * cohabit one database without interference.
10
+ * - `lockTimeout` — seconds passed to `set_config('lock_timeout', …, true)`
11
+ * inside every append, bounding how long a blocked writer waits on the
12
+ * exclusive lock. `0` (the default) means wait indefinitely — parity with the
13
+ * in-memory semaphore's unbounded wait, so the shared contract suite behaves
14
+ * identically across engines. A non-zero timeout that fires (`55P03`) is a
15
+ * Postgres-only outcome surfaced OFF the shared `append` contract (a defect),
16
+ * never as `AppendConditionFailed` — the in-memory oracle cannot produce it.
17
+ * - `pollInterval` — MILLISECONDS a live `subscribe` waits for a NOTIFY wake
18
+ * before re-reading from its cursor anyway, defaulting to core's
19
+ * `DEFAULT_POLL_INTERVAL_MILLIS`. Because this engine HAS a wake source, the
20
+ * value bounds worst-case live-tail latency rather than being it — only a
21
+ * subscriber no NOTIFY reaches waits the whole interval; lower it (e.g. in tests)
22
+ * to poll faster. See `packages/core/src/SubscribeMachine.ts` for what the poll
23
+ * guarantees and what a wake source is for.
24
+ * - `catchUpPageSize` — the `subscribe` reconcile page size, defaulting to core's
25
+ * `DEFAULT_CATCH_UP_PAGE_SIZE`: how many events a single catch-up/live read
26
+ * fetches before looping.
27
+ */
28
+ export declare const PostgresStoreConfig: Schema.filter<Schema.Struct<{
29
+ pollInterval: Schema.optionalWith<Schema.filter<Schema.filter<typeof Schema.Number>>, {
30
+ exact: true;
31
+ }>;
32
+ catchUpPageSize: Schema.optionalWith<Schema.filter<typeof Schema.Int>, {
33
+ exact: true;
34
+ }>;
35
+ schema: Schema.optionalWith<Schema.filter<Schema.filter<typeof Schema.String>>, {
36
+ exact: true;
37
+ }>;
38
+ tablePrefix: Schema.optionalWith<Schema.filter<Schema.filter<typeof Schema.String>>, {
39
+ exact: true;
40
+ }>;
41
+ lockTimeout: Schema.optionalWith<Schema.filter<Schema.filter<typeof Schema.Number>>, {
42
+ exact: true;
43
+ }>;
44
+ }>>;
45
+ export type PostgresStoreConfig = typeof PostgresStoreConfig.Type;
46
+ /**
47
+ * Decode an unknown value into a validated `PostgresStoreConfig`, throwing a
48
+ * `ParseError` on any violation. Used by `layer`/`ensureSchema` so a bad config
49
+ * fails at construction time.
50
+ */
51
+ export declare const decodePostgresStoreConfig: (u: unknown, overrideOptions?: import("effect/SchemaAST").ParseOptions) => {
52
+ readonly schema?: string;
53
+ readonly tablePrefix?: string;
54
+ readonly lockTimeout?: number;
55
+ readonly pollInterval?: number;
56
+ readonly catchUpPageSize?: number;
57
+ };
58
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/config.ts"],"names":[],"mappings":"AAqBA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAiB/B;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;GAkB/B,CAAA;AACD,MAAM,MAAM,mBAAmB,GAAG,OAAO,mBAAmB,CAAC,IAAI,CAAA;AAEjE;;;;GAIG;AACH,eAAO,MAAM,yBAAyB;;;;;;CACS,CAAA"}