@kairos-es/read 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +538 -0
  3. package/dist/cjs/EventLogDurability.js +184 -0
  4. package/dist/cjs/EventLogDurability.js.map +1 -0
  5. package/dist/cjs/ProjectionRunner.js +478 -0
  6. package/dist/cjs/ProjectionRunner.js.map +1 -0
  7. package/dist/cjs/ProjectionStore.js +233 -0
  8. package/dist/cjs/ProjectionStore.js.map +1 -0
  9. package/dist/cjs/foldIntoRef.js +36 -0
  10. package/dist/cjs/foldIntoRef.js.map +1 -0
  11. package/dist/cjs/inMemoryProjectionStore.js +138 -0
  12. package/dist/cjs/inMemoryProjectionStore.js.map +1 -0
  13. package/dist/cjs/index.js +128 -0
  14. package/dist/cjs/index.js.map +1 -0
  15. package/dist/cjs/projectionWiringFault.js +532 -0
  16. package/dist/cjs/projectionWiringFault.js.map +1 -0
  17. package/dist/cjs/runProjection.js +117 -0
  18. package/dist/cjs/runProjection.js.map +1 -0
  19. package/dist/cjs/runProjections.js +144 -0
  20. package/dist/cjs/runProjections.js.map +1 -0
  21. package/dist/cjs/superviseOnProgress.js +580 -0
  22. package/dist/cjs/superviseOnProgress.js.map +1 -0
  23. package/dist/cjs/testing.js +143 -0
  24. package/dist/cjs/testing.js.map +1 -0
  25. package/dist/dts/EventLogDurability.d.ts +182 -0
  26. package/dist/dts/EventLogDurability.d.ts.map +1 -0
  27. package/dist/dts/ProjectionRunner.d.ts +557 -0
  28. package/dist/dts/ProjectionRunner.d.ts.map +1 -0
  29. package/dist/dts/ProjectionStore.d.ts +475 -0
  30. package/dist/dts/ProjectionStore.d.ts.map +1 -0
  31. package/dist/dts/foldIntoRef.d.ts +39 -0
  32. package/dist/dts/foldIntoRef.d.ts.map +1 -0
  33. package/dist/dts/inMemoryProjectionStore.d.ts +11 -0
  34. package/dist/dts/inMemoryProjectionStore.d.ts.map +1 -0
  35. package/dist/dts/index.d.ts +185 -0
  36. package/dist/dts/index.d.ts.map +1 -0
  37. package/dist/dts/projectionWiringFault.d.ts +260 -0
  38. package/dist/dts/projectionWiringFault.d.ts.map +1 -0
  39. package/dist/dts/runProjection.d.ts +185 -0
  40. package/dist/dts/runProjection.d.ts.map +1 -0
  41. package/dist/dts/runProjections.d.ts +480 -0
  42. package/dist/dts/runProjections.d.ts.map +1 -0
  43. package/dist/dts/superviseOnProgress.d.ts +587 -0
  44. package/dist/dts/superviseOnProgress.d.ts.map +1 -0
  45. package/dist/dts/testing.d.ts +207 -0
  46. package/dist/dts/testing.d.ts.map +1 -0
  47. package/dist/esm/EventLogDurability.js +175 -0
  48. package/dist/esm/EventLogDurability.js.map +1 -0
  49. package/dist/esm/ProjectionRunner.js +468 -0
  50. package/dist/esm/ProjectionRunner.js.map +1 -0
  51. package/dist/esm/ProjectionStore.js +223 -0
  52. package/dist/esm/ProjectionStore.js.map +1 -0
  53. package/dist/esm/foldIntoRef.js +29 -0
  54. package/dist/esm/foldIntoRef.js.map +1 -0
  55. package/dist/esm/inMemoryProjectionStore.js +131 -0
  56. package/dist/esm/inMemoryProjectionStore.js.map +1 -0
  57. package/dist/esm/index.js +185 -0
  58. package/dist/esm/index.js.map +1 -0
  59. package/dist/esm/package.json +4 -0
  60. package/dist/esm/projectionWiringFault.js +524 -0
  61. package/dist/esm/projectionWiringFault.js.map +1 -0
  62. package/dist/esm/runProjection.js +109 -0
  63. package/dist/esm/runProjection.js.map +1 -0
  64. package/dist/esm/runProjections.js +137 -0
  65. package/dist/esm/runProjections.js.map +1 -0
  66. package/dist/esm/superviseOnProgress.js +571 -0
  67. package/dist/esm/superviseOnProgress.js.map +1 -0
  68. package/dist/esm/testing.js +133 -0
  69. package/dist/esm/testing.js.map +1 -0
  70. package/package.json +41 -0
  71. package/src/EventLogDurability.ts +201 -0
  72. package/src/ProjectionRunner.ts +923 -0
  73. package/src/ProjectionStore.ts +528 -0
  74. package/src/foldIntoRef.ts +63 -0
  75. package/src/inMemoryProjectionStore.ts +163 -0
  76. package/src/index.ts +218 -0
  77. package/src/projectionWiringFault.ts +694 -0
  78. package/src/runProjection.ts +270 -0
  79. package/src/runProjections.ts +623 -0
  80. package/src/superviseOnProgress.ts +897 -0
  81. package/src/testing.ts +290 -0
  82. package/testing/package.json +6 -0
