@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,207 @@
1
+ /**
2
+ * Test-support for the READ side, shared across every kairos-es suite — and
3
+ * across a consumer's own suites — via the published `@kairos-es/read/testing`
4
+ * subpath.
5
+ *
6
+ * ## Why the read side needs a testing surface at all
7
+ *
8
+ * A projection is a DAEMON. Every assertion about a read model is therefore an
9
+ * assertion about a forked fibre that has not necessarily finished, so the very
10
+ * first thing anybody writing a read-model test has to invent is "run this
11
+ * projection until it has consumed the log, THEN look at the view". There is no
12
+ * other way to phrase it: `runProjection` returns as soon as the daemon is
13
+ * forked, on purpose, because starting a projection is not the same event as a
14
+ * projection having caught up.
15
+ *
16
+ * Left uninvented, that gap is filled by a sleep — and a sleep is the one answer
17
+ * that is wrong in both directions at once. Too short and the suite is flaky on a
18
+ * loaded machine; too long and every case pays for the worst machine anybody runs
19
+ * it on. So the answer here is to POLL THE OBSERVABLE STATE: re-read the stored
20
+ * checkpoint until it reaches the position under test, under a generous ceiling
21
+ * whose only job is to turn a genuine hang into a failed assertion NAMING the
22
+ * position that was never reached, rather than an undiagnosable framework
23
+ * timeout. A slow machine then takes longer; it does not fail.
24
+ *
25
+ * That reasoning was written out three times in this repo — in `read`'s own
26
+ * runner suite, in `read-postgres`'s live-log cases and in the worked example —
27
+ * with three slightly different timeout strings, which is the usual evidence that
28
+ * a library is missing a surface rather than that its tests are untidy. It is
29
+ * written once here, and everything else imports it.
30
+ *
31
+ * ## Why a published SUBPATH rather than a package
32
+ *
33
+ * `@kairos-es/core/testing` is the precedent and the test this module has to
34
+ * pass: a subpath shares its package's dependency surface, so it may exist only
35
+ * if it adds NO peer to that surface. Nothing below imports anything
36
+ * `@kairos-es/read` does not already peer (`effect`, `@kairos-es/core`,
37
+ * `@kairos-es/codec`), and in particular nothing here imports a test FRAMEWORK.
38
+ * There is no `expect`, no `it`, no `@effect/vitest`: what a caller gets is plain
39
+ * `Effect`s (and one pure function, `lastPosition`) run under whatever runner it
40
+ * already has. That is what keeps `@kairos-es/read`'s published peer set
41
+ * unchanged, and it is why this is not `@kairos-es/testing`, which peers
42
+ * `@effect/vitest` because a Given/When/Then surface genuinely needs one.
43
+ *
44
+ * ## The boundary against `@kairos-es/projection-store-contract-tests`
45
+ *
46
+ * That package publishes `keyFor` / `position` / `storedPosition` /
47
+ * `advanceCheckpoint`, and the overlap is apparent rather than real. Those are
48
+ * tools for proving the MULTI-KEY PORT: they brand raw strings and bigints into
49
+ * the port's domain types and perform the setup moves a guard case needs, they
50
+ * are addressed to somebody implementing a `ProjectionStore` BACKEND, and the
51
+ * package is private and unpublished. This module is addressed to somebody
52
+ * running a READ MODEL — an application author, or one of this repo's own suites
53
+ * — and every export that observes a view takes the `KeyedProjectionStore` a read
54
+ * model is actually handed, never a `(store, key)` pair. (`lastPosition` takes no
55
+ * store at all: it derives a convergence TARGET from what `append` returned, which
56
+ * is not an observation of a view.) Same domain, different surface, different
57
+ * publication status. Neither imports the other: `read` cannot depend on a suite
58
+ * that already depends on `read`.
59
+ *
60
+ * ## Platform-neutral, like the rest of the package
61
+ *
62
+ * No `node:*`, no `Date`, no host RNG. The poll interval and the ceiling are
63
+ * Effect `Duration`s scheduled through the `Clock`, so a caller who can drive the
64
+ * clock still can.
65
+ */
66
+ import type { RequirementOf, Serializer, SliceProjection } from '@kairos-es/codec';
67
+ import type { DcbEventStore, Position } from '@kairos-es/core';
68
+ import { type Duration, Effect, type Scope } from 'effect';
69
+ import type { EventLogDurability } from './EventLogDurability.js';
70
+ import type { ProjectionRunner, ProjectionRunnerOptions } from './ProjectionRunner.js';
71
+ import type { KeyedProjectionStore } from './ProjectionStore.js';
72
+ import { type ReadModel } from './runProjection.js';
73
+ /**
74
+ * How a convergence wait is paced and bounded.
75
+ *
76
+ * Both figures are per-DEPLOYMENT judgements rather than contract, in the same
77
+ * sense the runner's own tuning is: an in-memory log converges in microseconds
78
+ * and a containerised Postgres does not, so the defaults suit the former and the
79
+ * latter says so at its call site.
80
+ */
81
+ export interface AwaitCheckpointOptions {
82
+ /** How often to re-read the stored checkpoint. Default `'2 millis'`. */
83
+ readonly pollInterval?: Duration.DurationInput;
84
+ /**
85
+ * How long to wait before treating the projection as hung. Default
86
+ * `'10 seconds'`. Exceeding it is a DEFECT, not a failure — see
87
+ * `awaitCheckpoint`.
88
+ */
89
+ readonly timeout?: Duration.DurationInput;
90
+ }
91
+ /**
92
+ * The stored checkpoint of a read model's bound view store, right now.
93
+ *
94
+ * `orDie` because a `ProjectionStoreError` means the test's own fixture cannot
95
+ * read its own checkpoint. The port puts that on the ERROR channel for the
96
+ * runner's supervisor, which has to tell retry-with-backoff from give-up
97
+ * (ADR-0007) — but a test has no supervisor and nothing to retry, so surfacing it
98
+ * as a defect keeps a broken fixture clearly distinct from the case's own
99
+ * assertion failing, and keeps every signature here `E = never` so a case never
100
+ * has to widen its own error channel to look at a view.
101
+ */
102
+ export declare const storedCheckpoint: (store: KeyedProjectionStore) => Effect.Effect<Position>;
103
+ /**
104
+ * Wait until the stored checkpoint has reached `target`, then return it.
105
+ *
106
+ * The read side's answer to "has the projection caught up yet?", and the reason
107
+ * a read-model test needs no sleep: it polls the OBSERVABLE state, so it ends the
108
+ * instant the runner has actually got there and no sooner. Comparison is `>=`
109
+ * rather than `===` and that is load-bearing — positions are strictly increasing
110
+ * but NOT gapless, so a target derived from an append may be overshot by a batch
111
+ * that carried later events too, and an equality test would then wait for a
112
+ * position the log will never store.
113
+ *
114
+ * A KEYED store rather than a `(store, key)` pair, matching the shape the package
115
+ * settled on everywhere else: a read model's materialisation carries
116
+ * `forKey(store, key)` INSTEAD of a key field, so "one materialisation, one key" is
117
+ * structural rather than conventional, and the same argument applies to observing
118
+ * one. There is no key to transpose, the timeout message names `store.key` and
119
+ * therefore cannot name a key the poll did not use, and `readModel.store` is
120
+ * already exactly the right argument. A caller holding the multi-key port writes
121
+ * `forKey(store, key)`, which is the library's own one-line adapter and is
122
+ * `store.readCheckpoint(key)` with a `suspend` around it — so a suite deliberately
123
+ * observing through the RAW port (as `read-postgres`' live-log cases do, to keep
124
+ * "the façade wrote where the port reads" an observation rather than an assumption)
125
+ * loses nothing.
126
+ *
127
+ * A read model materialised N ways (`runProjections`) is N of these observations,
128
+ * one per store, and there is deliberately no plural helper for it. Under one
129
+ * `ProjectionId` and the default partition the N keys are IDENTICAL — a key names a
130
+ * cursor within a store — so the STORE is what distinguishes the materialisations,
131
+ * and each runner carries its own: `awaitCheckpoint(runners[i].store, target)` is
132
+ * already the per-entry observation, over the very binding the call made. So a plural
133
+ * helper would take nothing a caller does not already hold, and the N waits are one
134
+ * `Effect.all` over the runners it was handed — concurrently, since waiting on them
135
+ * in turn would prove only that each converges once the others have.
136
+ *
137
+ * Exceeding the timeout is a DEFECT, so this stays `E = never` and a case need
138
+ * not thread a timeout error it has no intention of handling. It is the right
139
+ * classification anyway: a projection that never converged is a broken test or a
140
+ * broken library, never an outcome to assert on.
141
+ */
142
+ export declare const awaitCheckpoint: (store: KeyedProjectionStore, target: Position, options?: AwaitCheckpointOptions) => Effect.Effect<Position>;
143
+ /**
144
+ * Everything `runProjectionUntil` takes: the runner's own tuning plus the wait's.
145
+ *
146
+ * ONE FLAT object, for the reason `ProjectionRunnerOptions` is itself flat —
147
+ * `{ ...OPTIONS, oneField }` is the dominant override idiom in this repo, and a
148
+ * spread is shallow, so a nested shape would silently drop a whole group's
149
+ * siblings. The two halves stay distinct TYPES, which is where the distinction
150
+ * belongs; the field names are disjoint, so nothing is ambiguous at a call site.
151
+ */
152
+ export interface RunProjectionUntilOptions extends ProjectionRunnerOptions, AwaitCheckpointOptions {
153
+ }
154
+ /**
155
+ * Start the projection maintaining `readModel` and wait until its checkpoint has
156
+ * reached `target` — "run this until it has consumed the log", as one call.
157
+ *
158
+ * This is the surface the read side was missing. `runProjection` returns once the
159
+ * daemon is FORKED, which is the only honest thing it can do, so every read-model
160
+ * test is otherwise a two-step dance the author has to know to write; this pairs
161
+ * the two steps so that a case's next line can assert on the view.
162
+ *
163
+ * It returns the `ProjectionRunner` rather than swallowing it, because everything
164
+ * a case might do next needs the handle: `Fiber.poll` it (is the daemon still
165
+ * up?), `Fiber.interrupt` it early, or await it. The scope is still the teardown
166
+ * mechanism — this forks into the CALLER's `Scope`, exactly as `runProjection`
167
+ * does, so closing that scope interrupts the daemon and waits for it.
168
+ *
169
+ * ## What it deliberately is NOT
170
+ *
171
+ * A TEST helper, not a production freshness API. It reports nothing about lag, it
172
+ * cannot enumerate checkpoints, and no consumer should reach for it to make a
173
+ * request wait for its own write to be projected — a read model is eventually
174
+ * consistent by construction and a `waitUntilProcessed` on the hot path would be
175
+ * a way to pretend otherwise. That is why it lives behind `/testing` and not on
176
+ * the package's main entry. Where a request genuinely must see its own write, the
177
+ * honest answer is not to wait on a MAINTAINED materialisation at all: fold the log
178
+ * at query time with `modelAtHead` from `@kairos-es/codec`, which is consistent as
179
+ * of the `head` its read returned and therefore never behind a checkpoint.
180
+ *
181
+ * It also does not race the daemon's fibre against the wait. A projection that
182
+ * gives up fails `ProjectionStalled`, and it would be possible to surface that
183
+ * here instead of timing out — but the daemon's failure and the convergence
184
+ * ceiling are two independent clocks, and a helper whose diagnosis depended on
185
+ * which fired first is a worse instrument than one that always says the same
186
+ * thing. A case that wants the stall itself has the fibre returned above, and
187
+ * `onStalled` besides.
188
+ */
189
+ export declare const runProjectionUntil: <T extends Record<string, SliceProjection>, E, R>(readModel: ReadModel<T, E, R>, target: Position, options?: RunProjectionUntilOptions) => Effect.Effect<ProjectionRunner, never, Scope.Scope | DcbEventStore | EventLogDurability | Serializer | RequirementOf<T> | R>;
190
+ /**
191
+ * The LAST position of a sequence — the convergence target, derived from what
192
+ * `append` actually reported.
193
+ *
194
+ * The companion to the two waits above, and the reason it is a helper rather than
195
+ * `positions[positions.length - 1]`: a fixture that appended nothing would
196
+ * otherwise yield `undefined`, and a wait for `undefined` is either a type error
197
+ * at best or a vacuous assertion at worst. Failing loudly at the point the
198
+ * fixture is wrong is worth four lines.
199
+ *
200
+ * Targets are built this way — from the positions the log HANDED OUT — rather
201
+ * than from a literal `1n..Nn`, because positions are strictly increasing and NOT
202
+ * gapless: a rolled-back append burns an id, and a tag-narrowed subscription
203
+ * never delivers the positions it does not match. Nothing here does arithmetic on
204
+ * a position.
205
+ */
206
+ export declare const lastPosition: (positions: ReadonlyArray<Position>) => Position;
207
+ //# sourceMappingURL=testing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../../src/testing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgEG;AACH,OAAO,KAAK,EACV,aAAa,EACb,UAAU,EACV,eAAe,EAChB,MAAM,kBAAkB,CAAA;AACzB,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAC9D,OAAO,EAAgB,KAAK,QAAQ,EAAE,MAAM,EAAU,KAAK,KAAK,EAAE,MAAM,QAAQ,CAAA;AAChF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAC9D,OAAO,KAAK,EACV,gBAAgB,EAChB,uBAAuB,EACxB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AAC7D,OAAO,EAAE,KAAK,SAAS,EAAiB,MAAM,iBAAiB,CAAA;AAoB/D;;;;;;;GAOG;AACH,MAAM,WAAW,sBAAsB;IACrC,wEAAwE;IACxE,QAAQ,CAAC,YAAY,CAAC,EAAE,QAAQ,CAAC,aAAa,CAAA;IAE9C;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,aAAa,CAAA;CAC1C;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gBAAgB,GAC3B,OAAO,oBAAoB,KAC1B,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAuC,CAAA;AAEhE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,eAAO,MAAM,eAAe,GAC1B,OAAO,oBAAoB,EAC3B,QAAQ,QAAQ,EAChB,UAAU,sBAAsB,KAC/B,MAAM,CAAC,MAAM,CAAC,QAAQ,CAkBtB,CAAA;AAEH;;;;;;;;GAQG;AACH,MAAM,WAAW,yBACf,SAAQ,uBAAuB,EAC7B,sBAAsB;CAAG;AAE7B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,eAAO,MAAM,kBAAkB,GAC7B,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,EACzC,CAAC,EACD,CAAC,EAED,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAC7B,QAAQ,QAAQ,EAChB,UAAU,yBAAyB,KAClC,MAAM,CAAC,MAAM,CACd,gBAAgB,EAChB,KAAK,EACH,KAAK,CAAC,KAAK,GACX,aAAa,GACb,kBAAkB,GAClB,UAAU,GACV,aAAa,CAAC,CAAC,CAAC,GAChB,CAAC,CAOF,CAAA;AAEH;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,YAAY,GAAI,WAAW,aAAa,CAAC,QAAQ,CAAC,KAAG,QAI/D,CAAA"}
@@ -0,0 +1,175 @@
1
+ /**
2
+ * `EventLogDurability` — the R2 input the EVENT LOG contributes: how far the log a
3
+ * projection subscribes to survives a restart, declared by the application that
4
+ * chose the store's `Layer` and read from context in ONE place — the
5
+ * `prepareProjection` phase both runner entry points share, which is also the gate's
6
+ * only caller.
7
+ *
8
+ * ## The rule: R2, durability ordering
9
+ *
10
+ * This is R2's home, and every other mention in the read side points here rather
11
+ * than restating it.
12
+ *
13
+ * Checkpoint durability must not EXCEED event-log durability. The comparison has
14
+ * two halves, each declared where it is known: the VIEW store carries its own
15
+ * class on the `ProjectionStore` it implements, and the LOG's class is this tag. A
16
+ * DURABLE view over an EPHEMERAL log is the one illegal pairing of the four.
17
+ *
18
+ * It is illegal because the failure is SILENT. The checkpoint survives a restart
19
+ * the log does not, so on the next boot `subscribe(query, checkpoint)` returns
20
+ * nothing against a re-emptied log: no fibre dies, no error reaches any channel,
21
+ * no log line appears, and the view simply sits frozen and permanently stale while
22
+ * continuing to serve reads. Nothing at runtime is placed to notice it, which is
23
+ * why the two declarations are compared BEFORE anything is forked.
24
+ *
25
+ * `durabilityOrderingFault` at the foot of this module is that comparison — the
26
+ * CHECK beside the argument for it, so a reader who comes here for the rule finds
27
+ * the code that enforces it and the sentence an author meets when they break it,
28
+ * rather than a restatement of the rule four hundred lines away in the pipeline
29
+ * module. It is the first PER-ENTRY rung of the five rules `projectionWiringFault`
30
+ * chains, behind only the set-level collision rung, and is judged for every
31
+ * materialisation in the call; that module owns the argument for the whole order.
32
+ *
33
+ * The asymmetry is deliberate: getting the declaration wrong in the SAFE direction
34
+ * — saying `'ephemeral'` over a genuinely durable log — costs nothing but a durable
35
+ * view, so nothing checks it. Only the unsafe direction is rejected.
36
+ *
37
+ * ## Why it is DECLARED at all
38
+ *
39
+ * `DcbEventStore` deliberately exposes no durability, and that opacity is a
40
+ * feature rather than an omission: a runner never learns which engine it is
41
+ * running against, which is exactly what lets one projection sit over the
42
+ * in-memory log in a test and over Postgres in production with nothing in between
43
+ * changing. The store contract is untouched by the read side. But R2 has to know
44
+ * the log's class, so the only honest source is the application that picked the
45
+ * layer.
46
+ *
47
+ * ## Why a `Context.Tag` rather than a field on `ReadModel`
48
+ *
49
+ * ONE log, ONE declaration. The fact being declared is a property of the EVENT
50
+ * LOG, and every read model in a deployment reads the same log, so authoring it
51
+ * per read model states one fact N times at N sites — and N copies can DISAGREE.
52
+ * One read model declaring `'durable'` beside a neighbour declaring `'ephemeral'`
53
+ * over the identical `DcbEventStore` is not a type error, and nothing anywhere
54
+ * could notice: `runProjection` only ever sees the one pair it was handed, so
55
+ * there is no vantage point from which the contradiction is even visible. The read
56
+ * side's own scaling story is one runner per materialisation — horizontal scale by
57
+ * functional decomposition, never by sharding one read model's stream (ADR-0007) —
58
+ * so N > 1 is the EXPECTED case, and the unsafe direction is then reachable by a
59
+ * copy-paste that gets one of N wrong. N materialisations of ONE read model
60
+ * (`runProjections`) only sharpen that: their whole point is that the view stores
61
+ * differ, so their R2 verdicts differ too and each is judged separately against this
62
+ * one declaration — which is precisely why the LOG's half must not be authored
63
+ * alongside them.
64
+ *
65
+ * Provided beside the store layer, the declaration sits with the choice it
66
+ * describes: `Layer.merge(DcbEventStoreInMemory, EphemeralEventLog)` names the log
67
+ * and its durability class in one expression, and there is exactly one of it
68
+ * however many runners the graph goes on to carry.
69
+ *
70
+ * The cost is real and taken deliberately: this is a REQUIRED service in the `R` of
71
+ * `runProjection`, `projectionLayer` and `runProjections` alike, which every consumer
72
+ * must provide — and most costly to forget at `runProjections`, where one missing
73
+ * layer refuses N materialisations at once. Defaulting it would
74
+ * defeat the point in both directions — defaulting to `'ephemeral'` would make the
75
+ * PRODUCTION wiring the one you have to remember, and defaulting to `'durable'`
76
+ * would turn a forgotten declaration into the silently frozen view R2 exists to
77
+ * prevent.
78
+ *
79
+ * ## Why the store layers do not provide it themselves
80
+ *
81
+ * `DcbEventStoreInMemory` could plausibly provide `'ephemeral'` on its own and make
82
+ * the all-in-memory case correct by default. It deliberately does not, for three
83
+ * reasons that outweigh the ergonomics.
84
+ *
85
+ * This is a READ-SIDE tag, and `@kairos-es/core` knows nothing of the read side —
86
+ * `read` peers `core`, not the other way round — so core would have to DEFINE the
87
+ * tag for its layer to provide it. That puts a read-side rule inside the store
88
+ * contract's own package, which is precisely the coupling the contract's
89
+ * durability-opacity exists to avoid.
90
+ *
91
+ * It would also move the declaration from the APPLICATION to the BACKEND. Then a
92
+ * backend that got its own class wrong (a log that persists only on a flag, say)
93
+ * would be wrong for every read model at once with no wiring site left to correct
94
+ * it, and the whole reason R2 keys on a declaration rather than on the store is
95
+ * that only the application knows.
96
+ *
97
+ * And two providers of one tag is not an error in a layer graph — it is resolved
98
+ * by merge order. An application that declared `DurableEventLog` beside a store
99
+ * layer declaring `'ephemeral'` would get whichever merge happened to win, silently,
100
+ * which is a worse failure than the one being fixed. The single token
101
+ * `EphemeralEventLog` costs is not worth any of that.
102
+ */
103
+ import { Context, Layer } from 'effect';
104
+ /**
105
+ * The event log's durability class, as the application declares it.
106
+ *
107
+ * The service value is the bare `Durability` rather than a record wrapping it:
108
+ * the tag's name says exactly what it holds, so `yield* EventLogDurability` is the
109
+ * whole read, and `Layer.succeed(EventLogDurability, 'durable')` is the whole
110
+ * declaration for anyone not reaching for the two layers below.
111
+ */
112
+ export class EventLogDurability extends /*#__PURE__*/Context.Tag('@kairos-es/read/EventLogDurability')() {}
113
+ /**
114
+ * Declare that the event log OUTLIVES the process — the production wiring, beside
115
+ * a durable `DcbEventStore` layer such as `@kairos-es/store-postgres`'s.
116
+ *
117
+ * Shipped as a `Layer` rather than left to `Layer.succeed` at each wiring site for
118
+ * the same reason `SerializerDefault` is: it names the declaration, so the two
119
+ * legal values cannot be typo'd, and it composes into a store layer's own merge
120
+ * without a second import from `effect`.
121
+ */
122
+ export const DurableEventLog = /*#__PURE__*/Layer.succeed(EventLogDurability, 'durable');
123
+ /**
124
+ * Declare that the event log DIES WITH THE PROCESS — `core`'s
125
+ * `DcbEventStoreInMemory`, and any other log whose contents do not survive a
126
+ * restart.
127
+ *
128
+ * Under this declaration a DURABLE `ProjectionStore` is rejected by R2, because
129
+ * its checkpoint would outlive the log it points into. It is also the honest
130
+ * conservative choice over a log whose durability is genuinely unknown: it forgoes
131
+ * a durable view and nothing else.
132
+ */
133
+ export const EphemeralEventLog = /*#__PURE__*/Layer.succeed(EventLogDurability, 'ephemeral');
134
+ /**
135
+ * R2 itself: `undefined` when the two declarations are legally ordered, otherwise
136
+ * the sentence an author meets when they are not.
137
+ *
138
+ * The one illegal pairing of the four is a DURABLE view over an EPHEMERAL log. The
139
+ * asymmetry is deliberate and the module doc argues it: declaring `'ephemeral'`
140
+ * over a genuinely durable log costs nothing but a durable view, so only the unsafe
141
+ * direction is rejected.
142
+ *
143
+ * ## Why NAMED FIELDS rather than two positional arguments
144
+ *
145
+ * The two operands are the same unbranded `Durability` union, so a positional pair
146
+ * would typecheck transposed — and a transposed R2 check is not a broken check, it
147
+ * is an INVERTED one: it would accept the one pairing that silently freezes a view
148
+ * and reject the three that are fine, which is worse than having no check at all.
149
+ * Named fields make that transposition unwritable without visibly naming the wrong
150
+ * field, which is as much as a call-site convention can be asked to carry.
151
+ *
152
+ * It is as much as is WANTED here, too. Branding the two halves so the type system
153
+ * separated them was considered and rejected: `Durability` is a two-value union
154
+ * read straight off a `Layer` and a `ProjectionStore`, both of them public surfaces
155
+ * a caller writes by hand, so branding it would put a constructor between an
156
+ * application and `Layer.succeed(EventLogDurability, 'durable')` for a mistake the
157
+ * R2 case and its non-vacuity control already catch — that control provides
158
+ * `DurableEventLog` over the SAME durable view store and asserts the runner starts,
159
+ * which no mis-comparison in here can satisfy while still failing the rejection
160
+ * half.
161
+ *
162
+ * The sentence begins with `R2 violated` and states the rule, the silent failure it
163
+ * prevents, and BOTH fixes, because it is written for whoever meets it in a log with
164
+ * no file open. It names `EventLogDurability` in particular: the log's half of the
165
+ * comparison is this service, provided beside the `DcbEventStore` layer, so that is
166
+ * where the reader has to go — a read model carries no field to correct. It does not
167
+ * name the projection; the caller's preamble carries that, for every rule at once.
168
+ * Whether it also names a PARTITION is the caller's to decide: `runProjection` writes
169
+ * one, and `runProjections` deliberately writes none — not because its entries share a
170
+ * partition (each resolves its own, and a distinct one is the escape hatch for two
171
+ * materialisations cohabiting a store) but because a fault there is already attributed
172
+ * by entry INDEX, which discriminates where a partition may not.
173
+ */
174
+ export const durabilityOrderingFault = declarations => declarations.eventLogDurability === 'ephemeral' && declarations.viewDurability === 'durable' ? 'R2 violated — a DURABLE ProjectionStore over an EPHEMERAL event log. ' + 'The checkpoint would survive a restart the log does not, so on the next ' + 'boot subscribe(query, checkpoint) would return nothing against a ' + 're-emptied log and the view would sit FROZEN and permanently stale ' + 'without ever erroring. Pair an ephemeral log with an ephemeral view ' + "store, or make the log durable. The log's half of this comparison is " + 'the EventLogDurability service provided beside your DcbEventStore layer ' + '(EphemeralEventLog here); the view store declares the other half itself.' : undefined;
175
+ //# sourceMappingURL=EventLogDurability.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EventLogDurability.js","names":["Context","Layer","EventLogDurability","Tag","DurableEventLog","succeed","EphemeralEventLog","durabilityOrderingFault","declarations","eventLogDurability","viewDurability","undefined"],"sources":["../../src/EventLogDurability.ts"],"sourcesContent":[null],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsGA,SAASA,OAAO,EAAEC,KAAK,QAAQ,QAAQ;AAGvC;;;;;;;;AAQA,OAAM,MAAOC,kBAAmB,sBAAQF,OAAO,CAACG,GAAG,CACjD,oCAAoC,CACrC,EAAkC;AAEnC;;;;;;;;;AASA,OAAO,MAAMC,eAAe,gBAAoCH,KAAK,CAACI,OAAO,CAC3EH,kBAAkB,EAClB,SAAS,CACV;AAED;;;;;;;;;;AAUA,OAAO,MAAMI,iBAAiB,gBAAoCL,KAAK,CAACI,OAAO,CAC7EH,kBAAkB,EAClB,WAAW,CACZ;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,OAAO,MAAMK,uBAAuB,GAAIC,YAGvC,IACCA,YAAY,CAACC,kBAAkB,KAAK,WAAW,IAC/CD,YAAY,CAACE,cAAc,KAAK,SAAS,GACrC,uEAAuE,GACvE,0EAA0E,GAC1E,mEAAmE,GACnE,qEAAqE,GACrE,sEAAsE,GACtE,uEAAuE,GACvE,0EAA0E,GAC1E,0EAA0E,GAC1EC,SAAS","ignoreList":[]}