@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.
- package/LICENSE +28 -0
- package/README.md +538 -0
- package/dist/cjs/EventLogDurability.js +184 -0
- package/dist/cjs/EventLogDurability.js.map +1 -0
- package/dist/cjs/ProjectionRunner.js +478 -0
- package/dist/cjs/ProjectionRunner.js.map +1 -0
- package/dist/cjs/ProjectionStore.js +233 -0
- package/dist/cjs/ProjectionStore.js.map +1 -0
- package/dist/cjs/foldIntoRef.js +36 -0
- package/dist/cjs/foldIntoRef.js.map +1 -0
- package/dist/cjs/inMemoryProjectionStore.js +138 -0
- package/dist/cjs/inMemoryProjectionStore.js.map +1 -0
- package/dist/cjs/index.js +128 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/projectionWiringFault.js +532 -0
- package/dist/cjs/projectionWiringFault.js.map +1 -0
- package/dist/cjs/runProjection.js +117 -0
- package/dist/cjs/runProjection.js.map +1 -0
- package/dist/cjs/runProjections.js +144 -0
- package/dist/cjs/runProjections.js.map +1 -0
- package/dist/cjs/superviseOnProgress.js +580 -0
- package/dist/cjs/superviseOnProgress.js.map +1 -0
- package/dist/cjs/testing.js +143 -0
- package/dist/cjs/testing.js.map +1 -0
- package/dist/dts/EventLogDurability.d.ts +182 -0
- package/dist/dts/EventLogDurability.d.ts.map +1 -0
- package/dist/dts/ProjectionRunner.d.ts +557 -0
- package/dist/dts/ProjectionRunner.d.ts.map +1 -0
- package/dist/dts/ProjectionStore.d.ts +475 -0
- package/dist/dts/ProjectionStore.d.ts.map +1 -0
- package/dist/dts/foldIntoRef.d.ts +39 -0
- package/dist/dts/foldIntoRef.d.ts.map +1 -0
- package/dist/dts/inMemoryProjectionStore.d.ts +11 -0
- package/dist/dts/inMemoryProjectionStore.d.ts.map +1 -0
- package/dist/dts/index.d.ts +185 -0
- package/dist/dts/index.d.ts.map +1 -0
- package/dist/dts/projectionWiringFault.d.ts +260 -0
- package/dist/dts/projectionWiringFault.d.ts.map +1 -0
- package/dist/dts/runProjection.d.ts +185 -0
- package/dist/dts/runProjection.d.ts.map +1 -0
- package/dist/dts/runProjections.d.ts +480 -0
- package/dist/dts/runProjections.d.ts.map +1 -0
- package/dist/dts/superviseOnProgress.d.ts +587 -0
- package/dist/dts/superviseOnProgress.d.ts.map +1 -0
- package/dist/dts/testing.d.ts +207 -0
- package/dist/dts/testing.d.ts.map +1 -0
- package/dist/esm/EventLogDurability.js +175 -0
- package/dist/esm/EventLogDurability.js.map +1 -0
- package/dist/esm/ProjectionRunner.js +468 -0
- package/dist/esm/ProjectionRunner.js.map +1 -0
- package/dist/esm/ProjectionStore.js +223 -0
- package/dist/esm/ProjectionStore.js.map +1 -0
- package/dist/esm/foldIntoRef.js +29 -0
- package/dist/esm/foldIntoRef.js.map +1 -0
- package/dist/esm/inMemoryProjectionStore.js +131 -0
- package/dist/esm/inMemoryProjectionStore.js.map +1 -0
- package/dist/esm/index.js +185 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/package.json +4 -0
- package/dist/esm/projectionWiringFault.js +524 -0
- package/dist/esm/projectionWiringFault.js.map +1 -0
- package/dist/esm/runProjection.js +109 -0
- package/dist/esm/runProjection.js.map +1 -0
- package/dist/esm/runProjections.js +137 -0
- package/dist/esm/runProjections.js.map +1 -0
- package/dist/esm/superviseOnProgress.js +571 -0
- package/dist/esm/superviseOnProgress.js.map +1 -0
- package/dist/esm/testing.js +133 -0
- package/dist/esm/testing.js.map +1 -0
- package/package.json +41 -0
- package/src/EventLogDurability.ts +201 -0
- package/src/ProjectionRunner.ts +923 -0
- package/src/ProjectionStore.ts +528 -0
- package/src/foldIntoRef.ts +63 -0
- package/src/inMemoryProjectionStore.ts +163 -0
- package/src/index.ts +218 -0
- package/src/projectionWiringFault.ts +694 -0
- package/src/runProjection.ts +270 -0
- package/src/runProjections.ts +623 -0
- package/src/superviseOnProgress.ts +897 -0
- package/src/testing.ts +290 -0
- package/testing/package.json +6 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `runProjection` — the read side's SINGULAR entry point: one read model, one view
|
|
3
|
+
* store, one supervised daemon forked into the caller's `Scope`; plus
|
|
4
|
+
* `projectionLayer`, the same call shaped as a `Layer` so an application's graph owns
|
|
5
|
+
* that daemon's lifetime.
|
|
6
|
+
*
|
|
7
|
+
* ## Two entry points, one substrate
|
|
8
|
+
*
|
|
9
|
+
* This module and `runProjections.ts` are the read side's TWO entry points, and they
|
|
10
|
+
* are deliberately symmetrical one arity apart: N = 1 here, N >= 1 there. Neither
|
|
11
|
+
* builds a pipeline of its own and neither carries a rule of its own. Both run the
|
|
12
|
+
* three phases in `ProjectionRunner.ts` — `resolveProjection`, `prepareProjection`
|
|
13
|
+
* (which is where the construction gate `projectionWiringFault.ts` is consulted, for
|
|
14
|
+
* both of them, before either forks anything) and `buildAndFork` — so what differs
|
|
15
|
+
* between them is the shape of their INPUT and nothing else.
|
|
16
|
+
*
|
|
17
|
+
* Neither calls the other, and that follows from those input shapes rather than being
|
|
18
|
+
* an omission: a `ReadModel` carries a BOUND `KeyedProjectionStore`, a
|
|
19
|
+
* `Materialisation` an UNBOUND `ProjectionStore` plus the one `ProjectionId` the call
|
|
20
|
+
* names, and `forKey` deliberately does not expose what it closed over — so there is
|
|
21
|
+
* no route back from the first shape to the second. `runProjections.ts` owns that
|
|
22
|
+
* argument in full; this is the pointer to it, not a second home for it.
|
|
23
|
+
*
|
|
24
|
+
* ## Why a module of its own rather than a section of the substrate
|
|
25
|
+
*
|
|
26
|
+
* Because a file declaring the `ProjectionRunner` handle, the two option sets, the
|
|
27
|
+
* three shared phases AND one of the two entry points reads as though that entry
|
|
28
|
+
* point were the privileged one. It is not: it is the N = 1 case, and a read model
|
|
29
|
+
* that grows a second materialisation moves to the sibling without changing anything
|
|
30
|
+
* about how it is run. Keeping the substrate under the name of the type it declares,
|
|
31
|
+
* and each entry point in a file named for the function it exports, is what makes
|
|
32
|
+
* that symmetry visible from the directory listing.
|
|
33
|
+
*/
|
|
34
|
+
import type {
|
|
35
|
+
RequirementOf,
|
|
36
|
+
Serializer,
|
|
37
|
+
SliceProjection,
|
|
38
|
+
} from '@kairos-es/codec'
|
|
39
|
+
import type { DcbEventStore, DecodedEvent } from '@kairos-es/core'
|
|
40
|
+
import { type Array as Arr, Effect, Layer, type Scope } from 'effect'
|
|
41
|
+
import type { EventLogDurability } from './EventLogDurability'
|
|
42
|
+
import {
|
|
43
|
+
buildAndFork,
|
|
44
|
+
type ProjectionRunner,
|
|
45
|
+
type ProjectionRunnerOptions,
|
|
46
|
+
prepareProjection,
|
|
47
|
+
resolveProjection,
|
|
48
|
+
} from './ProjectionRunner'
|
|
49
|
+
import type { KeyedProjectionStore } from './ProjectionStore'
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* One read model: where it materialises (under which key), which slices drive it,
|
|
53
|
+
* and how a batch is applied.
|
|
54
|
+
*
|
|
55
|
+
* The slices are load-bearing twice over: their composed query is the
|
|
56
|
+
* subscription's query (so the runner reads exactly the events the folds want,
|
|
57
|
+
* with no separate query to keep in step), and their merged `type -> Schema` map
|
|
58
|
+
* is the decode lookup. There is no way to declare one without the other.
|
|
59
|
+
*
|
|
60
|
+
* The other half of R2 — the EVENT LOG's durability — is deliberately NOT a field
|
|
61
|
+
* here: it is declared once beside the store layer as the `EventLogDurability`
|
|
62
|
+
* service and read from context. `EventLogDurability.ts` argues why, and states
|
|
63
|
+
* the rule both halves feed.
|
|
64
|
+
*
|
|
65
|
+
* By R3, read models sharing one subscription share one cursor and therefore one
|
|
66
|
+
* `CheckpointKey`, one view store and one durability class — which is why a
|
|
67
|
+
* composed bundle is expressed as ONE `ReadModel` over several slices rather than
|
|
68
|
+
* several `ReadModel`s over one subscription.
|
|
69
|
+
*
|
|
70
|
+
* That bound is about sharing one SUBSCRIPTION, and the boundary is worth stating
|
|
71
|
+
* because the sentence above otherwise reads as one materialisation per read model.
|
|
72
|
+
* It says nothing about materialising ONE read model more than once: several
|
|
73
|
+
* materialisations of one read model deliberately take N subscriptions, N cursors
|
|
74
|
+
* and N checkpoints, exactly because R1 and R2 forbid them to share any of the
|
|
75
|
+
* three. Wire that with `runProjections`, which holds one `slices` value and one
|
|
76
|
+
* `ProjectionId` across N materialisations so their query and their decode cannot
|
|
77
|
+
* diverge, while each keeps its own store, its own `apply`, its own checkpoint and
|
|
78
|
+
* its own supervision hooks. What it does NOT hold is the folds, so agreement
|
|
79
|
+
* between the figures two materialisations arrive at stays empirical.
|
|
80
|
+
*/
|
|
81
|
+
export interface ReadModel<
|
|
82
|
+
T extends Record<string, SliceProjection>,
|
|
83
|
+
E = never,
|
|
84
|
+
R = never,
|
|
85
|
+
> {
|
|
86
|
+
/**
|
|
87
|
+
* The view store this read model materialises into, ALREADY BOUND to the
|
|
88
|
+
* checkpoint it advances (`forKey(store, key)`).
|
|
89
|
+
*
|
|
90
|
+
* A VALUE on the read model rather than a context tag, because the view store
|
|
91
|
+
* is selected per MATERIALISATION: one deployment can run an in-memory admin view
|
|
92
|
+
* and a durable production view against the same event log, and a slice can
|
|
93
|
+
* graduate from the first to the second with its `slices`, its composed query,
|
|
94
|
+
* its `CheckpointKey` and this runner untouched. What necessarily changes is
|
|
95
|
+
* this field and `apply` below — the two that name the view store — and nothing
|
|
96
|
+
* else.
|
|
97
|
+
*
|
|
98
|
+
* Graduation is SEQUENTIAL, memory and then Postgres, and it is not the only
|
|
99
|
+
* shape the selection permits: the same slice record can feed both AT ONCE, which
|
|
100
|
+
* is what `runProjections` wires. This field stays SINGULAR either way — one
|
|
101
|
+
* materialisation, one store, one key, one runner — and N materialisations are N
|
|
102
|
+
* of these values over one `slices` value, never one value holding a list. A list
|
|
103
|
+
* would imply one atomic `commit` across two stores, which R1 says is unsound;
|
|
104
|
+
* the demonstration of the simultaneous shape is
|
|
105
|
+
* `@kairos-es/read-postgres`'s `test/courseRosterGraduation.test.ts`, and the
|
|
106
|
+
* worked production wiring is `examples/course-subscriptions`'s
|
|
107
|
+
* `src/courseRosterMaterialisations.ts`.
|
|
108
|
+
*
|
|
109
|
+
* KEYED rather than a `(key, store)` pair, for the reason `KeyedProjectionStore`
|
|
110
|
+
* in `ProjectionStore.ts` argues — it owns why binding a key to a store turns
|
|
111
|
+
* "one materialisation, one key, one runner" from convention into structure. What
|
|
112
|
+
* is local to this field is that the key stays readable as `store.key`, which is
|
|
113
|
+
* where the runner's log annotations and failure payloads get it.
|
|
114
|
+
*/
|
|
115
|
+
readonly store: KeyedProjectionStore
|
|
116
|
+
|
|
117
|
+
/** The slices whose composed query drives the subscription and whose schemas decode. */
|
|
118
|
+
readonly slices: T
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The per-batch view writes, handed to `ProjectionStore.commit` so they land
|
|
122
|
+
* inside the same transaction as the checkpoint advance.
|
|
123
|
+
*
|
|
124
|
+
* The batch is non-empty by construction, so `apply` needs no empty case. If
|
|
125
|
+
* the store is NON-transactional this must be a SINGLE atomic effect (fold the
|
|
126
|
+
* batch purely, then one write — see `foldIntoRef`): the port cannot roll back
|
|
127
|
+
* a multi-write `apply` that fails part-way, and that requirement, not the
|
|
128
|
+
* store, is where the obligation sits.
|
|
129
|
+
*/
|
|
130
|
+
readonly apply: (
|
|
131
|
+
batch: Arr.NonEmptyReadonlyArray<DecodedEvent>,
|
|
132
|
+
) => Effect.Effect<void, E, R>
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Start the daemon maintaining `readModel`, forked into the caller's `Scope`.
|
|
137
|
+
*
|
|
138
|
+
* The returned effect is INFALLIBLE (`E = never`): starting a projection cannot
|
|
139
|
+
* fail, because everything that can go wrong once it is running is the
|
|
140
|
+
* supervisor's business and lives on the fibre's error channel.
|
|
141
|
+
*
|
|
142
|
+
* ## What can go wrong BEFORE the fork is a programming error, and those are defects
|
|
143
|
+
*
|
|
144
|
+
* Which wirings are refused on the READ SIDE'S rules, why each one is a wiring
|
|
145
|
+
* nothing at runtime would ever notice, and the order they are judged in are all
|
|
146
|
+
* `projectionWiringFault`'s — one gate over five rules, whose only relation to this
|
|
147
|
+
* function is that it is the one place they can be applied before anything exists to
|
|
148
|
+
* be wrong. The gate judges a SET of materialisations, and this function supplies a
|
|
149
|
+
* SINGLETON: the two PER-ENTRY rungs judge that one wiring, the two that are
|
|
150
|
+
* functions of the shared query and slice record judge it once and would do so
|
|
151
|
+
* whatever N was, and the collision rung is a property of the set and is vacuous over
|
|
152
|
+
* one entry, having no sibling to be paired with. That is also why nothing here carries
|
|
153
|
+
* an entry index — the gate names one only where there is a set to index into.
|
|
154
|
+
* A further refusal is the CODEC's and fires just below the gate; `prepareProjection`
|
|
155
|
+
* owns that ordering for both entry points, and the gate's doc says why the rule
|
|
156
|
+
* stays in the codec. This function keeps the two claims that are its own.
|
|
157
|
+
*
|
|
158
|
+
* The verdict is a DEFECT rather than a value on the error channel, because no
|
|
159
|
+
* application can handle its own mis-wiring: the only repair is to change the
|
|
160
|
+
* wiring, and a channel value would ask every caller to write a handler for a
|
|
161
|
+
* mistake that has already been made by the time the program runs. `Effect.die`
|
|
162
|
+
* with an `Error` is the same classification the store gives a mis-built query
|
|
163
|
+
* through `assertServableQuery`, which is one of the rules.
|
|
164
|
+
*
|
|
165
|
+
* And it lands on THIS effect before the `forkScoped` at the foot of `buildAndFork`:
|
|
166
|
+
* the gate is consulted while the daemon is still being
|
|
167
|
+
* assembled, so a rejected wiring has read no checkpoint, opened no subscription
|
|
168
|
+
* and left no half-started fibre behind. That is what makes the defect safe to
|
|
169
|
+
* raise rather than merely early, and every construction case in this package
|
|
170
|
+
* asserts the stored checkpoint is still `ORIGIN` afterwards to keep it true.
|
|
171
|
+
*
|
|
172
|
+
* The preamble on the message is written HERE, once, naming the projection and the
|
|
173
|
+
* partition — the gate returns the sentence about the RULE, and this function knows
|
|
174
|
+
* WHO broke it. The single `Effect.die` that joins them is `prepareProjection`'s,
|
|
175
|
+
* which is why there is one for both entry points rather than one per rule.
|
|
176
|
+
*/
|
|
177
|
+
export const runProjection = <T extends Record<string, SliceProjection>, E, R>(
|
|
178
|
+
readModel: ReadModel<T, E, R>,
|
|
179
|
+
options?: ProjectionRunnerOptions,
|
|
180
|
+
): Effect.Effect<
|
|
181
|
+
ProjectionRunner,
|
|
182
|
+
never,
|
|
183
|
+
| Scope.Scope
|
|
184
|
+
| DcbEventStore
|
|
185
|
+
| EventLogDurability
|
|
186
|
+
| Serializer
|
|
187
|
+
| RequirementOf<T>
|
|
188
|
+
| R
|
|
189
|
+
> =>
|
|
190
|
+
Effect.gen(function* () {
|
|
191
|
+
// ## RESOLVE — every value the gate has to judge, and every figure that runs
|
|
192
|
+
//
|
|
193
|
+
// `resolveProjection` is a pure function shared with `runProjections`, so the
|
|
194
|
+
// defaults, the key and the view's durability are read in ONE place for both
|
|
195
|
+
// callers: the figure that was judged cannot differ from the figure that runs.
|
|
196
|
+
const view = readModel.store
|
|
197
|
+
const resolved = resolveProjection(view, options)
|
|
198
|
+
|
|
199
|
+
// ## PREPARE — the log's durability, the merged query, the gate, the decode
|
|
200
|
+
//
|
|
201
|
+
// The shared middle, and the same call `runProjections` makes: it reads
|
|
202
|
+
// `EventLogDurability` from context, composes the query, consults the gate and
|
|
203
|
+
// only then builds the decode stage — an order `prepareProjection` owns on behalf
|
|
204
|
+
// of both entry points, and whose reasons are on it rather than restated here.
|
|
205
|
+
//
|
|
206
|
+
// The entry list is a SINGLETON, over which the collision rung has no pair to
|
|
207
|
+
// find. Its `cursorIdentity` is what that rung compares by reference — the BOUND
|
|
208
|
+
// view here, which `runProjections` could not use and does not, since a lone
|
|
209
|
+
// entry can only ever be compared with itself and the choice is therefore free at
|
|
210
|
+
// this arity.
|
|
211
|
+
//
|
|
212
|
+
// The preamble is this function's own half of the defect message and the only
|
|
213
|
+
// half it writes: the gate returns the sentence about the RULE, and this names
|
|
214
|
+
// the projection and the partition whose wiring broke it. No entry index, because
|
|
215
|
+
// there is no set to index into.
|
|
216
|
+
const prepared = yield* prepareProjection(
|
|
217
|
+
readModel.slices,
|
|
218
|
+
[{ ...resolved, cursorIdentity: view }],
|
|
219
|
+
`runProjection: invalid wiring for projection ` +
|
|
220
|
+
`'${resolved.key.projection}' (partition ` +
|
|
221
|
+
`'${resolved.key.partition}')`,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
// ## BUILD AND FORK — the pipeline, the read side's own rules now satisfied
|
|
225
|
+
return yield* buildAndFork(resolved, {
|
|
226
|
+
query: prepared.query,
|
|
227
|
+
decode: prepared.decode,
|
|
228
|
+
apply: readModel.apply,
|
|
229
|
+
options,
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* `runProjection` as a `Layer` whose own scope owns the daemon.
|
|
235
|
+
*
|
|
236
|
+
* It provides NOTHING (`ROut = never`): a projection is a background process, not
|
|
237
|
+
* a service, and nothing should be able to depend on it as one — a read model is
|
|
238
|
+
* queried through its view store, never through the runner. What the `Layer` buys
|
|
239
|
+
* is lifecycle: dropped into an application's layer graph, the daemon starts when
|
|
240
|
+
* the graph is built and is interrupted when it is released, alongside the store
|
|
241
|
+
* layers it reads from, with no bespoke startup or shutdown code.
|
|
242
|
+
*
|
|
243
|
+
* Discarding the runner discards its fibre too, which is correct and has one
|
|
244
|
+
* consequence worth wiring for: under this `Layer` — the recommended shape — a
|
|
245
|
+
* `ProjectionStalled` has no fibre to be awaited on, so it reaches the outside
|
|
246
|
+
* world only through the supervisor's `Effect.logError` and through `onStalled`.
|
|
247
|
+
* That hook is the reason a stall need not be a log line somebody happens to grep
|
|
248
|
+
* for; pass one in `options` if a stalled projection should fail a health check,
|
|
249
|
+
* exit non-zero, or page.
|
|
250
|
+
*
|
|
251
|
+
* `Layer.scopedDiscard` discharges the `Scope` the fork needs; the store, its
|
|
252
|
+
* `EventLogDurability` declaration, the `Serializer`, the slices' requirements and
|
|
253
|
+
* the read model's own `R` stay on the layer's inputs, so they are satisfied the
|
|
254
|
+
* same way every other layer's are. `EventLogDurability` being an INPUT is what
|
|
255
|
+
* makes the wiring read correctly: the declaration is provided once, alongside the
|
|
256
|
+
* `DcbEventStore` layer it describes, and every projection layer in the graph is
|
|
257
|
+
* then satisfied from that one declaration.
|
|
258
|
+
*/
|
|
259
|
+
export const projectionLayer = <
|
|
260
|
+
T extends Record<string, SliceProjection>,
|
|
261
|
+
E,
|
|
262
|
+
R,
|
|
263
|
+
>(
|
|
264
|
+
readModel: ReadModel<T, E, R>,
|
|
265
|
+
options?: ProjectionRunnerOptions,
|
|
266
|
+
): Layer.Layer<
|
|
267
|
+
never,
|
|
268
|
+
never,
|
|
269
|
+
DcbEventStore | EventLogDurability | Serializer | RequirementOf<T> | R
|
|
270
|
+
> => Layer.scopedDiscard(runProjection(readModel, options))
|