@kairos-es/store-postgres 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +28 -0
- package/README.md +167 -0
- package/dist/cjs/clients.js +62 -0
- package/dist/cjs/clients.js.map +1 -0
- package/dist/cjs/config.js +94 -0
- package/dist/cjs/config.js.map +1 -0
- package/dist/cjs/ensureSchema.js +53 -0
- package/dist/cjs/ensureSchema.js.map +1 -0
- package/dist/cjs/index.js +45 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/internal/ddl.js +436 -0
- package/dist/cjs/internal/ddl.js.map +1 -0
- package/dist/cjs/internal/matchSql.js +189 -0
- package/dist/cjs/internal/matchSql.js.map +1 -0
- package/dist/cjs/internal/readPlan.js +67 -0
- package/dist/cjs/internal/readPlan.js.map +1 -0
- package/dist/cjs/internal/subscribe.js +120 -0
- package/dist/cjs/internal/subscribe.js.map +1 -0
- package/dist/cjs/internal/transport.js +66 -0
- package/dist/cjs/internal/transport.js.map +1 -0
- package/dist/cjs/store.js +256 -0
- package/dist/cjs/store.js.map +1 -0
- package/dist/dts/clients.d.ts +34 -0
- package/dist/dts/clients.d.ts.map +1 -0
- package/dist/dts/config.d.ts +58 -0
- package/dist/dts/config.d.ts.map +1 -0
- package/dist/dts/ensureSchema.d.ts +35 -0
- package/dist/dts/ensureSchema.d.ts.map +1 -0
- package/dist/dts/index.d.ts +41 -0
- package/dist/dts/index.d.ts.map +1 -0
- package/dist/dts/internal/ddl.d.ts +174 -0
- package/dist/dts/internal/ddl.d.ts.map +1 -0
- package/dist/dts/internal/matchSql.d.ts +140 -0
- package/dist/dts/internal/matchSql.d.ts.map +1 -0
- package/dist/dts/internal/readPlan.d.ts +72 -0
- package/dist/dts/internal/readPlan.d.ts.map +1 -0
- package/dist/dts/internal/subscribe.d.ts +87 -0
- package/dist/dts/internal/subscribe.d.ts.map +1 -0
- package/dist/dts/internal/transport.d.ts +116 -0
- package/dist/dts/internal/transport.d.ts.map +1 -0
- package/dist/dts/store.d.ts +17 -0
- package/dist/dts/store.d.ts.map +1 -0
- package/dist/esm/clients.js +51 -0
- package/dist/esm/clients.js.map +1 -0
- package/dist/esm/config.js +87 -0
- package/dist/esm/config.js.map +1 -0
- package/dist/esm/ensureSchema.js +45 -0
- package/dist/esm/ensureSchema.js.map +1 -0
- package/dist/esm/index.js +40 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/ddl.js +424 -0
- package/dist/esm/internal/ddl.js.map +1 -0
- package/dist/esm/internal/matchSql.js +176 -0
- package/dist/esm/internal/matchSql.js.map +1 -0
- package/dist/esm/internal/readPlan.js +59 -0
- package/dist/esm/internal/readPlan.js.map +1 -0
- package/dist/esm/internal/subscribe.js +113 -0
- package/dist/esm/internal/subscribe.js.map +1 -0
- package/dist/esm/internal/transport.js +56 -0
- package/dist/esm/internal/transport.js.map +1 -0
- package/dist/esm/package.json +4 -0
- package/dist/esm/store.js +249 -0
- package/dist/esm/store.js.map +1 -0
- package/package.json +35 -0
- package/src/clients.ts +69 -0
- package/src/config.ts +92 -0
- package/src/ensureSchema.ts +46 -0
- package/src/index.ts +45 -0
- package/src/internal/ddl.ts +599 -0
- package/src/internal/matchSql.ts +219 -0
- package/src/internal/readPlan.ts +117 -0
- package/src/internal/transport.ts +141 -0
- package/src/store.ts +413 -0
|
@@ -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"}
|