@@ -0,0 +1,475 @@
1
+ /**
2
+ * `ProjectionStore` — the read side's single port: a scalar checkpoint plus the
3
+ * atomic bracket a read model's own view writes are committed INSIDE (ADR-0007).
4
+ *
5
+ * It is deliberately one port rather than two (a checkpoint store and a view
6
+ * store), because the whole exactly-once property of a projection is that the
7
+ * view write and the checkpoint advance land together or not at all. Splitting
8
+ * them would put the bracket in the caller's hands, and a forgotten bracket is
9
+ * silent: the view would commit without the checkpoint advancing and the next
10
+ * pass would apply the same batch again. So `commit` takes `viewWrites` as an
11
+ * ARGUMENT — the caller hands its writes in, the port owns the transaction — and
12
+ * there is no operation on this interface that advances a checkpoint outside it.
13
+ *
14
+ * The checkpoint is a SCALAR `Position`, not a set of processed positions and not
15
+ * a high-water mark with gap tracking. That is licensed by the store's
16
+ * exclusive-lock append (ADR-0002): id-allocation order equals commit order, so
17
+ * once a position is visible every lower position is either already visible or
18
+ * permanently absent. A reader may therefore process strictly ascending and
19
+ * treat an absent position as absent forever. Positions are compared, never
20
+ * arithmetic'd, and never assumed contiguous.
21
+ *
22
+ * The port is MULTI-KEY — every operation takes a `CheckpointKey` — because one
23
+ * `SqlClient` legitimately serves many read models, and a store must keep every
24
+ * key independent. One MATERIALISATION of a read model, however, has exactly ONE
25
+ * key, so it is handed the `KeyedProjectionStore` façade at the bottom of this
26
+ * module rather than the port itself: `forKey` binds a store to a key once, and from
27
+ * there no operation has a key to get wrong. Both surfaces are real and neither
28
+ * replaces the other. A read model may have SEVERAL materialisations, one per store;
29
+ * the `CheckpointKey` doc below says what that does and does not do to key
30
+ * uniqueness.
31
+ *
32
+ * This module is dependency-free by design — branded ids, two tagged errors, two
33
+ * interfaces and the one-line façade that relates them. `@kairos-es/read` ships the
34
+ * in-memory implementation (the rule being that the package defining a port ships
35
+ * that port's dependency-free implementation, as `core` does for the in-memory
36
+ * event store); every dependency-bearing backend is its own package.
37
+ */
38
+ import type { Position } from '@kairos-es/core';
39
+ import { Effect, Schema } from 'effect';
40
+ /**
41
+ * The name of a read model, as a branded non-empty string.
42
+ *
43
+ * Branded rather than a bare `string` for the same reason every other kairos-es
44
+ * identifier is (`Position`, `Tag`, `EventType`): the projection name and the
45
+ * partition name are both non-empty strings with identical shape, so an
46
+ * unbranded pair is trivially transposable at a call site and the mistake would
47
+ * surface only as a projection silently reading somebody else's checkpoint.
48
+ * Construct with `ProjectionId.make(...)`.
49
+ */
50
+ export declare const ProjectionId: Schema.brand<Schema.filter<typeof Schema.String>, "ProjectionId">;
51
+ export type ProjectionId = typeof ProjectionId.Type;
52
+ /**
53
+ * The partition of a read model's checkpoint, as a branded non-empty string.
54
+ *
55
+ * `DEFAULT_PARTITION` is the single value the RUNNER uses, because stream
56
+ * sharding is deliberately not offered: DCB events carry a tag matrix, so two
57
+ * shards can own rows for the same tag while each holds its own checkpoint —
58
+ * both guarded compare-and-sets then succeed and the lost update goes undetected
59
+ * (the guard arbitrates cursor progress, not row ownership). Nothing on this port
60
+ * restricts the value, and a `ProjectionStore` must keep every distinct
61
+ * `(projection, partition)` pair independent — the shared contract suite commits
62
+ * to a second partition precisely to prove that. The dimension is in the KEY so
63
+ * that a restricted, opt-in sharding capability — for read models that provably
64
+ * need no cross-tag ordering and whose every row is owned by one partition value
65
+ * — could be added later with no migration of the checkpoint's shape or storage.
66
+ */
67
+ export declare const PartitionId: Schema.brand<Schema.filter<typeof Schema.String>, "PartitionId">;
68
+ export type PartitionId = typeof PartitionId.Type;
69
+ /** The partition value every runner uses while sharding is cut (ADR-0007). */
70
+ export declare const DEFAULT_PARTITION: PartitionId;
71
+ /**
72
+ * What a checkpoint is stored under: the read model plus its partition.
73
+ *
74
+ * **A key is unique WITHIN ONE STORE**, and stating the scope is load-bearing
75
+ * rather than pedantic: without it, the uniqueness rule below points a reader at the
76
+ * opposite of the right answer for a read model materialised more than once. The
77
+ * scope is a consequence of R1 rather than a policy — the checkpoint is co-located
78
+ * with its view so the pair commits atomically, so a key is only ever resolved
79
+ * against the store holding it, and there is no global namespace to collide in.
80
+ *
81
+ * Two DIFFERENT read models never share a key in one store, and by R3 read models
82
+ * that share one subscription share one cursor and therefore ONE key — a composed
83
+ * bundle checkpoints as a unit and rebuilds as a unit. But ONE read model
84
+ * materialised in several stores SHARES one `ProjectionId` across them, which is
85
+ * what its single identity means, and which
86
+ * `@kairos-es/read-postgres`'s `test/courseRosterGraduation.test.ts` asserts as its
87
+ * acceptance criterion. Distinctness is forced only where two materialisations of
88
+ * one read model COHABIT a store, which is what `PartitionId` is for: two of them
89
+ * in one store under one key would be two runners over one cursor, each advancing
90
+ * the other's, and the read side's construction gate refuses that pairing rather
91
+ * than leaving it to be discovered — a rung only a multi-materialisation call can
92
+ * reach, a lone materialisation having no sibling to collide with.
93
+ */
94
+ export interface CheckpointKey {
95
+ readonly projection: ProjectionId;
96
+ readonly partition: PartitionId;
97
+ }
98
+ /**
99
+ * Build a `CheckpointKey`, defaulting the partition — the constructor nearly
100
+ * every call site wants, so that the sharding dimension stays in the key shape
101
+ * without appearing in ordinary wiring code.
102
+ */
103
+ export declare const checkpointKey: (projection: ProjectionId, partition?: PartitionId) => CheckpointKey;
104
+ /**
105
+ * How far a store's state survives: `'ephemeral'` dies with the process,
106
+ * `'durable'` outlives it.
107
+ *
108
+ * Declared rather than inferred so that R2 — checkpoint durability must not
109
+ * EXCEED event-log durability — is checkable at construction instead of
110
+ * invisible at runtime. This type is the VIEW's half of that comparison;
111
+ * `EventLogDurability` is the log's, and `EventLogDurability.ts` is where R2's
112
+ * argument and the silent failure it prevents are written.
113
+ */
114
+ export type Durability = 'ephemeral' | 'durable';
115
+ declare const ProjectionStoreError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
116
+ readonly _tag: "ProjectionStoreError";
117
+ } & Readonly<A>;
118
+ /**
119
+ * An infrastructure fault from a view store — a connection dropped, a statement
120
+ * failed, the checkpoint table is missing.
121
+ *
122
+ * This is a value on the ERROR channel rather than a defect, which inverts the
123
+ * store contract's rule (`DcbEventStore` dies on infrastructure faults, ADR-0002)
124
+ * and does so on purpose: the read side has a supervisor above it, and the
125
+ * supervisor's whole job is to tell "retry the subscription with backoff" from
126
+ * "give up and report a stalled projection". A defect carries no such
127
+ * distinction. `cause` is exact-optional — only set when there is one.
128
+ */
129
+ export declare class ProjectionStoreError extends ProjectionStoreError_base<{
130
+ readonly reason: string;
131
+ readonly cause?: unknown;
132
+ }> {
133
+ /**
134
+ * `reason` again, as the `Error` field that says why — so the fault renders
135
+ * ITSELF.
136
+ *
137
+ * `Data.TaggedError` sets `name` to the tag and leaves `message` empty, so
138
+ * without this a `ProjectionStoreError` stringifies to `'ProjectionStoreError'`
139
+ * and the sentence explaining the fault reaches a log line only where something
140
+ * OUTSIDE the class knows to go and fetch `reason`. With it,
141
+ * `Error.prototype.toString` yields `'ProjectionStoreError: <reason>'`, and
142
+ * `String(fault)`, `Cause.pretty` (which reads `message` off anything `instanceof
143
+ * Error`) and the projection supervisor's `reason` annotation all take it from the
144
+ * one place it is declared. That matters most for exactly the audience this error
145
+ * exists for: the supervisor above the read side, whose log line is what an
146
+ * operator sees when a view store starts refusing to answer.
147
+ *
148
+ * ## What filling `message` reaches — and what it deliberately leaves alone
149
+ *
150
+ * Not the supervisor's two log lines: `message` is the field `Cause.pretty` reads
151
+ * off anything `instanceof Error`, so this changes how a `ProjectionStoreError`
152
+ * prints EVERYWHERE it is reported — a failed fibre's `cause` annotation on all
153
+ * three shipped loggers, `Effect.logError(message, cause)`, an unhandled
154
+ * failure's report, and any reporter or test runner that reads `.message`. That
155
+ * blast radius is the point of doing it on the class instead of at one call site:
156
+ * one declaration improves every reader at once, and none of them has to know
157
+ * this class's field names.
158
+ *
159
+ * What it does NOT change is the STRUCTURED shape. `Data.Error` overrides
160
+ * `YieldableError`'s `toJSON` with `{ ...plainArgs, ...this }` — own enumerables
161
+ * plus the constructor args, and a prototype accessor is neither — so
162
+ * `JSON.stringify(fault)` and an `Effect.logError(fault)` that JSON-encodes its
163
+ * object message emit exactly the `reason`/`_tag` they always did. The humane
164
+ * rendering gains a sentence; the machine-parsed one is untouched.
165
+ *
166
+ * A GETTER rather than a second field, so `reason` stays the single declaration
167
+ * and the two cannot disagree. Safe as a prototype accessor because
168
+ * `Data.TaggedError`'s constructor is `super(args?.message, …)` followed by
169
+ * `Object.assign(this, args)` (verified against `effect@3.22`'s `Data.ts`) and
170
+ * `message` is not among the fields above, so nothing shadows it with an own
171
+ * property.
172
+ *
173
+ * Both mechanics — that accessor safety and the `toJSON` shape above it — are
174
+ * argued HERE and nowhere else, for every read-side fault that fills the field:
175
+ * `CheckpointSuperseded` below, `PipelineDied` in `ProjectionRunner.ts` and
176
+ * `ProjectionStalled` in `superviseOnProgress.ts` each point back rather than
177
+ * re-derive, and none of the four declares a `message` field for that
178
+ * `Object.assign` to shadow its accessor with. One version pin to re-verify on an
179
+ * `effect` bump, not four.
180
+ */
181
+ get message(): string;
182
+ }
183
+ declare const CheckpointSuperseded_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
184
+ readonly _tag: "CheckpointSuperseded";
185
+ } & Readonly<A>;
186
+ /**
187
+ * The guarded compare-and-set lost: the stored checkpoint no longer equals
188
+ * `expected`, so somebody else advanced this key.
189
+ *
190
+ * A tagged ERROR, never a returned outcome value. A returned
191
+ * `Advanced | Superseded` only rolls the view writes back if the caller remembers
192
+ * to convert it into a failure, and forgetting is silent corruption — the view
193
+ * commits while the checkpoint stays put, and the next pass double-applies the
194
+ * batch. On the error channel the abort is automatic and cannot be ignored.
195
+ *
196
+ * It carries the key and the expectation but deliberately NOT the stored
197
+ * position, mirroring `AppendConditionFailed` (ADR-0002) for the same reason: the
198
+ * recovery path re-reads the checkpoint, so a carried position would already be
199
+ * stale by the time anything acted on it, and carrying one would invite exactly
200
+ * the resume-from-the-conflict-point bug that omitting it prevents.
201
+ *
202
+ * Like its sibling `ProjectionStoreError` it fills `message`, from the expectation
203
+ * it already carries. The getter below says what the field reaches, and also
204
+ * records the reasoning it REPLACED — this class once left `message` empty on a
205
+ * ground that could not be true, and that is worth being able to recognise again.
206
+ */
207
+ export declare class CheckpointSuperseded extends CheckpointSuperseded_base<{
208
+ readonly key: CheckpointKey;
209
+ readonly expected: Position;
210
+ }> {
211
+ /**
212
+ * The sentence this class's own first paragraph writes, plus the expectation the
213
+ * guard turned on — as the `Error` field that says why, so the fault renders
214
+ * ITSELF.
215
+ *
216
+ * `Data.TaggedError` sets `name` to the tag and leaves `message` empty, and
217
+ * `Cause.pretty` substitutes its own `'An error has occurred'` for an empty one,
218
+ * so unfilled this fault printed through a `Cause` reads
219
+ * `'CheckpointSuperseded: An error has occurred'` — a line naming neither a guard,
220
+ * nor a checkpoint, nor an expectation. Filled, `Error.prototype.toString` yields
221
+ * the tag followed by the sentence, everywhere the fault is reported. How far that
222
+ * reaches and what it deliberately leaves alone is set out on
223
+ * `ProjectionStoreError` above; the argument is identical for every fault that
224
+ * fills the field, so it is made once, on the one an operator meets most.
225
+ *
226
+ * ## Why this field was once left EMPTY, and why that reason was wrong
227
+ *
228
+ * Recorded rather than quietly deleted, because a rationale that cannot be true is
229
+ * worse than a missing one and the shape of this mistake is easy to repeat. The
230
+ * argument ran: `expected` is a `bigint`; a `bigint` is not JSON-serialisable, and
231
+ * `effect@3.22`'s `Logger.json` throws outright on one nested in an annotation
232
+ * value and loses the whole line with it; therefore filling `message` would push
233
+ * that value class into the field every renderer reaches for first.
234
+ *
235
+ * The measurement is true and stays load-bearing. The inference from it is a
236
+ * category error. `message` is typed `string`, and TEMPLATE INTERPOLATION of a
237
+ * `bigint` yields its decimal digits, so nothing downstream of this getter can see
238
+ * a `bigint` through it. The hazard is about the values handed to a LOGGER as
239
+ * ANNOTATIONS — which is exactly where the projection supervisor applies it, in
240
+ * `shouldRestart`, whose `position` annotation is `String(stored)` — and it says
241
+ * nothing whatever about a string rendered from one. Nor was the substitute the
242
+ * old argument offered a substitute: the supervisor's `position` annotation is the
243
+ * progress signal it RE-READ after the failure, not this expectation. Meanwhile
244
+ * the cost of the omission was real and was being paid on every `Cause.pretty` of
245
+ * a superseded commit.
246
+ *
247
+ * What the correction does NOT touch is the other ground this class states, which
248
+ * stands unchanged: no STORED position is carried, because the recovery path
249
+ * re-reads and a carried one would already be stale. That decision is about a
250
+ * RECOVERY INPUT — what a caller might wrongly resume from. This getter is a
251
+ * human-readable label over a field the class already carries, and `expected` is
252
+ * exactly as stale in the label as it is in the payload: a reader is being told
253
+ * what the guard expected, never what to resume from.
254
+ *
255
+ * A GETTER rather than a `message` field, so `expected` stays the single
256
+ * declaration. Why a prototype accessor is safe here, and why a structured log of
257
+ * this fault still emits the same `key`/`expected`/`_tag` it always did, are set
258
+ * out once for all four of these getters on `ProjectionStoreError` above.
259
+ */
260
+ get message(): string;
261
+ }
262
+ /**
263
+ * Where read models materialise: a checkpoint and the view it tracks, in one store
264
+ * so each pair can be committed atomically.
265
+ *
266
+ * Each such pair is ONE MATERIALISATION of a read model, and a read model may have
267
+ * several across several stores — which is why the `CheckpointKey` doc above scopes
268
+ * uniqueness to one store rather than globally, and why R1 is what forces that
269
+ * scope.
270
+ *
271
+ * **Multi-key on purpose, and NOT the surface a read model is given.** Every
272
+ * operation takes a `CheckpointKey`, because one store instance — one `SqlClient`,
273
+ * one connection pool — serves however many read models an application runs, and
274
+ * keeping their keys independent is a contract obligation the shared suite proves.
275
+ * One materialisation has exactly one key, though, so it is handed
276
+ * `forKey(store, key)` instead: see `KeyedProjectionStore` below for why the
277
+ * difference is structural rather than stylistic. Implement THIS interface; hand out
278
+ * THAT one.
279
+ *
280
+ * **Annotate an implementation at its DEFINITION site** — `const store:
281
+ * ProjectionStore = { … }`, rather than leaning on the factory's return type alone
282
+ * — so a drift from this interface is a compile error THERE, on the offending
283
+ * member, instead of an opaque mismatch at the export. Both shipped
284
+ * implementations do it and say so in one line; a new backend should too.
285
+ *
286
+ * **The contract a NON-TRANSACTIONAL implementation must honour.** A SQL store
287
+ * runs `commit` in a transaction, so a `viewWrites` that fails PART-WAY is rolled
288
+ * back whole. An in-memory store has no transaction and cannot undo the writes
289
+ * that already landed before the failure. That single divergence is discharged by
290
+ * a requirement on the CALLER of a non-transactional store rather than by
291
+ * capability tiers in the shared contract suite: the per-batch view write MUST be
292
+ * a single atomic effect — fold the batch purely, then perform ONE write (see
293
+ * `foldIntoRef`, which is exactly that shape). Under that requirement a part-way
294
+ * failure is unreachable, every implementation's observable behaviour is
295
+ * identical, and one suite tests them all with no per-backend exemptions. State
296
+ * it loudly at any site that hands a multi-write `apply` to an in-memory store:
297
+ * that, not the store, is the bug.
298
+ *
299
+ * R1 (atomicity) is what this interface IS. The atomic unit is the VIEW's own
300
+ * transaction and never the event log's, so a view in a DIFFERENT database from the
301
+ * log is still exactly-once: the checkpoint travels with the view, and re-reading
302
+ * the log from a position needs no transaction at all, being deterministic and
303
+ * side-effect-free.
304
+ *
305
+ * A view store offering no transaction at all — a search index, an object store, an
306
+ * HTTP API — cannot implement this interface honestly: there is nothing for the
307
+ * checkpoint advance to ride along inside, so such a view is at-least-once and its
308
+ * writes must be IDEMPOTENT. That is a different design and not this port, but it
309
+ * has a recommended shape, so it is written down rather than left to be
310
+ * rediscovered: make every write POSITION-KEYED so re-applying an event no-ops —
311
+ * in SQL terms an `INSERT … ON CONFLICT DO NOTHING` keyed by the event's position,
312
+ * and in a search index or an object store the same idea, the document or object
313
+ * key derived from the position so a replay overwrites itself instead of
314
+ * accumulating. The guarantee then degrades from exactly-once to "at least once,
315
+ * and twice is indistinguishable from once".
316
+ *
317
+ * It is RECOMMENDED rather than mandated (and no helper is shipped for it)
318
+ * because the library cannot force it: the position-keying lives in the read
319
+ * model's own rows, whose shape this port knows nothing about. Nothing here can
320
+ * check it, so the honest move is to document the pattern and say plainly that
321
+ * the obligation is the reader's.
322
+ */
323
+ export interface ProjectionStore {
324
+ /**
325
+ * How far this store's state survives — the R2 input. Checked against the
326
+ * event log's durability at runner construction.
327
+ */
328
+ readonly durability: Durability;
329
+ /**
330
+ * The stored position for `key`. A key that has never been committed reads
331
+ * back as `ORIGIN` (`0n`), so a fresh read model and a reset read model are
332
+ * indistinguishable and both simply catch up from the start of the log.
333
+ */
334
+ readonly readCheckpoint: (key: CheckpointKey) => Effect.Effect<Position, ProjectionStoreError>;
335
+ /**
336
+ * Apply `viewWrites` and advance the checkpoint from `expected` to `next`
337
+ * ATOMICALLY: both land or neither does.
338
+ *
339
+ * The view writes are passed IN rather than the caller opening its own
340
+ * transaction, so the bracket cannot be forgotten and no path exists to a
341
+ * checkpoint advance outside it. `viewWrites` is an arbitrary `Effect`, so
342
+ * nothing that belongs in the same transaction is excluded — the caller's own
343
+ * statements join it through the same client — and its `E`/`R` pass straight
344
+ * through this signature untouched, so a projection keeps its own error and
345
+ * requirement types.
346
+ *
347
+ * Fails `CheckpointSuperseded` when the stored position no longer equals
348
+ * `expected`, and that failure ABORTS the whole commit: the view writes must
349
+ * have no observable effect afterwards. That abort is the exactly-once
350
+ * mechanism, so it is the one behaviour the shared contract suite proves
351
+ * against a known-broken store rather than assuming.
352
+ *
353
+ * `next` is used AS IS. It is the last processed event's position, never
354
+ * `expected + 1` or any other arithmetic: positions are strictly increasing but
355
+ * not gapless, so a jump is normal and an increment would be wrong.
356
+ */
357
+ readonly commit: <E, R>(args: {
358
+ readonly key: CheckpointKey;
359
+ readonly expected: Position;
360
+ readonly next: Position;
361
+ readonly viewWrites: Effect.Effect<void, E, R>;
362
+ }) => Effect.Effect<void, E | CheckpointSuperseded | ProjectionStoreError, R>;
363
+ /**
364
+ * Return `key` to `ORIGIN`, so the next run catches up from the start of the
365
+ * log — the rebuild path.
366
+ *
367
+ * It resets the CHECKPOINT only, never the view: what a rebuild must do with
368
+ * existing rows (truncate them, or write into a fresh table and swap) is the
369
+ * read model's business, and the port has no idea what its rows are.
370
+ */
371
+ readonly resetCheckpoint: (key: CheckpointKey) => Effect.Effect<void, ProjectionStoreError>;
372
+ }
373
+ /**
374
+ * A `ProjectionStore` BOUND to one `CheckpointKey`: the same three operations,
375
+ * with no key to pass and therefore none to get wrong.
376
+ *
377
+ * **Why this exists: an invariant that was convention is now structure.** "One
378
+ * materialisation, one key, one runner" (ADR-0007) was true of every wiring in this
379
+ * repo and of every wiring a reader would write, but nothing enforced it. A
380
+ * `ReadModel` carrying `key` and `store` as INDEPENDENT fields admits two DIFFERENT
381
+ * read models authored against the same key in one store, or one read model whose
382
+ * `apply` reads a neighbour's checkpoint, and neither is a type error — the mistake
383
+ * would surface as one projection silently advancing another's cursor, which is
384
+ * exactly the failure mode `ProjectionId` is branded to prevent at the level below.
385
+ * Binding the key to the store ONCE, at the wiring site, makes the pairing the only
386
+ * thing there is: `commit` no longer has a key argument, so there is no key to
387
+ * transpose, omit, or refresh from the wrong variable.
388
+ *
389
+ * **`runProjections` is the same move one level up, and the collision gate is what
390
+ * binding alone cannot reach.** Where this façade binds a key to a store, that
391
+ * function binds ONE slice record and ONE `ProjectionId` across N materialisations
392
+ * and does the `forKey` call per entry itself, so "one read model, N
393
+ * materialisations, one identity" stops being a convention too. What binding cannot
394
+ * catch is the one incoherence that is invisible AFTER it: two entries bound to the
395
+ * same store under equal keys are each a perfectly well-formed
396
+ * `KeyedProjectionStore`, and `forKey` deliberately does not expose the store it
397
+ * closed over, so nothing downstream can ask whether two of them are the same. That
398
+ * question is only answerable at the UNBOUND port, which is where
399
+ * the read side's construction gate asks it.
400
+ *
401
+ * It is a FAÇADE and not a replacement. The multi-key port underneath is a real
402
+ * requirement — one client serves many read models, and per-key independence is
403
+ * contract behaviour the shared suite proves against every backend — so `forKey`
404
+ * narrows a store for one consumer rather than narrowing the port for everyone.
405
+ *
406
+ * `readCheckpoint` and `resetCheckpoint` are Effect VALUES rather than
407
+ * zero-argument functions. An `Effect` is a lazy description, so a value is
408
+ * re-runnable and a no-argument call has nothing left to express; `yield*
409
+ * view.readCheckpoint` is then the whole operation, with no vestigial `()` to
410
+ * suggest an argument might one day return.
411
+ */
412
+ export interface KeyedProjectionStore {
413
+ /**
414
+ * The key this store is bound to.
415
+ *
416
+ * Carried rather than dropped because the RUNNER still needs it: its log
417
+ * annotations and its `PipelineDied`/`ProjectionStalled` payloads name BOTH halves —
418
+ * an operator greps on `projection` and `partition` together — and its
419
+ * construction-time defect messages name at least the projection. That is the whole
420
+ * reason the pair travels as one value: an operator's first question about a stalled
421
+ * projection is which one, and in a store where two materialisations cohabit the
422
+ * partition is the half that answers it. It
423
+ * is exposed for READING; nothing here takes it as an argument, which is the
424
+ * point.
425
+ */
426
+ readonly key: CheckpointKey;
427
+ /** The underlying store's durability — the R2 input, passed straight through. */
428
+ readonly durability: Durability;
429
+ /** The stored position for this store's key. `ORIGIN` when never committed. */
430
+ readonly readCheckpoint: Effect.Effect<Position, ProjectionStoreError>;
431
+ /**
432
+ * Apply `viewWrites` and advance this key's checkpoint from `expected` to
433
+ * `next` atomically — `ProjectionStore.commit` with the one argument a caller
434
+ * could get wrong already supplied.
435
+ */
436
+ readonly commit: <E, R>(args: {
437
+ readonly expected: Position;
438
+ readonly next: Position;
439
+ readonly viewWrites: Effect.Effect<void, E, R>;
440
+ }) => Effect.Effect<void, E | CheckpointSuperseded | ProjectionStoreError, R>;
441
+ /** Return this key to `ORIGIN` — the rebuild path, checkpoint only. */
442
+ readonly resetCheckpoint: Effect.Effect<void, ProjectionStoreError>;
443
+ }
444
+ /**
445
+ * Bind a `ProjectionStore` to one `CheckpointKey`, producing the surface a read
446
+ * model is actually given.
447
+ *
448
+ * A FREE FUNCTION rather than a method on `ProjectionStore`, decided that way for
449
+ * two reasons beyond the obvious one (a single implementation, which every present
450
+ * and future backend then gets for nothing rather than re-deriving — and could
451
+ * re-derive WRONGLY, since a `forKey` that closed over the wrong key would be
452
+ * invisible until two read models started trading cursors).
453
+ *
454
+ * The sharper reason is DECORATORS. Wrapping a store is an established move here —
455
+ * a test's never-committing view store, a durability override — and every one of
456
+ * them is written `{ ...inner, commit: … }`. Were `forKey` a method, that spread
457
+ * would copy the INNER store's own `forKey`, whose closure still points at the
458
+ * inner store, so `forKey(decorated, key)` would silently route straight past the
459
+ * decoration. As a free function it reads `store.commit` off the value it is
460
+ * handed, so a decorated store decorates.
461
+ *
462
+ * The second reason is the port's implementation burden: `ProjectionStore` stays
463
+ * three methods, which is what a backend has to get right, and no implementation
464
+ * can drift on a member that is pure derivation.
465
+ *
466
+ * `Effect.suspend` on the two no-argument members preserves the port's per-call
467
+ * construction timing exactly: an implementation is entitled to build its effect
468
+ * when its method is called (the SQL store binds parameters there), so suspending
469
+ * keeps `yield* view.readCheckpoint` indistinguishable from
470
+ * `yield* store.readCheckpoint(key)` rather than freezing one construction for the
471
+ * store's lifetime.
472
+ */
473
+ export declare const forKey: (store: ProjectionStore, key: CheckpointKey) => KeyedProjectionStore;
474
+ export {};
475
+ //# sourceMappingURL=ProjectionStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ProjectionStore.d.ts","sourceRoot":"","sources":["../../src/ProjectionStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAC/C,OAAO,EAAQ,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAE7C;;;;;;;;;GASG;AACH,eAAO,MAAM,YAAY,mEAGxB,CAAA;AACD,MAAM,MAAM,YAAY,GAAG,OAAO,YAAY,CAAC,IAAI,CAAA;AAEnD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,WAAW,kEAGvB,CAAA;AACD,MAAM,MAAM,WAAW,GAAG,OAAO,WAAW,CAAC,IAAI,CAAA;AAEjD,8EAA8E;AAC9E,eAAO,MAAM,iBAAiB,EAAE,WAAyC,CAAA;AAEzE;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,UAAU,EAAE,YAAY,CAAA;IACjC,QAAQ,CAAC,SAAS,EAAE,WAAW,CAAA;CAChC;AAED;;;;GAIG;AACH,eAAO,MAAM,aAAa,GACxB,YAAY,YAAY,EACxB,YAAW,WAA+B,KACzC,aAA4C,CAAA;AAE/C;;;;;;;;;GASG;AACH,MAAM,MAAM,UAAU,GAAG,WAAW,GAAG,SAAS,CAAA;;;;AAEhD;;;;;;;;;;GAUG;AACH,qBAAa,oBAAqB,SAAQ,0BAExC;IACA,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CACzB,CAAC;IACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA+CG;IACH,IAAa,OAAO,IAAI,MAAM,CAE7B;CACF;;;;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,oBAAqB,SAAQ,0BAExC;IACA,QAAQ,CAAC,GAAG,EAAE,aAAa,CAAA;IAC3B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAA;CAC5B,CAAC;IACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAgDG;IACH,IAAa,OAAO,IAAI,MAAM,CAE7B;CACF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4DG;AACH,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAA;IAE/B;;;;OAIG;IACH,QAAQ,CAAC,cAAc,EAAE,CACvB,GAAG,EAAE,aAAa,KACf,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,oBAAoB,CAAC,CAAA;IAElD;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE;QAC5B,QAAQ,CAAC,GAAG,EAAE,aAAa,CAAA;QAC3B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAA;QAC3B,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;QACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;KAC/C,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,GAAG,oBAAoB,GAAG,oBAAoB,EAAE,CAAC,CAAC,CAAA;IAE7E;;;;;;;OAOG;IACH,QAAQ,CAAC,eAAe,EAAE,CACxB,GAAG,EAAE,aAAa,KACf,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,oBAAoB,CAAC,CAAA;CAC/C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,MAAM,WAAW,oBAAoB;IACnC;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,GAAG,EAAE,aAAa,CAAA;IAE3B,iFAAiF;IACjF,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAA;IAE/B,+EAA+E;IAC/E,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,oBAAoB,CAAC,CAAA;IAEtE;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE;QAC5B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAA;QAC3B,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;QACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;KAC/C,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,GAAG,oBAAoB,GAAG,oBAAoB,EAAE,CAAC,CAAC,CAAA;IAE7E,uEAAuE;IACvE,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,oBAAoB,CAAC,CAAA;CACpE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,eAAO,MAAM,MAAM,GACjB,OAAO,eAAe,EACtB,KAAK,aAAa,KACjB,oBAmBD,CAAA"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * `foldIntoRef` — the folded-state read model's `apply`: fold a batch PURELY
3
+ * through `composeProjections`, then perform exactly ONE write.
4
+ *
5
+ * This is the shape the `ProjectionStore` port's single-atomic-effect requirement
6
+ * asks for, and it is why that requirement costs nothing. A non-transactional view
7
+ * store cannot roll back a `viewWrites` that fails part-way; a fold followed by a
8
+ * single `Ref.update` has no part-way to fail at — the fold is pure (it cannot
9
+ * fail, and it touches nothing observable) and the write is a single atomic step.
10
+ * So the one divergence between a transactional and a non-transactional
11
+ * `ProjectionStore` is unreachable for a read model written this way, which is
12
+ * what keeps the shared contract suite free of capability tiers.
13
+ *
14
+ * It is deliberately a thin helper over primitives that already exist rather than
15
+ * a new fold abstraction: the composition is `core`'s `composeProjections`, the
16
+ * slices are the codec's `projection().on().build()`, and this module only wires
17
+ * the two to a `Ref`. Nothing here is required — a read model with its own
18
+ * multi-statement SQL `apply` ignores it entirely and relies on the transaction
19
+ * instead. It is the RECOMMENDED `apply` for an in-memory view specifically.
20
+ */
21
+ import type { SliceProjection } from '@kairos-es/codec';
22
+ import { type CompositeState, type DecodedEvent } from '@kairos-es/core';
23
+ import type { Array as Arr } from 'effect';
24
+ import { type Effect, Ref } from 'effect';
25
+ /**
26
+ * Build the per-batch `apply` for a read model whose whole view is `view`.
27
+ *
28
+ * `slices` must be the SAME record the read model's subscription query is derived
29
+ * from, so the events the runner delivers are exactly the events these folds
30
+ * expect; `composeProjections` gates each slice on its own query before
31
+ * dispatching, so a slice never sees an event it did not ask for.
32
+ *
33
+ * The returned effect is infallible (`E = never`) and requirement-free
34
+ * (`R = never`), which is the whole point: handed to `ProjectionStore.commit` as
35
+ * `viewWrites`, it adds nothing to the commit's error or requirement channels, so
36
+ * the ONLY way that commit can fail is the checkpoint guard.
37
+ */
38
+ export declare const foldIntoRef: <T extends Record<string, SliceProjection>>(slices: T, view: Ref.Ref<CompositeState<T>>) => ((batch: Arr.NonEmptyReadonlyArray<DecodedEvent>) => Effect.Effect<void>);
39
+ //# sourceMappingURL=foldIntoRef.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"foldIntoRef.d.ts","sourceRoot":"","sources":["../../src/foldIntoRef.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AACvD,OAAO,EACL,KAAK,cAAc,EAEnB,KAAK,YAAY,EAClB,MAAM,iBAAiB,CAAA;AACxB,OAAO,KAAK,EAAE,KAAK,IAAI,GAAG,EAAE,MAAM,QAAQ,CAAA;AAC1C,OAAO,EAAE,KAAK,MAAM,EAAE,GAAG,EAAE,MAAM,QAAQ,CAAA;AAEzC;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,WAAW,GAAI,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,EACnE,QAAQ,CAAC,EACT,MAAM,GAAG,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,KAC/B,CAAC,CACF,KAAK,EAAE,GAAG,CAAC,qBAAqB,CAAC,YAAY,CAAC,KAC3C,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAevB,CAAA"}
@@ -0,0 +1,11 @@
1
+ import { Effect } from 'effect';
2
+ import type { ProjectionStore } from './ProjectionStore.js';
3
+ /**
4
+ * Build an in-memory `ProjectionStore`.
5
+ *
6
+ * Every call is an independent store with its own checkpoints and its own
7
+ * semaphore, so two read models wired to two calls of this factory cannot
8
+ * interfere — and a test needing a fresh store just calls it again.
9
+ */
10
+ export declare const makeInMemoryProjectionStore: Effect.Effect<ProjectionStore>;
11
+ //# sourceMappingURL=inMemoryProjectionStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inMemoryProjectionStore.d.ts","sourceRoot":"","sources":["../../src/inMemoryProjectionStore.ts"],"names":[],"mappings":"AAoCA,OAAO,EAAE,MAAM,EAAO,MAAM,QAAQ,CAAA;AACpC,OAAO,KAAK,EAAiB,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAuBvE;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,EAAE,MAAM,CAAC,MAAM,CAAC,eAAe,CA+FnE,CAAA"}