@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.
Files changed (51) hide show
  1. package/README.md +35 -29
  2. package/dist/cjs/config.js +9 -13
  3. package/dist/cjs/config.js.map +1 -1
  4. package/dist/cjs/ensureCheckpointSchema.js +2 -2
  5. package/dist/cjs/ensureCheckpointSchema.js.map +1 -1
  6. package/dist/cjs/index.js.map +1 -1
  7. package/dist/cjs/internal/ddl.js +2 -2
  8. package/dist/cjs/projectionStore.js +62 -12
  9. package/dist/cjs/projectionStore.js.map +1 -1
  10. package/dist/{dts → dts-cjs}/config.d.ts +7 -11
  11. package/dist/{dts → dts-cjs}/config.d.ts.map +1 -1
  12. package/dist/{dts → dts-cjs}/ensureCheckpointSchema.d.ts +2 -2
  13. package/dist/{dts → dts-cjs}/ensureCheckpointSchema.d.ts.map +1 -1
  14. package/dist/{dts → dts-cjs}/index.d.ts +6 -4
  15. package/dist/{dts → dts-cjs}/index.d.ts.map +1 -1
  16. package/dist/{dts → dts-cjs}/internal/ddl.d.ts +4 -4
  17. package/dist/{dts → dts-cjs}/internal/ddl.d.ts.map +1 -1
  18. package/dist/dts-cjs/package.json +3 -0
  19. package/dist/dts-cjs/projectionStore.d.ts +77 -0
  20. package/dist/dts-cjs/projectionStore.d.ts.map +1 -0
  21. package/dist/dts-esm/config.d.ts +61 -0
  22. package/dist/dts-esm/config.d.ts.map +1 -0
  23. package/dist/dts-esm/ensureCheckpointSchema.d.ts +39 -0
  24. package/dist/dts-esm/ensureCheckpointSchema.d.ts.map +1 -0
  25. package/dist/dts-esm/index.d.ts +92 -0
  26. package/dist/dts-esm/index.d.ts.map +1 -0
  27. package/dist/dts-esm/internal/ddl.d.ts +146 -0
  28. package/dist/dts-esm/internal/ddl.d.ts.map +1 -0
  29. package/dist/dts-esm/internal/statements.d.ts +100 -0
  30. package/dist/dts-esm/internal/statements.d.ts.map +1 -0
  31. package/dist/dts-esm/package.json +3 -0
  32. package/dist/dts-esm/projectionStore.d.ts +77 -0
  33. package/dist/dts-esm/projectionStore.d.ts.map +1 -0
  34. package/dist/esm/config.js +9 -13
  35. package/dist/esm/config.js.map +1 -1
  36. package/dist/esm/ensureCheckpointSchema.js +2 -2
  37. package/dist/esm/ensureCheckpointSchema.js.map +1 -1
  38. package/dist/esm/index.js.map +1 -1
  39. package/dist/esm/internal/ddl.js +2 -2
  40. package/dist/esm/projectionStore.js +61 -12
  41. package/dist/esm/projectionStore.js.map +1 -1
  42. package/package.json +13 -9
  43. package/src/config.ts +11 -11
  44. package/src/ensureCheckpointSchema.ts +3 -2
  45. package/src/index.ts +6 -4
  46. package/src/internal/ddl.ts +4 -4
  47. package/src/projectionStore.ts +28 -20
  48. package/dist/dts/projectionStore.d.ts +0 -30
  49. package/dist/dts/projectionStore.d.ts.map +0 -1
  50. /package/dist/{dts → dts-cjs}/internal/statements.d.ts +0 -0
  51. /package/dist/{dts → dts-cjs}/internal/statements.d.ts.map +0 -0
