@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,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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuFG;AACH,OAAO,KAAK,EACV,aAAa,EACb,UAAU,EACV,eAAe,EAChB,MAAM,kBAAkB,CAAA;AACzB,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAClE,OAAO,EAAE,KAAK,IAAI,GAAG,EAAE,MAAM,EAAE,KAAK,KAAK,EAAE,MAAM,QAAQ,CAAA;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAC9D,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,EAG7B,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,eAAe,EACrB,MAAM,mBAAmB,CAAA;AAE1B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,WAAW,eAAe,CAAC,CAAC,GAAG,KAAK,EAAE,CAAC,GAAG,KAAK;IACnD;;;;;;OAMG;IACH,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAA;IAE/B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAkCG;IACH,QAAQ,CAAC,KAAK,EAAE,CACd,KAAK,EAAE,GAAG,CAAC,qBAAqB,CAAC,YAAY,CAAC,KAC3C,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;IAE9B;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,WAAW,CAAA;IAEhC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAiCG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,uBAAuB,CAAA;CAC3C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,KAAK,kBAAkB,GAAG,eAAe,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;AAE3D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,KAAK,sBAAsB,CACzB,EAAE,SAAS,GAAG,CAAC,qBAAqB,CAAC,kBAAkB,CAAC,IACtD,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,eAAe,CAAC,MAAM,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAA;AAEzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiGG;AACH,MAAM,WAAW,sBAAsB,CACrC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,EACzC,EAAE,SACA,GAAG,CAAC,qBAAqB,CAAC,kBAAkB,CAAC,GAAG,GAAG,CAAC,qBAAqB,CAAC,eAAe,CAAC;IAE5F;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAA;IAElB;;;;;;OAMG;IACH,QAAQ,CAAC,UAAU,EAAE,YAAY,CAAA;IAEjC,mDAAmD;IACnD,QAAQ,CAAC,gBAAgB,EAAE,EAAE,CAAA;CAC9B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,eAAO,MAAM,cAAc,GACzB,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,EACzC,EAAE,SAAS,GAAG,CAAC,qBAAqB,CAAC,kBAAkB,CAAC,EAExD,MAAM,sBAAsB,CAAC,CAAC,EAAE,EAAE,CAAC,KAClC,MAAM,CAAC,MAAM,CACd,GAAG,CAAC,qBAAqB,CAAC,gBAAgB,CAAC,EAC3C,KAAK,EACH,KAAK,CAAC,KAAK,GACX,aAAa,GACb,kBAAkB,GAClB,UAAU,GACV,aAAa,CAAC,CAAC,CAAC,GAChB,sBAAsB,CAAC,EAAE,CAAC,CAkG1B,CAAA"}