@kairos-es/read-postgres 1.0.0-alpha.3 → 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/dist/esm/internal/ddl.js
CHANGED
|
@@ -113,8 +113,8 @@ export const CHECKPOINT_COLUMNS = {
|
|
|
113
113
|
partition: /*#__PURE__*/quoteAtom('partition'),
|
|
114
114
|
/**
|
|
115
115
|
* The scalar checkpoint: `bigint`, because a `Position` is 64-bit and exceeds
|
|
116
|
-
* JS `number` safety at 2^53.
|
|
117
|
-
*
|
|
116
|
+
* JS `number` safety at 2^53. The driver reads it back as a `bigint`, and it
|
|
117
|
+
* is never coerced through `number`.
|
|
118
118
|
*/
|
|
119
119
|
lastPosition: /*#__PURE__*/quoteAtom('last_position')
|
|
120
120
|
};
|
|
@@ -1,7 +1,51 @@
|
|
|
1
|
-
|
|
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
|
+
*/
|
|
2
45
|
import { ORIGIN, Position } from '@kairos-es/core';
|
|
3
46
|
import { CheckpointSuperseded, ProjectionStoreError } from '@kairos-es/read';
|
|
4
47
|
import { Effect } from 'effect';
|
|
48
|
+
import { SqlClient } from 'effect/sql';
|
|
5
49
|
import { decodeReadPostgresConfig } from "./config.js";
|
|
6
50
|
import { resolveCheckpointNames } from "./internal/ddl.js";
|
|
7
51
|
import { advanceParams, compileStatements, keyParams } from "./internal/statements.js";
|
|
@@ -35,20 +79,23 @@ const unwrapCommitError = (key, error) => {
|
|
|
35
79
|
*
|
|
36
80
|
* A factory `Effect` rather than a `Layer`, matching the in-memory store: the view
|
|
37
81
|
* store is selected PER MATERIALISATION, so it is a value handed to a read model, not
|
|
38
|
-
* a context
|
|
82
|
+
* a context service one store could claim process-wide. Per materialisation rather than
|
|
39
83
|
* per read model, because one read model may be materialised several times at once —
|
|
40
84
|
* a store from here beside an in-memory one, wired by `@kairos-es/read`'s
|
|
41
|
-
* `runProjections` — which is a shape a process-wide
|
|
85
|
+
* `runProjections` — which is a shape a process-wide service could not express at all. It
|
|
42
86
|
* acquires the client ONCE and closes over it, which is what keeps `commit`'s `R`
|
|
43
87
|
* down to the caller's own.
|
|
44
88
|
*
|
|
45
89
|
* **The atomic bracket covers writes issued through THIS client.** The caller's
|
|
46
|
-
* `viewWrites` join the transaction
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
90
|
+
* `viewWrites` join the transaction only when they are issued through the same
|
|
91
|
+
* `SqlClient` the store was built from: `effect/sql` carries the transaction's
|
|
92
|
+
* connection in the Effect CONTEXT under a key that belongs to that one client
|
|
93
|
+
* instance, so a statement run inside `commit` finds the open transaction only if
|
|
94
|
+
* its client owns that key. The store's own `withoutTransforms()` copy shares the
|
|
95
|
+
* key, which is why the guarded advance and the caller's view writes land in one
|
|
96
|
+
* transaction; a write through any OTHER `SqlClient` instance — even one over the
|
|
97
|
+
* same database — or to a different database is NOT bracketed, is not rolled back
|
|
98
|
+
* on a lost guard, and the exactly-once property does not extend to it.
|
|
52
99
|
*
|
|
53
100
|
* `ensureCheckpointSchema(config)` must have run (in the caller's migrator or at
|
|
54
101
|
* boot) before the store is used; this factory performs no DDL, so it stays a pure
|
|
@@ -56,13 +103,15 @@ const unwrapCommitError = (key, error) => {
|
|
|
56
103
|
*/
|
|
57
104
|
export const makeSqlProjectionStore = (config = {}) => Effect.gen(function* () {
|
|
58
105
|
// `withoutTransforms()` because this package takes the CONSUMER's generic
|
|
59
|
-
// client, which may carry a result-name transform (
|
|
106
|
+
// client, which may carry a result-name transform (`effect/sql` will happily
|
|
60
107
|
// camel-case every returned key). Under one, `last_position` would arrive as
|
|
61
108
|
// `lastPosition`, `readCheckpoint` would see `undefined` and report `ORIGIN`,
|
|
62
109
|
// and a durable projection would silently replay its whole backlog on every
|
|
63
110
|
// boot. Stripping the transform for OUR statements is the fix; the caller's
|
|
64
111
|
// own view writes keep their client's transforms and still join the same
|
|
65
|
-
// transaction
|
|
112
|
+
// transaction, because the transform-free copy carries the original client's
|
|
113
|
+
// transaction key and so finds the connection that client's transaction put
|
|
114
|
+
// in the context.
|
|
66
115
|
const sql = (yield* SqlClient.SqlClient).withoutTransforms();
|
|
67
116
|
// Decode INSIDE the Effect so a malformed config (empty/dotted atom, an
|
|
68
117
|
// over-63-byte derived name) surfaces as a defect on the fibre building the
|
|
@@ -125,7 +174,7 @@ export const makeSqlProjectionStore = (config = {}) => Effect.gen(function* () {
|
|
|
125
174
|
// interrupt landing mid-body rolls back exactly like a failure.
|
|
126
175
|
// `args` itself, not three values pulled out of it in an order: see
|
|
127
176
|
// `guardedAdvance`.
|
|
128
|
-
Effect.
|
|
177
|
+
Effect.andThen(guardedAdvance(args)))).pipe(Effect.mapError(error => unwrapCommitError(args.key, error)));
|
|
129
178
|
const resetCheckpoint = key =>
|
|
130
179
|
// DELETE rather than `SET last_position = 0`, so "never committed" and
|
|
131
180
|
// "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":["ORIGIN","Position","CheckpointSuperseded","ProjectionStoreError","Effect","SqlClient","decodeReadPostgresConfig","resolveCheckpointNames","advanceParams","compileStatements","keyParams","storeError","attempt","key","cause","reason","projection","partition","message","unwrapCommitError","error","_tag","makeSqlProjectionStore","config","gen","sql","withoutTransforms","names","statements","readCheckpoint","unsafe","select","pipe","mapError","map","rows","stored","last_position","undefined","make","BigInt","guardedAdvance","args","expected","text","upsertFromOrigin","advance","flatMap","length","fail","void","commit","withTransaction","viewWrites","andThen","resetCheckpoint","reset","asVoid","store","durability"],"sources":["../../src/projectionStore.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kairos-es/read-postgres",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.5",
|
|
4
4
|
"description": "Postgres ProjectionStore backend and checkpoint DDL for kairos-es",
|
|
5
5
|
"license": "BSD-3-Clause",
|
|
6
6
|
"repository": {
|
|
@@ -11,19 +11,23 @@
|
|
|
11
11
|
"sideEffects": [],
|
|
12
12
|
"homepage": "https://github.com/neverbland/kairos-es",
|
|
13
13
|
"peerDependencies": {
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"@kairos-es/
|
|
17
|
-
"@kairos-es/read": "^1.0.0-alpha.3"
|
|
14
|
+
"effect": "^4.0.5",
|
|
15
|
+
"@kairos-es/core": "^1.0.0-alpha.5",
|
|
16
|
+
"@kairos-es/read": "^1.0.0-alpha.5"
|
|
18
17
|
},
|
|
19
18
|
"main": "./dist/cjs/index.js",
|
|
20
19
|
"module": "./dist/esm/index.js",
|
|
21
|
-
"types": "./dist/dts/index.d.ts",
|
|
20
|
+
"types": "./dist/dts-cjs/index.d.ts",
|
|
22
21
|
"exports": {
|
|
23
22
|
".": {
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
"import": {
|
|
24
|
+
"types": "./dist/dts-esm/index.d.ts",
|
|
25
|
+
"default": "./dist/esm/index.js"
|
|
26
|
+
},
|
|
27
|
+
"require": {
|
|
28
|
+
"types": "./dist/dts-cjs/index.d.ts",
|
|
29
|
+
"default": "./dist/cjs/index.js"
|
|
30
|
+
}
|
|
27
31
|
},
|
|
28
32
|
"./package.json": "./package.json"
|
|
29
33
|
},
|
package/src/config.ts
CHANGED
|
@@ -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.
|
|
@@ -42,9 +42,9 @@ import { oversizedIdentifier } from './internal/ddl'
|
|
|
42
42
|
* dot-split when quoted, so an embedded dot would silently re-partition the name
|
|
43
43
|
* into the wrong schema/object.
|
|
44
44
|
*/
|
|
45
|
-
const IdentifierAtom = Schema.String.
|
|
46
|
-
Schema.
|
|
47
|
-
Schema.
|
|
45
|
+
const IdentifierAtom = Schema.String.check(
|
|
46
|
+
Schema.isNonEmpty(),
|
|
47
|
+
Schema.isPattern(/^[^.]+$/, {
|
|
48
48
|
identifier: 'PostgresIdentifierAtom',
|
|
49
49
|
description: 'a non-empty identifier atom containing no "."',
|
|
50
50
|
}),
|
|
@@ -61,22 +61,22 @@ const IdentifierAtom = Schema.String.pipe(
|
|
|
61
61
|
* objects, one isolated store per prefix.
|
|
62
62
|
*/
|
|
63
63
|
export const ReadPostgresConfig = Schema.Struct({
|
|
64
|
-
// `
|
|
64
|
+
// `optionalKey` keeps the decoded type at `key?: T` (never `T | undefined`),
|
|
65
65
|
// matching the repo's `exactOptionalPropertyTypes` discipline — an absent key,
|
|
66
66
|
// not an explicit `undefined`.
|
|
67
|
-
schema: Schema.
|
|
68
|
-
tablePrefix: Schema.
|
|
69
|
-
}).
|
|
67
|
+
schema: Schema.optionalKey(IdentifierAtom),
|
|
68
|
+
tablePrefix: Schema.optionalKey(IdentifierAtom),
|
|
69
|
+
}).check(
|
|
70
70
|
// Cross-field guard: the derived table name must fit Postgres's 63-byte
|
|
71
71
|
// identifier limit. Returning the message string fails the decode with it;
|
|
72
|
-
// `undefined
|
|
73
|
-
Schema.
|
|
72
|
+
// `undefined` passes.
|
|
73
|
+
Schema.makeFilter((config) => oversizedIdentifier(config)),
|
|
74
74
|
)
|
|
75
75
|
export type ReadPostgresConfig = typeof ReadPostgresConfig.Type
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
78
|
* Decode an unknown value into a validated `ReadPostgresConfig`, throwing a
|
|
79
|
-
* `
|
|
79
|
+
* `SchemaError` on any violation. Used by `makeSqlProjectionStore` and
|
|
80
80
|
* `ensureCheckpointSchema` so a bad config fails at construction time.
|
|
81
81
|
*/
|
|
82
82
|
export const decodeReadPostgresConfig =
|
|
@@ -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,9 @@
|
|
|
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
|
-
|
|
30
|
+
|
|
31
31
|
import { Effect } from 'effect'
|
|
32
|
+
import { SqlClient, type SqlError } from 'effect/sql'
|
|
32
33
|
import { decodeReadPostgresConfig, type ReadPostgresConfig } from './config'
|
|
33
34
|
import { resolveCheckpointNames, runCheckpointDdl } from './internal/ddl'
|
|
34
35
|
|
package/src/index.ts
CHANGED
|
@@ -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'
|
|
88
90
|
export { ensureCheckpointSchema } from './ensureCheckpointSchema'
|
package/src/internal/ddl.ts
CHANGED
|
@@ -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
|
/**
|
|
37
37
|
* The one object a read-side config owns. An interface rather than a bare string
|
|
@@ -187,8 +187,8 @@ export const CHECKPOINT_COLUMNS = {
|
|
|
187
187
|
partition: quoteAtom('partition'),
|
|
188
188
|
/**
|
|
189
189
|
* The scalar checkpoint: `bigint`, because a `Position` is 64-bit and exceeds
|
|
190
|
-
* JS `number` safety at 2^53.
|
|
191
|
-
*
|
|
190
|
+
* JS `number` safety at 2^53. The driver reads it back as a `bigint`, and it
|
|
191
|
+
* is never coerced through `number`.
|
|
192
192
|
*/
|
|
193
193
|
lastPosition: quoteAtom('last_position'),
|
|
194
194
|
} as const
|
package/src/projectionStore.ts
CHANGED
|
@@ -42,8 +42,7 @@
|
|
|
42
42
|
* the error classification. The SQL itself and the parameter lists that feed it are
|
|
43
43
|
* `internal/statements.ts`, which owns the argument for why the two live together.
|
|
44
44
|
*/
|
|
45
|
-
|
|
46
|
-
import { SqlClient } from '@effect/sql'
|
|
45
|
+
|
|
47
46
|
import { ORIGIN, Position } from '@kairos-es/core'
|
|
48
47
|
import {
|
|
49
48
|
type CheckpointKey,
|
|
@@ -52,6 +51,8 @@ import {
|
|
|
52
51
|
ProjectionStoreError,
|
|
53
52
|
} from '@kairos-es/read'
|
|
54
53
|
import { Effect } from 'effect'
|
|
54
|
+
import type { SqlError } from 'effect/sql'
|
|
55
|
+
import { SqlClient } from 'effect/sql'
|
|
55
56
|
import { decodeReadPostgresConfig, type ReadPostgresConfig } from './config'
|
|
56
57
|
import { resolveCheckpointNames } from './internal/ddl'
|
|
57
58
|
import {
|
|
@@ -63,14 +64,16 @@ import {
|
|
|
63
64
|
/**
|
|
64
65
|
* A row of the checkpoint table's only interesting column.
|
|
65
66
|
*
|
|
66
|
-
* `last_position` is
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
67
|
+
* `last_position` is an `int8`, which `@effect/sql-pg` decodes straight to a
|
|
68
|
+
* `bigint`. It is typed `bigint | string` and normalised with `BigInt(...)` — the
|
|
69
|
+
* identity on a `bigint`, and exact on the decimal string an `int8` arrived as
|
|
70
|
+
* under the driver this package ran on before — and it is never coerced through
|
|
71
|
+
* `number`, since a 64-bit position exceeds JS `number` safety at 2^53. The event
|
|
72
|
+
* store's `MAX(id)` head takes the same shape. The snake_case key is the column
|
|
73
|
+
* name verbatim; see `withoutTransforms()` below for why a consumer's result-name
|
|
74
|
+
* transform cannot rename it under us.
|
|
72
75
|
*/
|
|
73
|
-
type LastPositionRow = { readonly last_position: string }
|
|
76
|
+
type LastPositionRow = { readonly last_position: bigint | string }
|
|
74
77
|
|
|
75
78
|
/** A statement returning no columns (a `DELETE` with no `RETURNING`). */
|
|
76
79
|
type NoRows = Record<string, never>
|
|
@@ -138,20 +141,23 @@ const unwrapCommitError = <E>(
|
|
|
138
141
|
*
|
|
139
142
|
* A factory `Effect` rather than a `Layer`, matching the in-memory store: the view
|
|
140
143
|
* store is selected PER MATERIALISATION, so it is a value handed to a read model, not
|
|
141
|
-
* a context
|
|
144
|
+
* a context service one store could claim process-wide. Per materialisation rather than
|
|
142
145
|
* per read model, because one read model may be materialised several times at once —
|
|
143
146
|
* a store from here beside an in-memory one, wired by `@kairos-es/read`'s
|
|
144
|
-
* `runProjections` — which is a shape a process-wide
|
|
147
|
+
* `runProjections` — which is a shape a process-wide service could not express at all. It
|
|
145
148
|
* acquires the client ONCE and closes over it, which is what keeps `commit`'s `R`
|
|
146
149
|
* down to the caller's own.
|
|
147
150
|
*
|
|
148
151
|
* **The atomic bracket covers writes issued through THIS client.** The caller's
|
|
149
|
-
* `viewWrites` join the transaction
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
152
|
+
* `viewWrites` join the transaction only when they are issued through the same
|
|
153
|
+
* `SqlClient` the store was built from: `effect/sql` carries the transaction's
|
|
154
|
+
* connection in the Effect CONTEXT under a key that belongs to that one client
|
|
155
|
+
* instance, so a statement run inside `commit` finds the open transaction only if
|
|
156
|
+
* its client owns that key. The store's own `withoutTransforms()` copy shares the
|
|
157
|
+
* key, which is why the guarded advance and the caller's view writes land in one
|
|
158
|
+
* transaction; a write through any OTHER `SqlClient` instance — even one over the
|
|
159
|
+
* same database — or to a different database is NOT bracketed, is not rolled back
|
|
160
|
+
* on a lost guard, and the exactly-once property does not extend to it.
|
|
155
161
|
*
|
|
156
162
|
* `ensureCheckpointSchema(config)` must have run (in the caller's migrator or at
|
|
157
163
|
* boot) before the store is used; this factory performs no DDL, so it stays a pure
|
|
@@ -162,13 +168,15 @@ export const makeSqlProjectionStore = (
|
|
|
162
168
|
): Effect.Effect<ProjectionStore, never, SqlClient.SqlClient> =>
|
|
163
169
|
Effect.gen(function* () {
|
|
164
170
|
// `withoutTransforms()` because this package takes the CONSUMER's generic
|
|
165
|
-
// client, which may carry a result-name transform (
|
|
171
|
+
// client, which may carry a result-name transform (`effect/sql` will happily
|
|
166
172
|
// camel-case every returned key). Under one, `last_position` would arrive as
|
|
167
173
|
// `lastPosition`, `readCheckpoint` would see `undefined` and report `ORIGIN`,
|
|
168
174
|
// and a durable projection would silently replay its whole backlog on every
|
|
169
175
|
// boot. Stripping the transform for OUR statements is the fix; the caller's
|
|
170
176
|
// own view writes keep their client's transforms and still join the same
|
|
171
|
-
// transaction
|
|
177
|
+
// transaction, because the transform-free copy carries the original client's
|
|
178
|
+
// transaction key and so finds the connection that client's transaction put
|
|
179
|
+
// in the context.
|
|
172
180
|
const sql = (yield* SqlClient.SqlClient).withoutTransforms()
|
|
173
181
|
|
|
174
182
|
// Decode INSIDE the Effect so a malformed config (empty/dotted atom, an
|
|
@@ -267,7 +275,7 @@ export const makeSqlProjectionStore = (
|
|
|
267
275
|
// interrupt landing mid-body rolls back exactly like a failure.
|
|
268
276
|
// `args` itself, not three values pulled out of it in an order: see
|
|
269
277
|
// `guardedAdvance`.
|
|
270
|
-
Effect.
|
|
278
|
+
Effect.andThen(guardedAdvance(args)),
|
|
271
279
|
),
|
|
272
280
|
)
|
|
273
281
|
.pipe(Effect.mapError((error) => unwrapCommitError<E>(args.key, error)))
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { SqlClient } from '@effect/sql';
|
|
2
|
-
import { type ProjectionStore } from '@kairos-es/read';
|
|
3
|
-
import { Effect } from 'effect';
|
|
4
|
-
import { type ReadPostgresConfig } from './config.js';
|
|
5
|
-
/**
|
|
6
|
-
* Build a durable `ProjectionStore` over the ambient `SqlClient`.
|
|
7
|
-
*
|
|
8
|
-
* A factory `Effect` rather than a `Layer`, matching the in-memory store: the view
|
|
9
|
-
* store is selected PER MATERIALISATION, so it is a value handed to a read model, not
|
|
10
|
-
* a context tag one store could claim process-wide. Per materialisation rather than
|
|
11
|
-
* per read model, because one read model may be materialised several times at once —
|
|
12
|
-
* a store from here beside an in-memory one, wired by `@kairos-es/read`'s
|
|
13
|
-
* `runProjections` — which is a shape a process-wide tag could not express at all. It
|
|
14
|
-
* acquires the client ONCE and closes over it, which is what keeps `commit`'s `R`
|
|
15
|
-
* down to the caller's own.
|
|
16
|
-
*
|
|
17
|
-
* **The atomic bracket covers writes issued through THIS client.** The caller's
|
|
18
|
-
* `viewWrites` join the transaction because `@effect/sql` carries the transaction's
|
|
19
|
-
* connection in the Effect CONTEXT, not in the client value — so any statement run
|
|
20
|
-
* through the same underlying client inside `commit` is in the same transaction,
|
|
21
|
-
* including through a differently-transformed copy of it. A write to a different
|
|
22
|
-
* database, or through an unrelated client, is NOT bracketed and the exactly-once
|
|
23
|
-
* property does not extend to it.
|
|
24
|
-
*
|
|
25
|
-
* `ensureCheckpointSchema(config)` must have run (in the caller's migrator or at
|
|
26
|
-
* boot) before the store is used; this factory performs no DDL, so it stays a pure
|
|
27
|
-
* runtime dependency on the client alone.
|
|
28
|
-
*/
|
|
29
|
-
export declare const makeSqlProjectionStore: (config?: ReadPostgresConfig) => Effect.Effect<ProjectionStore, never, SqlClient.SqlClient>;
|
|
30
|
-
//# sourceMappingURL=projectionStore.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"projectionStore.d.ts","sourceRoot":"","sources":["../../src/projectionStore.ts"],"names":[],"mappings":"AA6CA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAEvC,OAAO,EAGL,KAAK,eAAe,EAErB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC/B,OAAO,EAA4B,KAAK,kBAAkB,EAAE,MAAM,UAAU,CAAA;AAiF5E;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,sBAAsB,GACjC,SAAQ,kBAAuB,KAC9B,MAAM,CAAC,MAAM,CAAC,eAAe,EAAE,KAAK,EAAE,SAAS,CAAC,SAAS,CA0IxD,CAAA"}
|
|
File without changes
|
|
File without changes
|