@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,480 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `runProjections` — one read model materialised N WAYS: one slice record and one
|
|
3
|
+
* `ProjectionId` for the whole call, one `Materialisation` per view store, and one
|
|
4
|
+
* `ProjectionRunner` forked for each.
|
|
5
|
+
*
|
|
6
|
+
* ## What it is for
|
|
7
|
+
*
|
|
8
|
+
* `ReadModel.store` records what a slice's GRADUATION from an in-memory view to
|
|
9
|
+
* Postgres does and does not change, and owns that claim. Per-read-model view-store
|
|
10
|
+
* selection is a FLOOR rather than a ceiling, though, and nothing in that claim says
|
|
11
|
+
* the two variants must be alternatives in TIME. A deployment can legitimately want
|
|
12
|
+
* both at once: a durable table somebody queries with SQL, beside an in-process view
|
|
13
|
+
* of the same slices holding one aggregate's hot figures with no round trip to reach
|
|
14
|
+
* them. Written as two `runProjection` calls that is two hand-built read models with
|
|
15
|
+
* the slice record named at both, and nothing placed to notice when the two copies
|
|
16
|
+
* drift apart.
|
|
17
|
+
*
|
|
18
|
+
* A hand-written pair also cannot get the REFUSAL the shape needs, and that is the
|
|
19
|
+
* half worth stating beside the sharing. Two materialisations bound to one store
|
|
20
|
+
* under one key would trade cursors, each advancing the other's; the gate's collision
|
|
21
|
+
* rung is what says so, and it is vacuous over the singleton each `runProjection`
|
|
22
|
+
* call hands it, having no sibling there to compare against. So the pairing is
|
|
23
|
+
* refused when the entries arrive together and unrefused when they are written
|
|
24
|
+
* separately, which is a property of the CALL rather than of any rule this module
|
|
25
|
+
* owns.
|
|
26
|
+
*
|
|
27
|
+
* ## The guarantee, at exactly its strength
|
|
28
|
+
*
|
|
29
|
+
* ONE QUERY, ONE DECODE, N APPLIES. The `slices` value is stated once for the call,
|
|
30
|
+
* and the two derivations from it are built ONCE for it — in `prepareProjection`,
|
|
31
|
+
* the phase `runProjection` shares — and handed to every entry: one
|
|
32
|
+
* `Query` object across the N subscriptions, one merged `type -> Schema` table across
|
|
33
|
+
* the N decode stages. There is no second place for either to be written
|
|
34
|
+
* differently, because there is no second place they are written at all — and now
|
|
35
|
+
* literally no second OBJECT either.
|
|
36
|
+
*
|
|
37
|
+
* It is NOT "N materialisations cannot drift", and nothing in this package should be
|
|
38
|
+
* worded that way. The FOLDS drift freely: a SQL `apply` that writes rows
|
|
39
|
+
* re-implements the fold rather than reusing `foldIntoRef`, so two materialisations
|
|
40
|
+
* of one slice record can disagree about what they made of the very same events.
|
|
41
|
+
* What is shared VERBATIM is the slices — the query and the decode schemas — and
|
|
42
|
+
* never the FOLD. Agreement between N materialised figures is therefore
|
|
43
|
+
* EMPIRICAL, something a test demonstrates by comparing the figures; it is not
|
|
44
|
+
* structural, and this function does not make it so.
|
|
45
|
+
*
|
|
46
|
+
* ## The guarantee is CONSTRUCTION-SCOPED, and that is enough
|
|
47
|
+
*
|
|
48
|
+
* This holds sharing at the moment of the CALL. Nothing afterwards carries it: each
|
|
49
|
+
* runner is an ordinary supervised pipeline from the fork onwards, and none of them
|
|
50
|
+
* can see its siblings. That is the same move `forKey` makes one level down — bind
|
|
51
|
+
* the pairing into the value every later operation flows through, so the incoherent
|
|
52
|
+
* thing is unwritable rather than merely unwritten — and it is sufficient here for a
|
|
53
|
+
* structural reason. The subscription's query and the decode table are both built
|
|
54
|
+
* FROM the slices at construction, in the one PREPARE phase, and are never rebuilt: a
|
|
55
|
+
* restart re-subscribes with the query it already holds. So there is no later moment at
|
|
56
|
+
* which one materialisation could come to hold a different query from its siblings,
|
|
57
|
+
* and no ongoing agreement for anything to police.
|
|
58
|
+
*
|
|
59
|
+
* ## Why a module of its own
|
|
60
|
+
*
|
|
61
|
+
* It composes the runner's three phases — `resolveProjection`, `prepareProjection`
|
|
62
|
+
* (which is where the construction gate is consulted) and `buildAndFork` — with
|
|
63
|
+
* `forKey`, and adds nothing to any of them: no pipeline
|
|
64
|
+
* stage, no supervision, no store operation, and no rule of its own at all, the
|
|
65
|
+
* collision being a rung of the one gate both entry points consult.
|
|
66
|
+
* The precedent for taking a whole job out of `ProjectionRunner.ts` is that module's
|
|
67
|
+
* own history, three times over: `superviseOnProgress.ts` and
|
|
68
|
+
* `projectionWiringFault.ts` were both extracted from it and each became testable on
|
|
69
|
+
* its own terms by being, and `runProjection.ts` — the SINGULAR entry point, this
|
|
70
|
+
* module's opposite number — was split off it in turn, leaving that file the shared
|
|
71
|
+
* pipeline substrate both entry points sit on. No count of lines is quoted here on
|
|
72
|
+
* purpose: the argument is that the file accretes JOBS, which is true of it at any
|
|
73
|
+
* length.
|
|
74
|
+
*
|
|
75
|
+
* ## Why there is no `projectionsLayer`
|
|
76
|
+
*
|
|
77
|
+
* `projectionLayer` is `Layer.scopedDiscard(runProjection(readModel, options))` and
|
|
78
|
+
* nothing else, so the layer shape for N materialisations is
|
|
79
|
+
* `Layer.scopedDiscard(runProjections(spec))` — which discards the N runners exactly
|
|
80
|
+
* as `projectionLayer` discards the one, and for the same reason (a projection is a
|
|
81
|
+
* background process, not a service). A second one-line wrapper would be a second
|
|
82
|
+
* home for one line, and the argument `projectionLayer`'s doc makes — that a `Layer`
|
|
83
|
+
* is how an application's graph owns a daemon's lifetime, with no bespoke startup or
|
|
84
|
+
* shutdown code — is already written down once. The one consequence worth carrying
|
|
85
|
+
* across is `projectionLayer`'s too: a discarded runner has no fibre to await, so a
|
|
86
|
+
* stall reaches the outside world only through the supervisor's `logError` and
|
|
87
|
+
* through `onStalled`, which under this function is a hook per materialisation.
|
|
88
|
+
*/
|
|
89
|
+
import type { RequirementOf, Serializer, SliceProjection } from '@kairos-es/codec';
|
|
90
|
+
import type { DcbEventStore, DecodedEvent } from '@kairos-es/core';
|
|
91
|
+
import { Array as Arr, Effect, type Scope } from 'effect';
|
|
92
|
+
import type { EventLogDurability } from './EventLogDurability.js';
|
|
93
|
+
import { type ProjectionRunner, type ProjectionRunnerOptions } from './ProjectionRunner.js';
|
|
94
|
+
import { type PartitionId, type ProjectionId, type ProjectionStore } from './ProjectionStore.js';
|
|
95
|
+
/**
|
|
96
|
+
* ONE way a read model is materialised: where its view lives, how a batch is
|
|
97
|
+
* written into it, and how its own runner is tuned.
|
|
98
|
+
*
|
|
99
|
+
* It is a `ReadModel` minus the two things `runProjections` supplies for the whole
|
|
100
|
+
* call — the slices, and the `forKey` binding — which is why the store here is the
|
|
101
|
+
* UNBOUND multi-key port rather than a `KeyedProjectionStore`.
|
|
102
|
+
*
|
|
103
|
+
* ## Why BOTH parameters survived the tuple fix, `E` included
|
|
104
|
+
*
|
|
105
|
+
* `E` was considered for removal and deliberately KEPT, so a reader meeting it here
|
|
106
|
+
* is not meeting an oversight. The case for dropping it was that one `E` shared
|
|
107
|
+
* across N entries fails to reconcile — but that was the SAME defect as `R`'s, and
|
|
108
|
+
* `MaterialisedProjection`'s tuple parameter fixes it for both by one mechanism:
|
|
109
|
+
* two entries with two distinct error classes now infer `ErrA | ErrB`. Removing the
|
|
110
|
+
* parameter would therefore delete a working, CHECKED type to route around a bug
|
|
111
|
+
* that is already fixed.
|
|
112
|
+
*
|
|
113
|
+
* Two things would have gone with it. The `ReadModel` correspondence this doc opens
|
|
114
|
+
* with: `ReadModel<T, E, R>` keeps its `E`, and typing an `apply`'s error as
|
|
115
|
+
* `unknown` here would mean the two were no longer the same shape — which
|
|
116
|
+
* `examples/course-subscriptions/src/courseRosterSql.ts` would be the first to
|
|
117
|
+
* notice, one value there serving as both. And a check a caller gets for free:
|
|
118
|
+
* `Materialisation<never>` states that an `apply` CANNOT fail and is verified,
|
|
119
|
+
* `Materialisation<SqlError>` that this one fails only that way. Under `unknown`
|
|
120
|
+
* both become unwritable and every `apply` typechecks, including one whose author
|
|
121
|
+
* got the error type wrong.
|
|
122
|
+
*
|
|
123
|
+
* The narrower version of that insight is real and IS taken, one level up:
|
|
124
|
+
* `MaterialisationAny` bounds BOTH positions with `unknown`, and loses nothing by
|
|
125
|
+
* it, because a CONSTRAINT is not a type anything declares a value at. Wherever a
|
|
126
|
+
* caller DOES declare one — a value typed `Materialisation<SqlError>`, or a domain
|
|
127
|
+
* interface of its own parameterised by that error, which is the shape
|
|
128
|
+
* `examples/course-subscriptions` takes — the check above is still theirs. The bound
|
|
129
|
+
* only has to admit the result.
|
|
130
|
+
*/
|
|
131
|
+
export interface Materialisation<E = never, R = never> {
|
|
132
|
+
/**
|
|
133
|
+
* The multi-key view store this materialisation writes into, UNBOUND.
|
|
134
|
+
*
|
|
135
|
+
* Unbound because `runProjections` does the binding, from the one `ProjectionId`
|
|
136
|
+
* the call names and this entry's own `partition`. `MaterialisedProjection`'s own
|
|
137
|
+
* doc argues why that direction is load-bearing rather than merely convenient.
|
|
138
|
+
*/
|
|
139
|
+
readonly store: ProjectionStore;
|
|
140
|
+
/**
|
|
141
|
+
* THIS materialisation's per-batch view writes, handed to its own
|
|
142
|
+
* `ProjectionStore.commit` so they land inside the same transaction as its own
|
|
143
|
+
* checkpoint advance.
|
|
144
|
+
*
|
|
145
|
+
* Every obligation `ReadModel.apply` carries applies here unchanged and per
|
|
146
|
+
* entry: the batch is non-empty by construction, and a NON-TRANSACTIONAL store's
|
|
147
|
+
* `apply` must be a SINGLE atomic effect (fold the batch purely, then one write —
|
|
148
|
+
* `foldIntoRef`), because the port cannot roll back a multi-write `apply` that
|
|
149
|
+
* fails part-way. N materialisations means N independent answers to that: an
|
|
150
|
+
* in-memory entry beside a SQL one owes the requirement, and the SQL one does not
|
|
151
|
+
* discharge it on its behalf.
|
|
152
|
+
*
|
|
153
|
+
* ## ONE MATERIALISATION OWNS ONE VIEW TARGET, and the gate cannot check it
|
|
154
|
+
*
|
|
155
|
+
* Two entries whose `apply`s write the SAME view — the same `Ref`, the same row,
|
|
156
|
+
* the same table — are a defect, and one nothing here refuses. It is the exact
|
|
157
|
+
* mirror of the collision the gate DOES catch: that one is two entries sharing a
|
|
158
|
+
* CURSOR, this one is two entries sharing a TARGET. Both end with the view wrong
|
|
159
|
+
* indefinitely and nothing on any error channel.
|
|
160
|
+
*
|
|
161
|
+
* It is undetectable rather than merely unchecked, and the reason is structural:
|
|
162
|
+
* the view lives INSIDE this closure, so the library sees an opaque effect and has
|
|
163
|
+
* nothing to compare. Where the collision rung has two store references and two
|
|
164
|
+
* resolved keys to hold up against each other, here it has two functions.
|
|
165
|
+
*
|
|
166
|
+
* The failure is worth recognising because it looks nothing like a race. Both
|
|
167
|
+
* runners are sound single writers of their OWN cursors, each committing exactly
|
|
168
|
+
* once per batch; they simply both apply every event to one target, so the figures
|
|
169
|
+
* come out doubled (or last-write-wins, for a `set` rather than an increment)
|
|
170
|
+
* while both checkpoints advance cleanly to head. A caller reaching for a second
|
|
171
|
+
* materialisation almost always wants a second VIEW to go with the second store —
|
|
172
|
+
* and if the two views really are meant to be one, that is ONE materialisation
|
|
173
|
+
* with one `apply`, which is the same answer the collision rung's message gives.
|
|
174
|
+
*/
|
|
175
|
+
readonly apply: (batch: Arr.NonEmptyReadonlyArray<DecodedEvent>) => Effect.Effect<void, E, R>;
|
|
176
|
+
/**
|
|
177
|
+
* This materialisation's checkpoint PARTITION. Defaults to `DEFAULT_PARTITION`,
|
|
178
|
+
* which is what nearly every call wants.
|
|
179
|
+
*
|
|
180
|
+
* Present for the one shape that needs it: two materialisations that genuinely
|
|
181
|
+
* must COHABIT one `ProjectionStore`. Under one `ProjectionId` and the default
|
|
182
|
+
* partition their `CheckpointKey`s would be identical, so they would be two
|
|
183
|
+
* runners over ONE cursor, racing the same guarded advance on every batch — which
|
|
184
|
+
* `materialisationCollisionFault` refuses at construction, and whose sentence
|
|
185
|
+
* names this field as the escape hatch. A distinct `PartitionId` is a distinct
|
|
186
|
+
* `CheckpointKey` and therefore a cursor of its own.
|
|
187
|
+
*
|
|
188
|
+
* It is NOT sharding: nothing here splits one materialisation's stream, which
|
|
189
|
+
* ADR-0007 rules out and `PartitionId`'s own doc explains. Each partition still
|
|
190
|
+
* has exactly one runner over the whole composed query.
|
|
191
|
+
*/
|
|
192
|
+
readonly partition?: PartitionId;
|
|
193
|
+
/**
|
|
194
|
+
* This materialisation's own pipeline tuning and supervision hooks — the same
|
|
195
|
+
* object `runProjection` takes, per entry.
|
|
196
|
+
*
|
|
197
|
+
* ## Why PER ENTRY, and why there is no call-level options object
|
|
198
|
+
*
|
|
199
|
+
* The figures are genuinely per materialisation: a SQL `apply` and a `Ref` update
|
|
200
|
+
* want different batch sizes and different latency ceilings, and there is nothing
|
|
201
|
+
* about "one read model" that makes one transaction size right for both.
|
|
202
|
+
*
|
|
203
|
+
* The observability halves settle it. Each materialisation's checkpoint IS its
|
|
204
|
+
* progress signal, so `onRestart` and `onStalled` are about ONE of them: a restart
|
|
205
|
+
* is a rate to graph and a stall needs a human, and an operator has to be able to
|
|
206
|
+
* attribute either — collapsing N signals into one pair of hooks would report that
|
|
207
|
+
* something restarted while saying nothing about WHAT. There is a sharp
|
|
208
|
+
* consequence worth stating because it surprises: under one `ProjectionId` and the
|
|
209
|
+
* default partition the N `CheckpointKey`s are IDENTICAL — the intended shape, a
|
|
210
|
+
* key naming a cursor WITHIN a store — so `info.key` on the hooks does not
|
|
211
|
+
* discriminate the materialisations. For SUPERVISION the discriminator is WHICH
|
|
212
|
+
* ENTRY'S HOOK FIRED, which is exactly what a per-entry options object gives and a
|
|
213
|
+
* call-level one would take away.
|
|
214
|
+
*
|
|
215
|
+
* That argument is about the hooks specifically and does not generalise to every
|
|
216
|
+
* observer. A hook is handed a payload rather than a handle, so the discriminator
|
|
217
|
+
* a RETURNED runner has — `runners[i].store`, the bound cursor it maintains — is
|
|
218
|
+
* not in reach from inside one. An operator attributing a stall therefore closes
|
|
219
|
+
* over the entry, while a caller reading how far each materialisation has got asks
|
|
220
|
+
* its runner.
|
|
221
|
+
*
|
|
222
|
+
* A call-level object would also be a second home for every figure in
|
|
223
|
+
* `ProjectionRunnerOptions`, free to disagree with the per-entry one, with a merge
|
|
224
|
+
* order to document and get wrong. Sharing a tuning across entries is a `const`
|
|
225
|
+
* the caller writes once and names twice, which needs no library support.
|
|
226
|
+
*/
|
|
227
|
+
readonly options?: ProjectionRunnerOptions;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Any materialisation at all, whatever its `apply` fails with and whatever it
|
|
231
|
+
* needs — the CONSTRAINT on the tuple below, and not a type anything declares a
|
|
232
|
+
* value at.
|
|
233
|
+
*
|
|
234
|
+
* Both positions are `unknown` — the widest bound that is not `any`, so the tuple
|
|
235
|
+
* admits every materialisation while erasing nothing a reader could mistake for a
|
|
236
|
+
* check. The `R` half of that is what makes the body's ONE assertion MANDATORY: a
|
|
237
|
+
* wildcard bound would make it optional, and an optional assertion is one nobody
|
|
238
|
+
* writes.
|
|
239
|
+
*
|
|
240
|
+
* Under this bound an element of `Ms` is known only to be a
|
|
241
|
+
* `Materialisation<unknown, unknown>`, so the requirement `Effect.forEach` would
|
|
242
|
+
* infer from `entry.apply` is `unknown`, which satisfies no declared requirement:
|
|
243
|
+
* the module does not compile until the one `buildAndFork` call says what the
|
|
244
|
+
* entries actually need. It says it once and narrowly — on `entry.apply` alone, to
|
|
245
|
+
* exactly `MaterialisationContext<Ms>`, the alias the return type already names —
|
|
246
|
+
* and everything downstream is then inferred from that alias rather than from a
|
|
247
|
+
* wildcard, so the WHOLE requirement channel of the declared return type is CHECKED.
|
|
248
|
+
* Deleting `Serializer`, `DcbEventStore` or `MaterialisationContext<Ms>` itself from
|
|
249
|
+
* that union is a compile error, as is deleting the assertion; verified by mutation
|
|
250
|
+
* rather than reasoned about, all four.
|
|
251
|
+
*
|
|
252
|
+
* `any` in the `R` position also compiles, and compiles with no assertion at all,
|
|
253
|
+
* which is what makes it the tempting shape. It is the wrong trade and not a small
|
|
254
|
+
* one: `X | any` normalises to `any`, so the body's requirement erases ENTIRELY and
|
|
255
|
+
* every member of that union could be deleted — or a service the body never needs
|
|
256
|
+
* added — with the module still green. An `as` on one line is the narrow, visible
|
|
257
|
+
* form of the trade `@kairos-es/codec`'s `makeRegistry` makes with its single cast;
|
|
258
|
+
* a wildcard bound is the wide, silent form of the same thing, and the absence of an
|
|
259
|
+
* `as` is not the absence of an assertion.
|
|
260
|
+
*
|
|
261
|
+
* The `E` position is `unknown` for a duller reason and needs no assertion of its
|
|
262
|
+
* own: `E` never reaches this function's return type, `runProjections` being
|
|
263
|
+
* infallible, so no error an entry declares has anywhere to go but its supervisor.
|
|
264
|
+
*/
|
|
265
|
+
type MaterialisationAny = Materialisation<unknown, unknown>;
|
|
266
|
+
/**
|
|
267
|
+
* The union of requirements `R` carried by a TUPLE of materialisations — what makes
|
|
268
|
+
* two entries needing two different services compile with nothing annotated.
|
|
269
|
+
*
|
|
270
|
+
* ## Why a tuple parameter, and what the shape it replaced could not do
|
|
271
|
+
*
|
|
272
|
+
* The obvious signature is one `Materialisation<E, R>` for every entry, and it is
|
|
273
|
+
* WRONG in a way that only shows up on the very shape this function exists for.
|
|
274
|
+
* With one `R` shared across N entries, TypeScript collects a candidate from each
|
|
275
|
+
* element and then checks the rest against the first, so two entries needing two
|
|
276
|
+
* different services do not union — the second is reported as not assignable to the
|
|
277
|
+
* first, and the caller's only recourse is to compute the union by hand and annotate
|
|
278
|
+
* it. Heterogeneous view stores is the whole point of this function, and under that
|
|
279
|
+
* signature the heterogeneous case was the one that did not work.
|
|
280
|
+
*
|
|
281
|
+
* A tuple parameter has no single `R` to reconcile. `Ms` is inferred PER ELEMENT, so
|
|
282
|
+
* no candidate has to lose, and this alias then reads the union back off the
|
|
283
|
+
* inferred tuple, and is also the body's one assertion target, for the reason
|
|
284
|
+
* `MaterialisationAny` gives. It is the mechanism `Effect.all` uses for exactly this
|
|
285
|
+
* problem —
|
|
286
|
+
* `All.ReturnTuple` indexes its tuple with `T[number]` and infers through the
|
|
287
|
+
* variance struct, and `effect`'s own dtslint suite pins the result
|
|
288
|
+
* (`Effect.all([string, number])` is
|
|
289
|
+
* `Effect<[string, number], "err-1" | "err-2", "dep-1" | "dep-2">`).
|
|
290
|
+
*
|
|
291
|
+
* ## Why the brackets, when they do nothing here
|
|
292
|
+
*
|
|
293
|
+
* They are the idiom `Effect.Context` is written in and the form that stays correct
|
|
294
|
+
* under the one edit a later reader is most likely to make. They are NOT what
|
|
295
|
+
* produces the union, and this doc says so plainly because the alternative is a
|
|
296
|
+
* reader concluding they are magic and preserving them while "simplifying" the tuple
|
|
297
|
+
* parameter away — which is the change that actually breaks it.
|
|
298
|
+
*
|
|
299
|
+
* Distribution happens only when the checked type is a NAKED type parameter, and
|
|
300
|
+
* `Ms[number]` is an indexed access, so the conditional here does not distribute and
|
|
301
|
+
* the brackets suppress nothing. Both forms were compiled side by side over a
|
|
302
|
+
* two-entry tuple, an array of a union and the empty tuple, and agreed on all three.
|
|
303
|
+
* Should the checked type ever become a bare parameter, the brackets are what keeps
|
|
304
|
+
* the answer the same.
|
|
305
|
+
*
|
|
306
|
+
* `R` is COVARIANT in Effect v3 (`Effect<out A, out E, out R>`, and `_R:
|
|
307
|
+
* Covariant<R>` on the variance struct), which is why inferring across the elements
|
|
308
|
+
* yields a UNION and not an intersection.
|
|
309
|
+
*/
|
|
310
|
+
type MaterialisationContext<Ms extends Arr.NonEmptyReadonlyArray<MaterialisationAny>> = [Ms[number]] extends [Materialisation<infer _E, infer R>] ? R : never;
|
|
311
|
+
/**
|
|
312
|
+
* One read model — its slices and its identity — plus the N ways it is materialised:
|
|
313
|
+
* the whole input of `runProjections`.
|
|
314
|
+
*
|
|
315
|
+
* ## Why the UNBOUND store per entry plus ONE `ProjectionId` for the call
|
|
316
|
+
*
|
|
317
|
+
* The alternative shape is N pre-bound `KeyedProjectionStore`s, and it was rejected
|
|
318
|
+
* for three reasons that are each load-bearing.
|
|
319
|
+
*
|
|
320
|
+
* It makes the PRIMARY GUARANTEE structural. One `slices` field cannot become two,
|
|
321
|
+
* so "one query, one decode" is a property of the type rather than of a caller's
|
|
322
|
+
* discipline — which is the whole point of the function, and would be given away by
|
|
323
|
+
* a shape that took N read models.
|
|
324
|
+
*
|
|
325
|
+
* It makes the KEY POLICY structural too. One `ProjectionId` for the call is what
|
|
326
|
+
* "one read model, N materialisations, ONE identity" MEANS, and it is the honest
|
|
327
|
+
* reading of a `CheckpointKey`: the key is a lookup identifier WITHIN one store, so
|
|
328
|
+
* N stores holding the same key are N materialisations of one read model, not a
|
|
329
|
+
* collision. N pre-bound stores would leave every caller free to fragment one read
|
|
330
|
+
* model's identity across N ids — nothing would break, and nothing would ever say
|
|
331
|
+
* so, until somebody went looking for one projection's cursors and found three
|
|
332
|
+
* names.
|
|
333
|
+
*
|
|
334
|
+
* And it is the only shape in which the collision check is POSSIBLE at all. `forKey`
|
|
335
|
+
* returns a fresh object literal that deliberately does not expose the store it
|
|
336
|
+
* closed over (so that a `{ ...inner, commit: … }` decorator decorates), so nothing
|
|
337
|
+
* downstream of a binding can compare two bound stores for store IDENTITY. The
|
|
338
|
+
* unbound port is the last point at which "these two entries are the same store" is
|
|
339
|
+
* a question anything can ask.
|
|
340
|
+
*
|
|
341
|
+
* All three are the same move as `forKey` itself, one level up: bind the pairing
|
|
342
|
+
* into the value every later operation flows through, so a wiring cannot express the
|
|
343
|
+
* incoherent thing.
|
|
344
|
+
*
|
|
345
|
+
* ## Why the materialisations are a NON-EMPTY array
|
|
346
|
+
*
|
|
347
|
+
* An empty list is a projection nobody maintains, forked silently — no runner, no
|
|
348
|
+
* checkpoint, no view, and no error either. The type refuses it a stack frame from
|
|
349
|
+
* where it was written, which is both earlier and cheaper than a rung of the
|
|
350
|
+
* construction gate. It is also why the return type is non-empty: the runners come
|
|
351
|
+
* back one per entry, in entry order.
|
|
352
|
+
*
|
|
353
|
+
* ## Why the materialisations are a TUPLE parameter and not `Arr.NonEmptyReadonlyArray<Materialisation<E, R>>`
|
|
354
|
+
*
|
|
355
|
+
* So that N entries needing N different services compile with nothing annotated.
|
|
356
|
+
* `MaterialisationContext` above owns that argument in full; what it means HERE is
|
|
357
|
+
* that the second parameter is the entries' own inferred tuple rather than a shared
|
|
358
|
+
* `E`/`R` pair, and that `Ms` is normally inferred and never written. Its default is
|
|
359
|
+
* what keeps the one-argument spelling — `MaterialisedProjection<Slices>` — reading
|
|
360
|
+
* as it always did, for the callers that only ever name the slices.
|
|
361
|
+
*
|
|
362
|
+
* ## What the tuple parameter COSTS: one assertion, on one line, at one call site
|
|
363
|
+
*
|
|
364
|
+
* It is stronger for CALLERS and costs the body exactly one `as`. Under the shape it
|
|
365
|
+
* replaced, the body's requirement came straight off `entry.apply`, which was
|
|
366
|
+
* `Effect<void, E, R>`. Under a tuple parameter an element is known only by its
|
|
367
|
+
* CONSTRAINT, so `entry.apply` reads as `Effect<void, unknown, unknown>` inside the
|
|
368
|
+
* body and the assembled effect satisfies no declared requirement until the
|
|
369
|
+
* `buildAndFork` call names one. `MaterialisationAny`'s doc owns that mechanic and
|
|
370
|
+
* the reason the bound is `unknown` rather than a wildcard; what it means HERE is
|
|
371
|
+
* the ONE thing the compiler is asked to take on trust.
|
|
372
|
+
*
|
|
373
|
+
* That one thing is narrow and worth stating exactly: an entry's `apply` requires no
|
|
374
|
+
* more than the union `MaterialisationContext<Ms>` reads back off the very tuple the
|
|
375
|
+
* entry came from. It is true by construction — the alias IS that union and the
|
|
376
|
+
* entry IS an element of that tuple — but TypeScript cannot verify it for an
|
|
377
|
+
* unresolved generic `Ms`, the alias being a deferred conditional over `Ms[number]`.
|
|
378
|
+
* Assert something wider there and callers would be asked for services no entry
|
|
379
|
+
* needs; assert `never` and they would be asked for nothing, and a genuinely missing
|
|
380
|
+
* service would surface as a runtime defect instead of a type error. Which is why
|
|
381
|
+
* the alias is written in exactly two places, the assertion and the return type, and
|
|
382
|
+
* they are the same expression.
|
|
383
|
+
*
|
|
384
|
+
* Everything downstream of that line is CHECKED, which is the whole return on
|
|
385
|
+
* keeping it narrow: the declared requirement channel is verified member by member,
|
|
386
|
+
* and the assertion itself cannot be quietly deleted by a later reader who finds it
|
|
387
|
+
* decorative. `MaterialisationAny` names the mutations that demonstrate both. The
|
|
388
|
+
* value channel is checked too — the runners come back one per entry and the
|
|
389
|
+
* non-emptiness is real.
|
|
390
|
+
*
|
|
391
|
+
* ## Why the runners come back as an ARRAY and not as a mapped TUPLE
|
|
392
|
+
*
|
|
393
|
+
* `{ readonly [K in keyof Ms]: ProjectionRunner }` compiles, and it makes a
|
|
394
|
+
* two-entry call hand back a `readonly [ProjectionRunner, ProjectionRunner]`. It was
|
|
395
|
+
* measured and declined. `Arr.With` — the type both `Arr.map` and `Effect.forEach`
|
|
396
|
+
* map their input through, `S extends NonEmptyReadonlyArray<any> ? NonEmptyArray<A>
|
|
397
|
+
* : Array<A>` in Effect v3 — has dropped the ARITY by the first of those two steps
|
|
398
|
+
* and nothing recovers it, so the mapped tuple needs a SECOND assertion, on the
|
|
399
|
+
* `Effect.forEach` result, and that one converts the value channel above from
|
|
400
|
+
* checked into asserted.
|
|
401
|
+
*
|
|
402
|
+
* What it buys back is arity and only arity. Every element is a `ProjectionRunner`,
|
|
403
|
+
* so it cannot express the pairing `runners[i]` ↔ `materialisations[i]` that the
|
|
404
|
+
* entry ORDER carries and that this module's prose states; and with
|
|
405
|
+
* `noUncheckedIndexedAccess` off, as it is across this repo, no call site reads
|
|
406
|
+
* differently either way. Trading a check for an assertion to type something no
|
|
407
|
+
* caller can use is the wrong direction.
|
|
408
|
+
*/
|
|
409
|
+
export interface MaterialisedProjection<T extends Record<string, SliceProjection>, Ms extends Arr.NonEmptyReadonlyArray<MaterialisationAny> = Arr.NonEmptyReadonlyArray<Materialisation>> {
|
|
410
|
+
/**
|
|
411
|
+
* The slices every materialisation shares: their composed query is the ONE
|
|
412
|
+
* subscription query and their merged schema table is the ONE decode lookup.
|
|
413
|
+
*
|
|
414
|
+
* Stated once for the call, which is the guarantee — see the module doc for what
|
|
415
|
+
* that guarantee does and does not extend to.
|
|
416
|
+
*/
|
|
417
|
+
readonly slices: T;
|
|
418
|
+
/**
|
|
419
|
+
* The read model's identity, shared by every materialisation.
|
|
420
|
+
*
|
|
421
|
+
* One name for one read model however many ways it is materialised, because a
|
|
422
|
+
* `CheckpointKey` identifies a cursor WITHIN a store: equal keys across two
|
|
423
|
+
* different stores are the intended shape of a graduated slice, not a clash.
|
|
424
|
+
*/
|
|
425
|
+
readonly projection: ProjectionId;
|
|
426
|
+
/** The N ways it is materialised, at least one. */
|
|
427
|
+
readonly materialisations: Ms;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Start one daemon per materialisation, all forked into the caller's `Scope`.
|
|
431
|
+
*
|
|
432
|
+
* The returned effect is INFALLIBLE for the reason `runProjection`'s is: starting a
|
|
433
|
+
* projection cannot fail, because everything that can go wrong once one is running
|
|
434
|
+
* belongs to its own supervisor and lives on its own fibre's error channel. The
|
|
435
|
+
* runners come back in ENTRY ORDER, so `runners[i]` is `materialisations[i]`'s.
|
|
436
|
+
*
|
|
437
|
+
* Each runner carries the store THIS call bound for its entry, so `runners[i].store`
|
|
438
|
+
* is that materialisation's cursor and a caller observing one re-derives no binding
|
|
439
|
+
* it never wrote. It is also what tells the N handles apart: under one `ProjectionId`
|
|
440
|
+
* the keys are equal wherever no entry named a partition, so a runner carrying only a
|
|
441
|
+
* key could not name which of the N views it maintains.
|
|
442
|
+
*
|
|
443
|
+
* ## N = 1 is legal and carries nothing inert
|
|
444
|
+
*
|
|
445
|
+
* One entry is `runProjection` with the `forKey` binding done for you, and every
|
|
446
|
+
* field means the same thing it means at N = 3. The two run the identical phases —
|
|
447
|
+
* one `resolveProjection`, one `prepareProjection`, one `buildAndFork` — rather than
|
|
448
|
+
* merely offering the same surface, so there is no threshold at which a caller should
|
|
449
|
+
* switch functions: a read model that grows a second materialisation adds an entry.
|
|
450
|
+
*
|
|
451
|
+
* ## Every rule is judged before ANY entry is forked
|
|
452
|
+
*
|
|
453
|
+
* The construction gate takes the whole call — the shared log, query and slice
|
|
454
|
+
* record, plus the N resolved wirings — and this call reaches it ONCE, through
|
|
455
|
+
* `prepareProjection`, above the `Effect.forEach` below. So a call refused for ANY
|
|
456
|
+
* reason has forked NOTHING,
|
|
457
|
+
* not even the entries that were sound: not the collision, which is a property of
|
|
458
|
+
* the set, and not a degenerate `batchWindow` or an R2 violation on the last entry
|
|
459
|
+
* either. Half a call running is a worse state than none of it, and the pairing of N
|
|
460
|
+
* materialisations is one wiring decision.
|
|
461
|
+
*
|
|
462
|
+
* Two consequences of the gate judging a SET are worth stating rather than leaving
|
|
463
|
+
* to be met.
|
|
464
|
+
*
|
|
465
|
+
* A per-entry rung names its ENTRY. The gate prefixes its verdict with
|
|
466
|
+
* `materialisations[i]` where there is more than one entry, in the vocabulary the
|
|
467
|
+
* collision sentence already uses; this function's preamble names the PROJECTION and
|
|
468
|
+
* no partition, because under one `ProjectionId` the N keys are equal wherever no
|
|
469
|
+
* entry named a partition, so a partition never discriminated them.
|
|
470
|
+
*
|
|
471
|
+
* And the order is over RULES, not over entries. Where two entries break two
|
|
472
|
+
* DIFFERENT rules the earlier RULE wins whatever entry it is in — entry 0's
|
|
473
|
+
* degenerate `batchWindow` beside entry 1's illegal durability pairing reports entry
|
|
474
|
+
* 1's R2 fault, because R2 is the wider mistake wherever it sits. The gate's module
|
|
475
|
+
* doc argues that, and it is why the rules are judged rule-major rather than by
|
|
476
|
+
* walking the entries.
|
|
477
|
+
*/
|
|
478
|
+
export declare const runProjections: <T extends Record<string, SliceProjection>, Ms extends Arr.NonEmptyReadonlyArray<MaterialisationAny>>(spec: MaterialisedProjection<T, Ms>) => Effect.Effect<Arr.NonEmptyReadonlyArray<ProjectionRunner>, never, Scope.Scope | DcbEventStore | EventLogDurability | Serializer | RequirementOf<T> | MaterialisationContext<Ms>>;
|
|
479
|
+
export {};
|
|
480
|
+
//# sourceMappingURL=runProjections.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runProjections.d.ts","sourceRoot":"","sources":["../../src/runProjections.ts"],"names":[],"mappings":"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"}
|