@@ -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. It is read back as a decimal STRING and
117
- * reconstructed with `BigInt(...)`, never coerced through `number`.
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
- import { SqlClient } from '@effect/sql';
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 tag one store could claim process-wide. Per materialisation rather than
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 tag could not express at all. It
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 because `@effect/sql` carries the transaction's
47
- * connection in the Effect CONTEXT, not in the client value — so any statement run
48
- * through the same underlying client inside `commit` is in the same transaction,
49
- * including through a differently-transformed copy of it. A write to a different
50
- * database, or through an unrelated client, is NOT bracketed and the exactly-once
51
- * property does not extend to it.
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 (`@effect/sql` will happily
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 (the connection travels in the context, not the client).
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.zipRight(guardedAdvance(args)))).pipe(Effect.mapError(error => unwrapCommitError(args.key, error)));
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":["SqlClient","ORIGIN","Position","CheckpointSuperseded","ProjectionStoreError","Effect","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","zipRight","resetCheckpoint","reset","asVoid","store","durability"],"sources":["../../src/projectionStore.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
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.4",
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
- "@effect/sql": "^0.52.0",
15
- "effect": "^3.22.0",
16
- "@kairos-es/core": "^1.0.0-alpha.4",
17
- "@kairos-es/read": "^1.0.0-alpha.4"
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
- "types": "./dist/dts/index.d.ts",
25
- "import": "./dist/esm/index.js",
26
- "default": "./dist/cjs/index.js"
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 `@effect/sql`, never on a sibling backend. The
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.pipe(
46
- Schema.nonEmptyString(),
47
- Schema.pattern(/^[^.]+$/, {
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
- // `exact: true` keeps the decoded type at `key?: T` (never `T | undefined`),
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.optionalWith(IdentifierAtom, { exact: true }),
68
- tablePrefix: Schema.optionalWith(IdentifierAtom, { exact: true }),
69
- }).pipe(
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`/`true` passes.
73
- Schema.filter((config) => oversizedIdentifier(config) ?? true),
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
- * `ParseError` on any violation. Used by `makeSqlProjectionStore` and
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` tag — not a package-owned client tag — precisely so a consumer can
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
- import { SqlClient, type SqlError } from '@effect/sql'
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
- * Peers `@effect/sql` and needs only the GENERIC `SqlClient`, deliberately not
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
- * `sql` statements join that transaction through the same client, and
70
- * `withTransaction` is savepoint-aware, so a projection already inside a
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 `@effect/sql`, never on a sibling backend.
87
+ * on `effect/sql`, never on a sibling backend.
86
88
  */
87
89
  export type { ReadPostgresConfig } from './config'
88
90
  export { ensureCheckpointSchema } from './ensureCheckpointSchema'
@@ -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 (`@effect/sql` applies it to every `sql(identifier)`), so
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 '@effect/sql'
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. It is read back as a decimal STRING and
191
- * reconstructed with `BigInt(...)`, never coerced through `number`.
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
@@ -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
- import type { SqlError } from '@effect/sql'
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 typed as a STRING because `bigint`/`int8` arrives from the
67
- * driver as a decimal string: a 64-bit position exceeds JS `number` safety at
68
- * 2^53, so it is reconstructed with `BigInt(...)` and never coerced through
69
- * `number` — the same round-trip the event store performs for its `MAX(id)` head.
70
- * The snake_case key is the column name verbatim; see `withoutTransforms()` below
71
- * for why a consumer's result-name transform cannot rename it under us.
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 tag one store could claim process-wide. Per materialisation rather than
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 tag could not express at all. It
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 because `@effect/sql` carries the transaction's
150
- * connection in the Effect CONTEXT, not in the client value — so any statement run
151
- * through the same underlying client inside `commit` is in the same transaction,
152
- * including through a differently-transformed copy of it. A write to a different
153
- * database, or through an unrelated client, is NOT bracketed and the exactly-once
154
- * property does not extend to it.
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 (`@effect/sql` will happily
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 (the connection travels in the context, not the client).
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.zipRight(guardedAdvance(args)),
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