@kairos-es/read-postgres 1.0.0-alpha.4 → 1.0.0-alpha.5
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/README.md +35 -29
- package/dist/cjs/config.js +9 -13
- package/dist/cjs/config.js.map +1 -1
- package/dist/cjs/ensureCheckpointSchema.js +2 -2
- package/dist/cjs/ensureCheckpointSchema.js.map +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/internal/ddl.js +2 -2
- package/dist/cjs/projectionStore.js +62 -12
- package/dist/cjs/projectionStore.js.map +1 -1
- package/dist/{dts → dts-cjs}/config.d.ts +7 -11
- package/dist/{dts → dts-cjs}/config.d.ts.map +1 -1
- package/dist/{dts → dts-cjs}/ensureCheckpointSchema.d.ts +2 -2
- package/dist/{dts → dts-cjs}/ensureCheckpointSchema.d.ts.map +1 -1
- package/dist/{dts → dts-cjs}/index.d.ts +6 -4
- package/dist/{dts → dts-cjs}/index.d.ts.map +1 -1
- package/dist/{dts → dts-cjs}/internal/ddl.d.ts +4 -4
- package/dist/{dts → dts-cjs}/internal/ddl.d.ts.map +1 -1
- package/dist/dts-cjs/package.json +3 -0
- package/dist/dts-cjs/projectionStore.d.ts +77 -0
- package/dist/dts-cjs/projectionStore.d.ts.map +1 -0
- package/dist/dts-esm/config.d.ts +61 -0
- package/dist/dts-esm/config.d.ts.map +1 -0
- package/dist/dts-esm/ensureCheckpointSchema.d.ts +39 -0
- package/dist/dts-esm/ensureCheckpointSchema.d.ts.map +1 -0
- package/dist/dts-esm/index.d.ts +92 -0
- package/dist/dts-esm/index.d.ts.map +1 -0
- package/dist/dts-esm/internal/ddl.d.ts +146 -0
- package/dist/dts-esm/internal/ddl.d.ts.map +1 -0
- package/dist/dts-esm/internal/statements.d.ts +100 -0
- package/dist/dts-esm/internal/statements.d.ts.map +1 -0
- package/dist/dts-esm/package.json +3 -0
- package/dist/dts-esm/projectionStore.d.ts +77 -0
- package/dist/dts-esm/projectionStore.d.ts.map +1 -0
- package/dist/esm/config.js +9 -13
- package/dist/esm/config.js.map +1 -1
- package/dist/esm/ensureCheckpointSchema.js +2 -2
- package/dist/esm/ensureCheckpointSchema.js.map +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/internal/ddl.js +2 -2
- package/dist/esm/projectionStore.js +61 -12
- package/dist/esm/projectionStore.js.map +1 -1
- package/package.json +13 -9
- package/src/config.ts +11 -11
- package/src/ensureCheckpointSchema.ts +3 -2
- package/src/index.ts +6 -4
- package/src/internal/ddl.ts +4 -4
- package/src/projectionStore.ts +28 -20
- package/dist/dts/projectionStore.d.ts +0 -30
- package/dist/dts/projectionStore.d.ts.map +0 -1
- /package/dist/{dts → dts-cjs}/internal/statements.d.ts +0 -0
- /package/dist/{dts → dts-cjs}/internal/statements.d.ts.map +0 -0
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ guarded compare-and-set that commits a read model's view writes and its checkpoi
|
|
|
5
5
|
advance in one transaction, and an idempotent `ensureCheckpointSchema` for the
|
|
6
6
|
consumer's own migrator (ADR-0007).
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Needs only the generic `SqlClient` from `effect/sql` — no `@effect/sql-pg`
|
|
9
9
|
and no reserved connection, because the event store owns the LISTEN.
|
|
10
10
|
|
|
11
11
|
A store is defined entirely by its `{ schema, tablePrefix }` pair — the same pair
|
|
@@ -15,28 +15,33 @@ store" keeps holding across the read side.
|
|
|
15
15
|
|
|
16
16
|
## Installation
|
|
17
17
|
|
|
18
|
-
`@kairos-es/core`, `@kairos-es/read
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
`@kairos-es/core`, `@kairos-es/read` and `effect` are peer dependencies (so the
|
|
19
|
+
app owns the single shared Effect + `SqlClient` instance, per ADR-0003); the
|
|
20
|
+
generic SQL modules (`SqlClient`, `Migrator`, `SqlError`) ship inside `effect`
|
|
21
|
+
itself, as `effect/sql`:
|
|
21
22
|
|
|
22
23
|
```sh
|
|
23
24
|
pnpm add @kairos-es/read-postgres@alpha @kairos-es/read@alpha @kairos-es/codec@alpha \
|
|
24
|
-
@kairos-es/core@alpha
|
|
25
|
-
effect@^3.22.0
|
|
25
|
+
@kairos-es/core@alpha effect@^4.0.5
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
because this package names
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
are required. This package re-declares none of them, so the unmet-peer warning
|
|
35
|
-
you get without them names the requirer rather than anything here.
|
|
28
|
+
`@kairos-es/codec` is on the line because something ELSE on it requires it, not
|
|
29
|
+
because this package names it: it is a peer of `@kairos-es/read`, which imports
|
|
30
|
+
from it. That requirer does not mark it optional — an optional peer is one the
|
|
31
|
+
REQUIRER lists in its own `peerDependenciesMeta`, and it declares no such field —
|
|
32
|
+
so it is required. This package does not re-declare it, so a package manager that
|
|
33
|
+
reports an unmet peer names the requirer rather than anything here.
|
|
36
34
|
|
|
37
35
|
A `SqlClient` **implementation** is deliberately not peered: this package needs
|
|
38
|
-
only the generic `SqlClient`
|
|
39
|
-
already provides that
|
|
36
|
+
only the generic `SqlClient` service, so the driver (`@effect/sql-pg`, or whatever
|
|
37
|
+
already provides that service in your app) stays your choice.
|
|
38
|
+
|
|
39
|
+
If that driver is `@effect/sql-pg` 4, it caches named prepared statements by
|
|
40
|
+
default, its transaction's `BEGIN` and `COMMIT` included. Behind a
|
|
41
|
+
transaction-mode pooler, such as the PgBouncer behind Neon's `-pooler` endpoint, a
|
|
42
|
+
statement may run on a different server connection from the one that prepared
|
|
43
|
+
it, so unless the pooler documents support for protocol-level prepared
|
|
44
|
+
statements, build the client you hand this package with `prepare: false`.
|
|
40
45
|
|
|
41
46
|
## Schema: `ensureCheckpointSchema` as a migration
|
|
42
47
|
|
|
@@ -47,9 +52,9 @@ so re-running it is a no-op. Migration **orchestration** is yours; the package
|
|
|
47
52
|
only provides the idempotent **payload**.
|
|
48
53
|
|
|
49
54
|
```ts
|
|
50
|
-
import { Migrator } from '@effect/sql'
|
|
51
55
|
import { ensureSchema } from '@kairos-es/store-postgres'
|
|
52
56
|
import { ensureCheckpointSchema } from '@kairos-es/read-postgres'
|
|
57
|
+
import { Migrator } from 'effect/sql'
|
|
53
58
|
|
|
54
59
|
const config = { schema: 'public', tablePrefix: 'dcb_events' }
|
|
55
60
|
|
|
@@ -59,7 +64,7 @@ const migrations = Migrator.fromRecord({
|
|
|
59
64
|
})
|
|
60
65
|
```
|
|
61
66
|
|
|
62
|
-
Because it requires the **generic** `SqlClient`
|
|
67
|
+
Because it requires the **generic** `SqlClient` service, it slots into any migrator or
|
|
63
68
|
boot sequence that already provides one. The table it creates:
|
|
64
69
|
|
|
65
70
|
```sql
|
|
@@ -78,10 +83,10 @@ lookup, and it is the arbiter index the first commit's `ON CONFLICT` names.
|
|
|
78
83
|
|
|
79
84
|
`makeSqlProjectionStore(config?)` is an `Effect`, not a `Layer` — the view store
|
|
80
85
|
is selected **per materialisation**, so it is a value handed to a read model rather
|
|
81
|
-
than a context
|
|
86
|
+
than a context service one store could claim process-wide. Per materialisation and not
|
|
82
87
|
merely per read model: one read model may be materialised several times at once, a
|
|
83
88
|
durable store from here beside an in-memory one, which is a shape a process-wide
|
|
84
|
-
|
|
89
|
+
service could not express at all. It closes over the `SqlClient` it acquires, so the
|
|
85
90
|
returned store's `commit` adds no `SqlClient` to the read model's own requirements.
|
|
86
91
|
|
|
87
92
|
```ts
|
|
@@ -99,7 +104,7 @@ const config = { schema: 'public', tablePrefix: 'dcb_events' }
|
|
|
99
104
|
|
|
100
105
|
const key = checkpointKey(ProjectionId.make('course-subscriptions'))
|
|
101
106
|
|
|
102
|
-
const courseView = Layer.
|
|
107
|
+
const courseView = Layer.unwrap(
|
|
103
108
|
Effect.gen(function* () {
|
|
104
109
|
const store = yield* makeSqlProjectionStore(config)
|
|
105
110
|
return projectionLayer({
|
|
@@ -169,12 +174,13 @@ as the event store's `layer` assumes `ensureSchema` has.
|
|
|
169
174
|
|
|
170
175
|
## The atomic bracket
|
|
171
176
|
|
|
172
|
-
`commit` is `sql.withTransaction(viewWrites *> guardedAdvance)`. Your
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
177
|
+
`commit` is `sql.withTransaction(viewWrites *> guardedAdvance)`. Your view writes
|
|
178
|
+
join `commit` only when issued through the **same `SqlClient`** the store was
|
|
179
|
+
built from. A write through any other `SqlClient` instance — even one connected
|
|
180
|
+
to the same database — or to another database is **not** bracketed, and is not
|
|
181
|
+
rolled back on a lost guard. `withTransaction` is savepoint-aware, so a
|
|
182
|
+
projection already inside a transaction on that client composes without a second
|
|
183
|
+
physical one.
|
|
178
184
|
|
|
179
185
|
The guarded advance touches zero rows when the stored position no longer equals
|
|
180
186
|
`expected`, which fails `CheckpointSuperseded` — and that failure aborts the
|
|
@@ -205,8 +211,8 @@ projection", and a defect carries no such distinction.
|
|
|
205
211
|
## Positions are strictly-increasing, not gapless
|
|
206
212
|
|
|
207
213
|
`last_position` is a `bigint` because a `Position` is 64-bit and exceeds JS
|
|
208
|
-
`number` safety at 2^53;
|
|
209
|
-
|
|
214
|
+
`number` safety at 2^53; the driver reads it back as a `bigint`, and it is never
|
|
215
|
+
coerced through `number`. The stored position is the last
|
|
210
216
|
processed event's position **as is** — never `expected + 1` — so a gap is normal
|
|
211
217
|
and arithmetic on a position would be wrong.
|
|
212
218
|
|
package/dist/cjs/config.js
CHANGED
|
@@ -29,7 +29,7 @@ var _ddl = require("./internal/ddl.js");
|
|
|
29
29
|
* per-materialisation view-store design exists to avoid: a durable view must be
|
|
30
30
|
* free to sit over any log satisfying `DcbEventStore` — the in-memory oracle,
|
|
31
31
|
* Postgres, a future backend — so this package depends on the PORT
|
|
32
|
-
* (`@kairos-es/read`) and on
|
|
32
|
+
* (`@kairos-es/read`) and on `effect/sql`, never on a sibling backend. The
|
|
33
33
|
* duplication also keeps the two guards honestly independent: each package
|
|
34
34
|
* measures only the identifiers IT materialises, so neither inherits a byte budget
|
|
35
35
|
* from names it never creates.
|
|
@@ -48,7 +48,7 @@ var _ddl = require("./internal/ddl.js");
|
|
|
48
48
|
* dot-split when quoted, so an embedded dot would silently re-partition the name
|
|
49
49
|
* into the wrong schema/object.
|
|
50
50
|
*/
|
|
51
|
-
const IdentifierAtom = /*#__PURE__*/_effect.Schema.String.
|
|
51
|
+
const IdentifierAtom = /*#__PURE__*/_effect.Schema.String.check(/*#__PURE__*/_effect.Schema.isNonEmpty(), /*#__PURE__*/_effect.Schema.isPattern(/^[^.]+$/, {
|
|
52
52
|
identifier: 'PostgresIdentifierAtom',
|
|
53
53
|
description: 'a non-empty identifier atom containing no "."'
|
|
54
54
|
}));
|
|
@@ -63,23 +63,19 @@ const IdentifierAtom = /*#__PURE__*/_effect.Schema.String.pipe(/*#__PURE__*/_eff
|
|
|
63
63
|
* objects, one isolated store per prefix.
|
|
64
64
|
*/
|
|
65
65
|
const ReadPostgresConfig = exports.ReadPostgresConfig = /*#__PURE__*/_effect.Schema.Struct({
|
|
66
|
-
// `
|
|
66
|
+
// `optionalKey` keeps the decoded type at `key?: T` (never `T | undefined`),
|
|
67
67
|
// matching the repo's `exactOptionalPropertyTypes` discipline — an absent key,
|
|
68
68
|
// not an explicit `undefined`.
|
|
69
|
-
schema: _effect.Schema.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
tablePrefix: _effect.Schema.optionalWith(IdentifierAtom, {
|
|
73
|
-
exact: true
|
|
74
|
-
})
|
|
75
|
-
}).pipe(/*#__PURE__*/
|
|
69
|
+
schema: _effect.Schema.optionalKey(IdentifierAtom),
|
|
70
|
+
tablePrefix: _effect.Schema.optionalKey(IdentifierAtom)
|
|
71
|
+
}).check(/*#__PURE__*/
|
|
76
72
|
// Cross-field guard: the derived table name must fit Postgres's 63-byte
|
|
77
73
|
// identifier limit. Returning the message string fails the decode with it;
|
|
78
|
-
// `undefined
|
|
79
|
-
_effect.Schema.
|
|
74
|
+
// `undefined` passes.
|
|
75
|
+
_effect.Schema.makeFilter(config => (0, _ddl.oversizedIdentifier)(config)));
|
|
80
76
|
/**
|
|
81
77
|
* Decode an unknown value into a validated `ReadPostgresConfig`, throwing a
|
|
82
|
-
* `
|
|
78
|
+
* `SchemaError` on any violation. Used by `makeSqlProjectionStore` and
|
|
83
79
|
* `ensureCheckpointSchema` so a bad config fails at construction time.
|
|
84
80
|
*/
|
|
85
81
|
const decodeReadPostgresConfig = exports.decodeReadPostgresConfig = /*#__PURE__*/_effect.Schema.decodeUnknownSync(ReadPostgresConfig);
|
package/dist/cjs/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","names":["_effect","require","_ddl","IdentifierAtom","Schema","String","
|
|
1
|
+
{"version":3,"file":"config.js","names":["_effect","require","_ddl","IdentifierAtom","Schema","String","check","isNonEmpty","isPattern","identifier","description","ReadPostgresConfig","exports","Struct","schema","optionalKey","tablePrefix","makeFilter","config","oversizedIdentifier","decodeReadPostgresConfig","decodeUnknownSync"],"sources":["../../src/config.ts"],"sourcesContent":[null],"mappings":";;;;;;AAmCA,IAAAA,OAAA,GAAAC,OAAA;AACA,IAAAC,IAAA,GAAAD,OAAA;AApCA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA;;;;;;AAMA,MAAME,cAAc,gBAAGC,cAAM,CAACC,MAAM,CAACC,KAAK,cACxCF,cAAM,CAACG,UAAU,EAAE,eACnBH,cAAM,CAACI,SAAS,CAAC,SAAS,EAAE;EAC1BC,UAAU,EAAE,wBAAwB;EACpCC,WAAW,EAAE;CACd,CAAC,CACH;AAED;;;;;;;;;;AAUO,MAAMC,kBAAkB,GAAAC,OAAA,CAAAD,kBAAA,gBAAGP,cAAM,CAACS,MAAM,CAAC;EAC9C;EACA;EACA;EACAC,MAAM,EAAEV,cAAM,CAACW,WAAW,CAACZ,cAAc,CAAC;EAC1Ca,WAAW,EAAEZ,cAAM,CAACW,WAAW,CAACZ,cAAc;CAC/C,CAAC,CAACG,KAAK;AACN;AACA;AACA;AACAF,cAAM,CAACa,UAAU,CAAEC,MAAM,IAAK,IAAAC,wBAAmB,EAACD,MAAM,CAAC,CAAC,CAC3D;AAGD;;;;;AAKO,MAAME,wBAAwB,GAAAR,OAAA,CAAAQ,wBAAA,gBACnChB,cAAM,CAACiB,iBAAiB,CAACV,kBAAkB,CAAC","ignoreList":[]}
|
|
@@ -4,8 +4,8 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.ensureCheckpointSchema = void 0;
|
|
7
|
-
var _sql = require("@effect/sql");
|
|
8
7
|
var _effect = require("effect");
|
|
8
|
+
var _sql = require("effect/sql");
|
|
9
9
|
var _config = require("./config.js");
|
|
10
10
|
var _ddl = require("./internal/ddl.js");
|
|
11
11
|
/**
|
|
@@ -14,7 +14,7 @@ var _ddl = require("./internal/ddl.js");
|
|
|
14
14
|
*
|
|
15
15
|
* It is a single `Effect<void, SqlError, SqlClient>` that creates (or refreshes)
|
|
16
16
|
* the one object this package owns: the checkpoint table. It requires the GENERIC
|
|
17
|
-
* `SqlClient`
|
|
17
|
+
* `SqlClient` service — not a package-owned client service — precisely so a consumer can
|
|
18
18
|
* drop it straight into THEIR OWN migrator, e.g.:
|
|
19
19
|
*
|
|
20
20
|
* ```ts
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ensureCheckpointSchema.js","names":["
|
|
1
|
+
{"version":3,"file":"ensureCheckpointSchema.js","names":["_effect","require","_sql","_config","_ddl","ensureCheckpointSchema","config","Effect","gen","sql","SqlClient","names","resolveCheckpointNames","decodeReadPostgresConfig","all","runCheckpointDdl","discard","exports"],"sources":["../../src/ensureCheckpointSchema.ts"],"sourcesContent":[null],"mappings":";;;;;;AA8BA,IAAAA,OAAA,GAAAC,OAAA;AACA,IAAAC,IAAA,GAAAD,OAAA;AACA,IAAAE,OAAA,GAAAF,OAAA;AACA,IAAAG,IAAA,GAAAH,OAAA;AAjCA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA;;;;;AAKO,MAAMI,sBAAsB,GACjCC,MAA0B,IAE1BC,cAAM,CAACC,GAAG,CAAC,aAAS;EAClB,MAAMC,GAAG,GAAG,OAAOC,cAAS,CAACA,SAAS;EACtC;EACA;EACA;EACA,MAAMC,KAAK,GAAG,IAAAC,2BAAsB,EAAC,IAAAC,gCAAwB,EAACP,MAAM,CAAC,CAAC;EACtE;EACA,OAAOC,cAAM,CAACO,GAAG,CAAC,IAAAC,qBAAgB,EAACN,GAAG,EAAEE,KAAK,CAAC,EAAE;IAAEK,OAAO,EAAE;EAAI,CAAE,CAAC;AACpE,CAAC,CAAC;AAAAC,OAAA,CAAAZ,sBAAA,GAAAA,sBAAA","ignoreList":[]}
|
package/dist/cjs/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["_ensureCheckpointSchema","require","_projectionStore"],"sources":["../../src/index.ts"],"sourcesContent":[null],"mappings":";;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.js","names":["_ensureCheckpointSchema","require","_projectionStore"],"sources":["../../src/index.ts"],"sourcesContent":[null],"mappings":";;;;;;;;;;;;;;;;;AAyFA,IAAAA,uBAAA,GAAAC,OAAA;AACA,IAAAC,gBAAA,GAAAD,OAAA","ignoreList":[]}
|
package/dist/cjs/internal/ddl.js
CHANGED
|
@@ -122,8 +122,8 @@ const CHECKPOINT_COLUMNS = exports.CHECKPOINT_COLUMNS = {
|
|
|
122
122
|
partition: /*#__PURE__*/quoteAtom('partition'),
|
|
123
123
|
/**
|
|
124
124
|
* The scalar checkpoint: `bigint`, because a `Position` is 64-bit and exceeds
|
|
125
|
-
* JS `number` safety at 2^53.
|
|
126
|
-
*
|
|
125
|
+
* JS `number` safety at 2^53. The driver reads it back as a `bigint`, and it
|
|
126
|
+
* is never coerced through `number`.
|
|
127
127
|
*/
|
|
128
128
|
lastPosition: /*#__PURE__*/quoteAtom('last_position')
|
|
129
129
|
};
|
|
@@ -4,13 +4,58 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.makeSqlProjectionStore = void 0;
|
|
7
|
-
var _sql = require("@effect/sql");
|
|
8
7
|
var _core = require("@kairos-es/core");
|
|
9
8
|
var _read = require("@kairos-es/read");
|
|
10
9
|
var _effect = require("effect");
|
|
10
|
+
var _sql = require("effect/sql");
|
|
11
11
|
var _config = require("./config.js");
|
|
12
12
|
var _ddl = require("./internal/ddl.js");
|
|
13
13
|
var _statements = require("./internal/statements.js");
|
|
14
|
+
/**
|
|
15
|
+
* The SQL `ProjectionStore`: a durable scalar checkpoint plus the guarded
|
|
16
|
+
* compare-and-set that brackets a read model's own view writes in ONE ordinary
|
|
17
|
+
* transaction (ADR-0007).
|
|
18
|
+
*
|
|
19
|
+
* This is the durable half of the parity pair. `@kairos-es/read`'s in-memory store
|
|
20
|
+
* is the oracle; this backend must produce identical OBSERVABLE behaviour, and the
|
|
21
|
+
* shared `projection-store-contract-tests` suite runs unchanged against both. The
|
|
22
|
+
* one place the two genuinely differ is HOW the abort is achieved:
|
|
23
|
+
*
|
|
24
|
+
* - in memory, the guard is checked FIRST and a loss means `viewWrites` never ran
|
|
25
|
+
* (abort by omission — there is nothing to undo);
|
|
26
|
+
* - here, `viewWrites` runs first and a lost guard FAILS, so the transaction rolls
|
|
27
|
+
* the view writes back with it (abort by undo).
|
|
28
|
+
*
|
|
29
|
+
* Both satisfy the port's promise that a lost guard leaves no observable view
|
|
30
|
+
* effect, which is why ordering inside the transaction is a performance choice
|
|
31
|
+
* rather than a correctness one: running the guard last keeps the checkpoint row
|
|
32
|
+
* lock held only for the advance instead of for the whole batch's worth of view
|
|
33
|
+
* writes.
|
|
34
|
+
*
|
|
35
|
+
* Two contract inversions worth naming, both deliberate:
|
|
36
|
+
*
|
|
37
|
+
* 1. A `SqlError` becomes a `ProjectionStoreError` on the ERROR CHANNEL — the
|
|
38
|
+
* opposite of the event store, where an infrastructure fault is a DEFECT
|
|
39
|
+
* (`Effect.die`, ADR-0002). The read side has a supervisor above it whose whole
|
|
40
|
+
* job is telling "retry the subscription with backoff" from "give up and report
|
|
41
|
+
* a stalled projection", and a defect carries no such distinction. On the event
|
|
42
|
+
* store there is no supervisor: a caller cannot meaningfully retry a broken
|
|
43
|
+
* connection, so dying is right there and wrong here.
|
|
44
|
+
* 2. A lost guard is a tagged failure (`CheckpointSuperseded`), never a returned
|
|
45
|
+
* outcome value, and that failure is exactly what aborts the transaction. A
|
|
46
|
+
* returned value would only roll back if the caller remembered to convert it
|
|
47
|
+
* into a failure, and forgetting is silent double-application.
|
|
48
|
+
*
|
|
49
|
+
* The store closes over the `SqlClient` it acquired, so the returned
|
|
50
|
+
* `ProjectionStore.commit` adds NO `SqlClient` to the caller's `R` — a read model
|
|
51
|
+
* stays free of the view store's requirements, which is what lets one runner drive
|
|
52
|
+
* read models over different backends.
|
|
53
|
+
*
|
|
54
|
+
* What this module owns is the Effect wiring: the client, the transaction bracket,
|
|
55
|
+
* the error classification. The SQL itself and the parameter lists that feed it are
|
|
56
|
+
* `internal/statements.ts`, which owns the argument for why the two live together.
|
|
57
|
+
*/
|
|
58
|
+
|
|
14
59
|
/**
|
|
15
60
|
* Turn a `ProjectionStoreError` out of a SQL fault, naming what was being
|
|
16
61
|
* attempted and for which key. `reason` is the human-readable line a supervisor
|
|
@@ -41,20 +86,23 @@ const unwrapCommitError = (key, error) => {
|
|
|
41
86
|
*
|
|
42
87
|
* A factory `Effect` rather than a `Layer`, matching the in-memory store: the view
|
|
43
88
|
* store is selected PER MATERIALISATION, so it is a value handed to a read model, not
|
|
44
|
-
* a context
|
|
89
|
+
* a context service one store could claim process-wide. Per materialisation rather than
|
|
45
90
|
* per read model, because one read model may be materialised several times at once —
|
|
46
91
|
* a store from here beside an in-memory one, wired by `@kairos-es/read`'s
|
|
47
|
-
* `runProjections` — which is a shape a process-wide
|
|
92
|
+
* `runProjections` — which is a shape a process-wide service could not express at all. It
|
|
48
93
|
* acquires the client ONCE and closes over it, which is what keeps `commit`'s `R`
|
|
49
94
|
* down to the caller's own.
|
|
50
95
|
*
|
|
51
96
|
* **The atomic bracket covers writes issued through THIS client.** The caller's
|
|
52
|
-
* `viewWrites` join the transaction
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
97
|
+
* `viewWrites` join the transaction only when they are issued through the same
|
|
98
|
+
* `SqlClient` the store was built from: `effect/sql` carries the transaction's
|
|
99
|
+
* connection in the Effect CONTEXT under a key that belongs to that one client
|
|
100
|
+
* instance, so a statement run inside `commit` finds the open transaction only if
|
|
101
|
+
* its client owns that key. The store's own `withoutTransforms()` copy shares the
|
|
102
|
+
* key, which is why the guarded advance and the caller's view writes land in one
|
|
103
|
+
* transaction; a write through any OTHER `SqlClient` instance — even one over the
|
|
104
|
+
* same database — or to a different database is NOT bracketed, is not rolled back
|
|
105
|
+
* on a lost guard, and the exactly-once property does not extend to it.
|
|
58
106
|
*
|
|
59
107
|
* `ensureCheckpointSchema(config)` must have run (in the caller's migrator or at
|
|
60
108
|
* boot) before the store is used; this factory performs no DDL, so it stays a pure
|
|
@@ -62,13 +110,15 @@ const unwrapCommitError = (key, error) => {
|
|
|
62
110
|
*/
|
|
63
111
|
const makeSqlProjectionStore = (config = {}) => _effect.Effect.gen(function* () {
|
|
64
112
|
// `withoutTransforms()` because this package takes the CONSUMER's generic
|
|
65
|
-
// client, which may carry a result-name transform (
|
|
113
|
+
// client, which may carry a result-name transform (`effect/sql` will happily
|
|
66
114
|
// camel-case every returned key). Under one, `last_position` would arrive as
|
|
67
115
|
// `lastPosition`, `readCheckpoint` would see `undefined` and report `ORIGIN`,
|
|
68
116
|
// and a durable projection would silently replay its whole backlog on every
|
|
69
117
|
// boot. Stripping the transform for OUR statements is the fix; the caller's
|
|
70
118
|
// own view writes keep their client's transforms and still join the same
|
|
71
|
-
// transaction
|
|
119
|
+
// transaction, because the transform-free copy carries the original client's
|
|
120
|
+
// transaction key and so finds the connection that client's transaction put
|
|
121
|
+
// in the context.
|
|
72
122
|
const sql = (yield* _sql.SqlClient.SqlClient).withoutTransforms();
|
|
73
123
|
// Decode INSIDE the Effect so a malformed config (empty/dotted atom, an
|
|
74
124
|
// over-63-byte derived name) surfaces as a defect on the fibre building the
|
|
@@ -131,7 +181,7 @@ const makeSqlProjectionStore = (config = {}) => _effect.Effect.gen(function* ()
|
|
|
131
181
|
// interrupt landing mid-body rolls back exactly like a failure.
|
|
132
182
|
// `args` itself, not three values pulled out of it in an order: see
|
|
133
183
|
// `guardedAdvance`.
|
|
134
|
-
_effect.Effect.
|
|
184
|
+
_effect.Effect.andThen(guardedAdvance(args)))).pipe(_effect.Effect.mapError(error => unwrapCommitError(args.key, error)));
|
|
135
185
|
const resetCheckpoint = key =>
|
|
136
186
|
// DELETE rather than `SET last_position = 0`, so "never committed" and
|
|
137
187
|
// "reset" stay ONE state — which is exactly what the port promises the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"projectionStore.js","names":["
|
|
1
|
+
{"version":3,"file":"projectionStore.js","names":["_core","require","_read","_effect","_sql","_config","_ddl","_statements","storeError","attempt","key","cause","ProjectionStoreError","reason","projection","partition","message","unwrapCommitError","error","_tag","makeSqlProjectionStore","config","Effect","gen","sql","SqlClient","withoutTransforms","names","resolveCheckpointNames","decodeReadPostgresConfig","statements","compileStatements","readCheckpoint","unsafe","select","keyParams","pipe","mapError","map","rows","stored","last_position","undefined","ORIGIN","Position","make","BigInt","guardedAdvance","args","expected","text","upsertFromOrigin","advance","advanceParams","flatMap","length","fail","CheckpointSuperseded","void","commit","withTransaction","viewWrites","andThen","resetCheckpoint","reset","asVoid","store","durability","exports"],"sources":["../../src/projectionStore.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* per-materialisation view-store design exists to avoid: a durable view must be
|
|
22
22
|
* free to sit over any log satisfying `DcbEventStore` — the in-memory oracle,
|
|
23
23
|
* Postgres, a future backend — so this package depends on the PORT
|
|
24
|
-
* (`@kairos-es/read`) and on
|
|
24
|
+
* (`@kairos-es/read`) and on `effect/sql`, never on a sibling backend. The
|
|
25
25
|
* duplication also keeps the two guards honestly independent: each package
|
|
26
26
|
* measures only the identifiers IT materialises, so neither inherits a byte budget
|
|
27
27
|
* from names it never creates.
|
|
@@ -44,21 +44,17 @@ import { Schema } from 'effect';
|
|
|
44
44
|
* store is configured with and the checkpoint table lands beside that store's
|
|
45
45
|
* objects, one isolated store per prefix.
|
|
46
46
|
*/
|
|
47
|
-
export declare const ReadPostgresConfig: Schema.
|
|
48
|
-
schema: Schema.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
tablePrefix: Schema.optionalWith<Schema.filter<Schema.filter<typeof Schema.String>>, {
|
|
52
|
-
exact: true;
|
|
53
|
-
}>;
|
|
54
|
-
}>>;
|
|
47
|
+
export declare const ReadPostgresConfig: Schema.Struct<{
|
|
48
|
+
readonly schema: Schema.optionalKey<Schema.String>;
|
|
49
|
+
readonly tablePrefix: Schema.optionalKey<Schema.String>;
|
|
50
|
+
}>;
|
|
55
51
|
export type ReadPostgresConfig = typeof ReadPostgresConfig.Type;
|
|
56
52
|
/**
|
|
57
53
|
* Decode an unknown value into a validated `ReadPostgresConfig`, throwing a
|
|
58
|
-
* `
|
|
54
|
+
* `SchemaError` on any violation. Used by `makeSqlProjectionStore` and
|
|
59
55
|
* `ensureCheckpointSchema` so a bad config fails at construction time.
|
|
60
56
|
*/
|
|
61
|
-
export declare const decodeReadPostgresConfig: (
|
|
57
|
+
export declare const decodeReadPostgresConfig: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => {
|
|
62
58
|
readonly schema?: string;
|
|
63
59
|
readonly tablePrefix?: string;
|
|
64
60
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAiB/B;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAiB/B;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB;;;EAW9B,CAAA;AACD,MAAM,MAAM,kBAAkB,GAAG,OAAO,kBAAkB,CAAC,IAAI,CAAA;AAE/D;;;;GAIG;AACH,eAAO,MAAM,wBAAwB;;;CACS,CAAA"}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* It is a single `Effect<void, SqlError, SqlClient>` that creates (or refreshes)
|
|
6
6
|
* the one object this package owns: the checkpoint table. It requires the GENERIC
|
|
7
|
-
* `SqlClient`
|
|
7
|
+
* `SqlClient` service — not a package-owned client service — precisely so a consumer can
|
|
8
8
|
* drop it straight into THEIR OWN migrator, e.g.:
|
|
9
9
|
*
|
|
10
10
|
* ```ts
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
* reads no rows and passes no identifier through `sql(identifier)`, so a
|
|
28
28
|
* consumer's name transforms have nothing here to rewrite.
|
|
29
29
|
*/
|
|
30
|
-
import { SqlClient, type SqlError } from '@effect/sql';
|
|
31
30
|
import { Effect } from 'effect';
|
|
31
|
+
import { SqlClient, type SqlError } from 'effect/sql';
|
|
32
32
|
import { type ReadPostgresConfig } from './config.js';
|
|
33
33
|
/**
|
|
34
34
|
* Produce the checkpoint migration payload for `config`. Resolves the table name
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ensureCheckpointSchema.d.ts","sourceRoot":"","sources":["../../src/ensureCheckpointSchema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;
|
|
1
|
+
{"version":3,"file":"ensureCheckpointSchema.d.ts","sourceRoot":"","sources":["../../src/ensureCheckpointSchema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC/B,OAAO,EAAE,SAAS,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAA;AACrD,OAAO,EAA4B,KAAK,kBAAkB,EAAE,MAAM,UAAU,CAAA;AAG5E;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,GACjC,QAAQ,kBAAkB,KACzB,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC,SAAS,CASzD,CAAA"}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* table plus the guarded compare-and-set that brackets a read model's own view
|
|
4
4
|
* writes in one ordinary transaction (ADR-0007).
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* Needs only the GENERIC `SqlClient` from `effect/sql`, deliberately not
|
|
7
7
|
* `@effect/sql-pg` and deliberately no reserved connection: the event store owns
|
|
8
8
|
* the LISTEN, so the read side needs no dedicated connection of its own, and
|
|
9
9
|
* requiring the generic client is what lets `ensureCheckpointSchema` drop into a
|
|
@@ -66,8 +66,10 @@
|
|
|
66
66
|
* package performs no DDL at runtime.
|
|
67
67
|
*
|
|
68
68
|
* `commit` is `sql.withTransaction(viewWrites *> guardedAdvance)`. The author's own
|
|
69
|
-
*
|
|
70
|
-
*
|
|
69
|
+
* view writes join that transaction only when issued through the SAME `SqlClient`
|
|
70
|
+
* the store was built from; a write through any other `SqlClient` instance is NOT
|
|
71
|
+
* bracketed and is not rolled back on a lost guard. `withTransaction` is
|
|
72
|
+
* savepoint-aware, so a projection already inside a
|
|
71
73
|
* transaction composes without a second physical one. A guarded advance touching
|
|
72
74
|
* zero rows raises `CheckpointSuperseded`, whose FAILURE aborts the transaction and
|
|
73
75
|
* rolls the view writes back with it — the abort that makes the projection
|
|
@@ -82,7 +84,7 @@
|
|
|
82
84
|
* implementation, which is precisely what per-materialisation view-store selection
|
|
83
85
|
* exists to avoid. A durable view must be free to sit over any log satisfying
|
|
84
86
|
* `DcbEventStore`, so this package depends on the port (`@kairos-es/read`) and
|
|
85
|
-
* on
|
|
87
|
+
* on `effect/sql`, never on a sibling backend.
|
|
86
88
|
*/
|
|
87
89
|
export type { ReadPostgresConfig } from './config.js';
|
|
88
90
|
export { ensureCheckpointSchema } from './ensureCheckpointSchema.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuFG;AACH,YAAY,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAA;AAClD,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAA;AACjE,OAAO,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAA"}
|
|
@@ -25,13 +25,13 @@
|
|
|
25
25
|
*
|
|
26
26
|
* The runtime deliberately does NOT route these names through `sql(identifier)`.
|
|
27
27
|
* This package takes the CONSUMER's generic `SqlClient`, which may carry a
|
|
28
|
-
* query-name transform (
|
|
28
|
+
* query-name transform (`effect/sql` applies it to every `sql(identifier)`), so
|
|
29
29
|
* a camel-casing consumer would silently rewrite `last_position` to
|
|
30
30
|
* `lastPosition` and address a column that does not exist. Pre-quoted text inside
|
|
31
31
|
* `sql.unsafe` is transform-proof; the result side is handled by
|
|
32
32
|
* `withoutTransforms()` in `projectionStore.ts`.
|
|
33
33
|
*/
|
|
34
|
-
import type { SqlClient, Statement } from '
|
|
34
|
+
import type { SqlClient, Statement } from 'effect/sql';
|
|
35
35
|
/**
|
|
36
36
|
* The one object a read-side config owns. An interface rather than a bare string
|
|
37
37
|
* so the `{ schema, tablePrefix }` atoms stay available to error messages and so
|
|
@@ -111,8 +111,8 @@ export declare const CHECKPOINT_COLUMNS: {
|
|
|
111
111
|
readonly partition: string;
|
|
112
112
|
/**
|
|
113
113
|
* The scalar checkpoint: `bigint`, because a `Position` is 64-bit and exceeds
|
|
114
|
-
* JS `number` safety at 2^53.
|
|
115
|
-
*
|
|
114
|
+
* JS `number` safety at 2^53. The driver reads it back as a `bigint`, and it
|
|
115
|
+
* is never coerced through `number`.
|
|
116
116
|
*/
|
|
117
117
|
readonly lastPosition: string;
|
|
118
118
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ddl.d.ts","sourceRoot":"","sources":["../../../src/internal/ddl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"ddl.d.ts","sourceRoot":"","sources":["../../../src/internal/ddl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAEtD;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,qEAAqE;IACrE,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;CACjC;AAED;;;;;GAKG;AACH,eAAO,MAAM,cAAc,WAAW,CAAA;AACtC,gFAAgF;AAChF,eAAO,MAAM,oBAAoB,eAAe,CAAA;AA0ChD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,mBAAmB,GAAI,QAAQ;IAC1C,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAC9B,KAAG,MAAM,GAAG,SAQZ,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,GAAI,QAAQ;IAC7C,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAC9B,KAAG,uBAYH,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,GAAI,MAAM,MAAM,KAAG,MAIhC,CAAA;AAQd;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,kBAAkB;IAC7B,uCAAuC;;IAEvC,wEAAwE;;IAExE;;;;OAIG;;CAEK,CAAA;AAEV;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,uBAAuB,GAClC,OAAO,uBAAuB,KAC7B,aAAa,CAAC,MAAM,CAiBtB,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,GAC3B,KAAK,SAAS,CAAC,SAAS,EACxB,OAAO,uBAAuB,KAC7B,aAAa,CAAC,SAAS,CAAC,SAAS,CAAC,OAAO,CAAC,CAC6B,CAAA"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SQL `ProjectionStore`: a durable scalar checkpoint plus the guarded
|
|
3
|
+
* compare-and-set that brackets a read model's own view writes in ONE ordinary
|
|
4
|
+
* transaction (ADR-0007).
|
|
5
|
+
*
|
|
6
|
+
* This is the durable half of the parity pair. `@kairos-es/read`'s in-memory store
|
|
7
|
+
* is the oracle; this backend must produce identical OBSERVABLE behaviour, and the
|
|
8
|
+
* shared `projection-store-contract-tests` suite runs unchanged against both. The
|
|
9
|
+
* one place the two genuinely differ is HOW the abort is achieved:
|
|
10
|
+
*
|
|
11
|
+
* - in memory, the guard is checked FIRST and a loss means `viewWrites` never ran
|
|
12
|
+
* (abort by omission — there is nothing to undo);
|
|
13
|
+
* - here, `viewWrites` runs first and a lost guard FAILS, so the transaction rolls
|
|
14
|
+
* the view writes back with it (abort by undo).
|
|
15
|
+
*
|
|
16
|
+
* Both satisfy the port's promise that a lost guard leaves no observable view
|
|
17
|
+
* effect, which is why ordering inside the transaction is a performance choice
|
|
18
|
+
* rather than a correctness one: running the guard last keeps the checkpoint row
|
|
19
|
+
* lock held only for the advance instead of for the whole batch's worth of view
|
|
20
|
+
* writes.
|
|
21
|
+
*
|
|
22
|
+
* Two contract inversions worth naming, both deliberate:
|
|
23
|
+
*
|
|
24
|
+
* 1. A `SqlError` becomes a `ProjectionStoreError` on the ERROR CHANNEL — the
|
|
25
|
+
* opposite of the event store, where an infrastructure fault is a DEFECT
|
|
26
|
+
* (`Effect.die`, ADR-0002). The read side has a supervisor above it whose whole
|
|
27
|
+
* job is telling "retry the subscription with backoff" from "give up and report
|
|
28
|
+
* a stalled projection", and a defect carries no such distinction. On the event
|
|
29
|
+
* store there is no supervisor: a caller cannot meaningfully retry a broken
|
|
30
|
+
* connection, so dying is right there and wrong here.
|
|
31
|
+
* 2. A lost guard is a tagged failure (`CheckpointSuperseded`), never a returned
|
|
32
|
+
* outcome value, and that failure is exactly what aborts the transaction. A
|
|
33
|
+
* returned value would only roll back if the caller remembered to convert it
|
|
34
|
+
* into a failure, and forgetting is silent double-application.
|
|
35
|
+
*
|
|
36
|
+
* The store closes over the `SqlClient` it acquired, so the returned
|
|
37
|
+
* `ProjectionStore.commit` adds NO `SqlClient` to the caller's `R` — a read model
|
|
38
|
+
* stays free of the view store's requirements, which is what lets one runner drive
|
|
39
|
+
* read models over different backends.
|
|
40
|
+
*
|
|
41
|
+
* What this module owns is the Effect wiring: the client, the transaction bracket,
|
|
42
|
+
* the error classification. The SQL itself and the parameter lists that feed it are
|
|
43
|
+
* `internal/statements.ts`, which owns the argument for why the two live together.
|
|
44
|
+
*/
|
|
45
|
+
import { type ProjectionStore } from '@kairos-es/read';
|
|
46
|
+
import { Effect } from 'effect';
|
|
47
|
+
import { SqlClient } from 'effect/sql';
|
|
48
|
+
import { type ReadPostgresConfig } from './config.js';
|
|
49
|
+
/**
|
|
50
|
+
* Build a durable `ProjectionStore` over the ambient `SqlClient`.
|
|
51
|
+
*
|
|
52
|
+
* A factory `Effect` rather than a `Layer`, matching the in-memory store: the view
|
|
53
|
+
* store is selected PER MATERIALISATION, so it is a value handed to a read model, not
|
|
54
|
+
* a context service one store could claim process-wide. Per materialisation rather than
|
|
55
|
+
* per read model, because one read model may be materialised several times at once —
|
|
56
|
+
* a store from here beside an in-memory one, wired by `@kairos-es/read`'s
|
|
57
|
+
* `runProjections` — which is a shape a process-wide service could not express at all. It
|
|
58
|
+
* acquires the client ONCE and closes over it, which is what keeps `commit`'s `R`
|
|
59
|
+
* down to the caller's own.
|
|
60
|
+
*
|
|
61
|
+
* **The atomic bracket covers writes issued through THIS client.** The caller's
|
|
62
|
+
* `viewWrites` join the transaction only when they are issued through the same
|
|
63
|
+
* `SqlClient` the store was built from: `effect/sql` carries the transaction's
|
|
64
|
+
* connection in the Effect CONTEXT under a key that belongs to that one client
|
|
65
|
+
* instance, so a statement run inside `commit` finds the open transaction only if
|
|
66
|
+
* its client owns that key. The store's own `withoutTransforms()` copy shares the
|
|
67
|
+
* key, which is why the guarded advance and the caller's view writes land in one
|
|
68
|
+
* transaction; a write through any OTHER `SqlClient` instance — even one over the
|
|
69
|
+
* same database — or to a different database is NOT bracketed, is not rolled back
|
|
70
|
+
* on a lost guard, and the exactly-once property does not extend to it.
|
|
71
|
+
*
|
|
72
|
+
* `ensureCheckpointSchema(config)` must have run (in the caller's migrator or at
|
|
73
|
+
* boot) before the store is used; this factory performs no DDL, so it stays a pure
|
|
74
|
+
* runtime dependency on the client alone.
|
|
75
|
+
*/
|
|
76
|
+
export declare const makeSqlProjectionStore: (config?: ReadPostgresConfig) => Effect.Effect<ProjectionStore, never, SqlClient.SqlClient>;
|
|
77
|
+
//# sourceMappingURL=projectionStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projectionStore.d.ts","sourceRoot":"","sources":["../../src/projectionStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAGH,OAAO,EAGL,KAAK,eAAe,EAErB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAE/B,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AACtC,OAAO,EAA4B,KAAK,kBAAkB,EAAE,MAAM,UAAU,CAAA;AAmF5E;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,sBAAsB,GACjC,SAAQ,kBAAuB,KAC9B,MAAM,CAAC,MAAM,CAAC,eAAe,EAAE,KAAK,EAAE,SAAS,CAAC,SAAS,CA4IxD,CAAA"}
|