@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,524 @@
1
+ /**
2
+ * The read side's CONSTRUCTION GATE: every wiring the read side refuses ON ITS OWN
3
+ * RULES, judged in one place, in one declared order, each verdict one
4
+ * `string | undefined`.
5
+ *
6
+ * ## ONE entry point over five rules, at three arities
7
+ *
8
+ * `projectionWiringFault` judges ONE CALL: the log the whole graph subscribes to,
9
+ * the one composed query and the one slice record every materialisation shares, and
10
+ * a LIST of the resolved per-entry wirings. `runProjection` hands it a singleton and
11
+ * `runProjections` hands it N, and both do so before either forks anything.
12
+ *
13
+ * The rules divide by what they are a function of, not by who called:
14
+ *
15
+ * - the COLLISION rung is a function of the whole list, pairwise;
16
+ * - R2 and the tuning figures are functions of ONE entry, judged for every entry in
17
+ * turn;
18
+ * - the servable grammar and the per-slice tag rule are functions of the SHARED
19
+ * query and the SHARED slice record, so they are judged once for the call.
20
+ *
21
+ * One record with the shared fields stated once and the entries as a list is what
22
+ * lets all three arities compose in one `??` chain. What the gate is stays what it
23
+ * was: one home, one return type, one declared order (below), and a caller that
24
+ * classifies the verdict under a preamble it was handed.
25
+ *
26
+ * ## The ONE refusal that is deliberately not here
27
+ *
28
+ * Naming it is what keeps "one place" honest. A slice record holding two DISTINCT
29
+ * `Schema`s under one event type is refused by `@kairos-es/codec`'s
30
+ * `decodeSlices`, which `prepareProjection` calls a few lines BELOW this gate on
31
+ * behalf of both entry points — so a rejected record still dies as a defect with
32
+ * nothing forked, exactly as a rejected wiring does, but the sentence a reader
33
+ * meets is the codec's. That rule
34
+ * belongs to the codec because BOTH of its consumers need it and only one is on
35
+ * the read side: the write path reaches the same merge through
36
+ * `buildDecisionModel` and never comes near this module, so a rung here
37
+ * would be a second home for one rule, covering half the hazard. `decode.ts` owns
38
+ * the argument; this is a pointer, not a restatement.
39
+ *
40
+ * Which of the two a record wrong in BOTH ways meets FIRST is not this module's to
41
+ * state either, and it has exactly one home: `prepareProjection` in
42
+ * `ProjectionRunner.ts`, the single body in which the gate call sits above the
43
+ * `decodeSlices` call, for both entry points at once. It was a claim two function
44
+ * bodies each made in prose until that phase was extracted, and pinned for only one
45
+ * of them.
46
+ *
47
+ * ## Why this is a module and not a paragraph of `runProjection`
48
+ *
49
+ * `runProjection` was doing two unrelated jobs. One is to refuse a wiring that can
50
+ * only ever be wrong — five rules, none of which mentions a stream, a scope or a
51
+ * fibre, and only one of which so much as compares two stores for identity. The
52
+ * other is to build and fork a pipeline. The refusals had
53
+ * grown to roughly two fifths of that function's body and most of its doc, and
54
+ * three of them wrote their own `Effect.die` with their own copy of the preamble
55
+ * naming the projection.
56
+ *
57
+ * The precedent is this package's own: `superviseOnProgress` was a deep module
58
+ * trapped inside the same function, and pulling it out is what made a supervision
59
+ * quantity pinnable against a stub instead of through the whole pipeline. This is
60
+ * the other half of that function, and it is deeper still by the measure that
61
+ * matters here — its interface is a record in and a sentence out, while everything
62
+ * behind it is five unrelated rules over four vocabularies (a layer graph, a
63
+ * tuning literal, a query grammar, a set of view stores).
64
+ *
65
+ * The fifth rule arriving from a DIFFERENT caller is what the module having been
66
+ * extracted bought. `runProjections` needed a construction-time refusal of its own,
67
+ * and there was already a place for it — with a settled return type, a settled
68
+ * division of labour with its caller, and a suite that states a rule against a
69
+ * record literal. Inline, it would have been a sixth thing inside a second pipeline
70
+ * function, in a second style, with a second preamble. It landed as a RUNG of the
71
+ * one chain rather than as a second entry point, which is what keeps the declared
72
+ * order below a property of this file rather than of which caller a reader opened.
73
+ *
74
+ * ## Why `string | undefined`, and why the shared record takes no `CheckpointKey`
75
+ *
76
+ * `undefined` for a sound wiring, otherwise the SPECIFIC sentence: what is wrong,
77
+ * what it would do, and how to fix it. It does not throw, does not call `Effect.die`
78
+ * and does not return an `Either` — ONE exit route and one return type, which is
79
+ * what lets five rules over four vocabularies compose with `??` and be read as five
80
+ * lines.
81
+ *
82
+ * The shared half of the record names no projection and no partition. The per-entry
83
+ * list does carry a resolved `CheckpointKey` each, but for the collision rung to
84
+ * COMPARE rather than for anything to report: no rule interpolates a `key` into its
85
+ * sentence except the collision rung, which names the partition the two offenders
86
+ * share.
87
+ *
88
+ * ATTRIBUTION and CLASSIFICATION both sit outside. This gate's ONE caller,
89
+ * `prepareProjection`, owns the single `Effect.die`; the PREAMBLE naming the
90
+ * projection (and, where there is one to name, the partition) comes from one level
91
+ * further out still, from whichever entry point was called. So the preamble is
92
+ * written once per ENTRY POINT rather than once per rule — which is exactly what
93
+ * collapsed three `Effect.die` sites to one. The gate
94
+ * writes the third party to that sentence: where there is more than one entry, the
95
+ * per-entry rungs prefix their verdict with the offending INDEX, in the same
96
+ * `materialisations[i]` vocabulary the collision rung already uses. Over a singleton
97
+ * there is no index to name and none is written, so a single wiring's message is
98
+ * what it always was. The same arrangement repeats one level DOWN, inside this file:
99
+ * `positiveFiniteDuration` owns the `Duration` bound and each of the four tuning
100
+ * call sites supplies the `role` clause, because the predicate knows the rule and
101
+ * its caller knows what breaks.
102
+ *
103
+ * The classification half of that is worth spelling out. That a mis-wiring is a
104
+ * DEFECT rather
105
+ * than a channel value is a claim about `runProjection`'s and `runProjections`'
106
+ * signatures — both error channels are `never` because everything that can go wrong
107
+ * once a projection is RUNNING belongs to the supervisor — and a gate returning a
108
+ * string makes no such claim, so it can be called from a plain function, a test, or
109
+ * an `Effect.gen` without three different failure conventions.
110
+ *
111
+ * ## The ORDER is a decision, and this is the only place it is stated
112
+ *
113
+ * The rules run OUTWARD-IN, from the widest wiring decision to the narrowest.
114
+ *
115
+ * The order is over RULES and not over ENTRIES. Entry 1's layer-graph mistake is
116
+ * still wider than entry 0's options-literal mistake, so rung 2 is judged for every
117
+ * entry before rung 3 is judged for any. An entry-major loop would instead let ARRAY
118
+ * POSITION decide which fault an author sees, and position carries no meaning here:
119
+ * the entries are a set, and are a tuple only because inference needs one.
120
+ *
121
+ * 1. **The collision rung**, once over the whole SET. Two materialisations sharing
122
+ * one store under one key is a property of the WHOLE CALL rather than of any one
123
+ * wiring in it, so it is judged before any single wiring is looked at — the same
124
+ * outward-in principle, one level up. And a colliding pair is worth hearing about
125
+ * BEFORE a mis-tuned figure in one of them, because fixing the tuning would leave
126
+ * the two runners racing one cursor. It cannot fire below N = 2, so it is vacuous
127
+ * for the singleton `runProjection` passes.
128
+ * 2. **R2, durability ordering**, PER ENTRY in entry order. A property of the LAYER
129
+ * GRAPH — which log, which view store — decided before any read model existed. If
130
+ * that pairing is illegal then nothing else about the runner matters, because the
131
+ * view it maintains cannot be true however well tuned the daemon is.
132
+ * 3. **The tuning figures**, PER ENTRY in entry order. What the caller wrote in the
133
+ * options literal, a smaller and more local mistake than picking the wrong pair
134
+ * of layers.
135
+ * 4. **The store's own servable grammar**, once over the SHARED composed query,
136
+ * reported in the STORE's vocabulary by `core`'s own `assertServableQuery`.
137
+ * 5. **The read side's stricter per-slice tag rule**, once over the SHARED slice
138
+ * record, and it MUST stay after (4): it catches only what that grammar admits,
139
+ * since a lone type-only item is servable. Were it first, a tagless-plus-tagged
140
+ * union — a violation of the shared grammar — would be reported as a read-side
141
+ * slice convention instead of in the vocabulary every other caller of that
142
+ * grammar meets it in.
143
+ *
144
+ * Hoisting the two SHARED rungs ahead of the two per-entry ones, which the
145
+ * shared-versus-per-entry axis would suggest, was considered and rejected: it
146
+ * changes the message a single wiring wrong in two ways gets, which is a fixed
147
+ * constraint. (2)-(3) ahead of (4)-(5) also restores the relative order that held
148
+ * before the supervisor was extracted, when its three restart durations were checked
149
+ * with `batchWindow` rather than a module away. But the point of writing the order
150
+ * down is not the restoration: it is that the order is now FIVE LINES a reader can
151
+ * read, rather than an emergent property of which of two modules happens to look at
152
+ * a field first.
153
+ *
154
+ * ## Why the input is a record of ALREADY-RESOLVED values
155
+ *
156
+ * Every field is a value, not an option: the caller has already applied its
157
+ * defaults — through `resolveProjection`, the one resolution site both callers
158
+ * share — so the gate judges exactly the figures that will run and cannot be handed
159
+ * one default while the pipeline uses another. That also holds the defaults
160
+ * themselves to the rules they document, for one decode per runner. The keys are
161
+ * RESOLVED `CheckpointKey`s for the same reason and one more: a partition a caller
162
+ * omitted is `DEFAULT_PARTITION`, so a rung judging the raw options would miss the
163
+ * commonest form of the collision, which is two entries that name no partition at
164
+ * all.
165
+ *
166
+ * The two INTEGER options are absent from the record on purpose. `BatchSize` and
167
+ * `MaxNoProgressRestarts` are branded, so their boundary is the caller's own
168
+ * `.make(...)` — a bad value is refused a stack frame away from where it was
169
+ * written and never reaches here at all.
170
+ *
171
+ * `slices` is a SHARED field and is typed as narrowly as the check needs — a query
172
+ * per slice, nothing else — which keeps this module free of a `@kairos-es/codec`
173
+ * import for a rule that has no opinion about schemas, payloads or folds.
174
+ * `Record<string, SliceProjection>` satisfies it structurally, so the record an entry
175
+ * point was handed reaches this rung unconverted.
176
+ *
177
+ * An entry's `cursorIdentity` is typed `object` and never called: it is an identity
178
+ * token the collision rung compares by REFERENCE. `object` rather than `ProjectionStore`
179
+ * because a `KeyedProjectionStore` is not structurally one (`readCheckpoint` is an
180
+ * `Effect` on the façade and a function on the port), so a caller holding only a
181
+ * bound view could not fill the field; and because `object` forbids `undefined`,
182
+ * which would otherwise make two unidentified entries compare equal.
183
+ *
184
+ * It is INTERNAL: nothing here is re-exported from the package index. The gate is
185
+ * how the two runner entry points refuse a wiring, not a capability the library
186
+ * offers — a caller with a wiring to validate has `runProjection` or
187
+ * `runProjections` itself. BOTH judge all five rules before either forks anything.
188
+ */
189
+ import { assertServableQuery } from '@kairos-es/core';
190
+ import { Duration, Either, Option } from 'effect';
191
+ import { durabilityOrderingFault } from "./EventLogDurability.js";
192
+ /**
193
+ * One `Duration.DurationInput` option, decoded and bounded: the `Duration` if it
194
+ * is positive and finite, otherwise the sentence explaining what it is and what
195
+ * that would do.
196
+ *
197
+ * FILE-PRIVATE, and applied four times, all of them by `tuningFault` below. It owns
198
+ * the BOUND and the shape of the sentence; each call site owns `role`, the clause
199
+ * saying what THIS option does and therefore what breaks at each degenerate end,
200
+ * because a generic "must be positive and finite" tells an author the rule but not
201
+ * the consequence, and the consequence is what makes the fix obvious. So the four
202
+ * call sites differ by exactly the thing that differs between them, and one bound is
203
+ * not stated four ways — the module doc's division, nested one level down.
204
+ *
205
+ * Three things about the checks are worth knowing rather than rediscovering:
206
+ *
207
+ * - **`Duration.decodeUnknown`, not `Duration.decode`.** The latter THROWS
208
+ * `Error('Invalid DurationInput')` on an unparseable string, which inside
209
+ * `Effect.gen` would already become a defect — but one whose message names
210
+ * neither the option nor the value. The `Option`-returning form (`decodeUnknown`
211
+ * is `Option.liftThrowable(decode)`) lets the fault carry both.
212
+ * - **`Duration.isZero` covers NEGATIVES too.** Every `Duration` constructor routes
213
+ * through one internal `make` that maps a non-positive `number` or `bigint` to the
214
+ * zero value, and the `[seconds, nanos]` form maps a `-Infinity` component to zero
215
+ * outright (verified against `effect@3.22`'s `Duration.ts`), so `-5`,
216
+ * `'-5 millis'` and `0` are indistinguishable by the time they are a `Duration`.
217
+ * There is therefore no separate "negative" case to test for — and the zero
218
+ * message has to SAY that, because a reader told "restartMinDelay is zero" while
219
+ * looking at `'-5 millis'` in their own wiring would otherwise conclude the check
220
+ * was reading the wrong field.
221
+ * - **Infinity is rejected everywhere, including `restartResetAfter`.** An infinite
222
+ * figure in any of these positions stops a TIMING mechanism from being one, and
223
+ * every claim the runner's docs make — a latency ceiling, a bounded backoff, a
224
+ * five-minute budget — stops being true. "Effectively never" remains expressible
225
+ * as a large finite figure, so a uniform rule costs a caller nothing and is one
226
+ * predicate rather than four. It needs its own check: `Duration.isZero` answers
227
+ * `false` for the infinite `Duration`, so the two bounds are independent.
228
+ *
229
+ * `String(input)`, never `JSON.stringify`, for the same reason `superviseOnProgress`
230
+ * reduces its log annotations with `String`: `DurationInput` admits a `bigint` of
231
+ * nanoseconds, and `JSON.stringify` THROWS on one — inside the very code path whose
232
+ * job is to explain a mistake clearly.
233
+ */
234
+ const positiveFiniteDuration = (name, input, role) => {
235
+ const decoded = Duration.decodeUnknown(input);
236
+ if (Option.isNone(decoded)) {
237
+ return Either.left(`${name} is not a Duration.DurationInput (got ${String(input)}). Pass a ` + "string like '50 millis', a Duration, a number of milliseconds, a " + 'bigint of nanoseconds, or a [seconds, nanos] tuple.');
238
+ }
239
+ if (!Duration.isFinite(decoded.value)) {
240
+ return Either.left(`${name} is infinite (got ${String(input)}). ${role}`);
241
+ }
242
+ if (Duration.isZero(decoded.value)) {
243
+ return Either.left(`${name} is zero (got ${String(input)}; Duration clamps any negative ` + `input to zero, so a negative arrives here as zero too). ${role}`);
244
+ }
245
+ return Either.right(decoded.value);
246
+ };
247
+ /**
248
+ * The runner's four `Duration`s, decoded and bounded: `undefined` when the tuning
249
+ * is sound, otherwise the sentence naming the FIRST violation.
250
+ *
251
+ * ## WHY these are checked at all when both integer options are branded
252
+ *
253
+ * `Duration.DurationInput` is a UNION — a `Duration`, millis as a `number`, nanos
254
+ * as a `bigint`, a `[seconds, nanos]` tuple, or a `'50 millis'` string — so there
255
+ * is no single primitive for `Schema.Int`-plus-`Schema.brand` to refine, and
256
+ * validity is only knowable after `Duration.decode` runs. Branding one anyway would
257
+ * mean one of two losses: a constructor every call site must thread a raw input
258
+ * through, or a field narrowed to `Duration` alone, which makes
259
+ * `batchWindow: '50 millis'` — the ergonomic point of `DurationInput`, and what
260
+ * every test and example in this repo writes — unwritable. So the check happens at
261
+ * CONSTRUCTION instead, where the caller turns it into a defect. Same discipline as
262
+ * `BatchSize` and `MaxNoProgressRestarts` (fail at the boundary, report where the
263
+ * author can act), different mechanism, because the type is a different shape.
264
+ *
265
+ * ## Why all four together, when they tune two different machines
266
+ *
267
+ * `batchWindow` paces the PIPELINE and the three restart figures pace the
268
+ * SUPERVISOR, and those two modules deliberately know nothing of each other. But
269
+ * the mistake is one mistake — a degenerate figure whose failure mode is a hot loop
270
+ * or a permanently frozen view with nothing on any error channel — and an author
271
+ * who wrote `restartMinDelay: 0` beside `batchWindow: 0` should not have to fix one
272
+ * and re-run to be told about the other by a differently-worded message from a
273
+ * different module. `Either.all` short-circuits on the first violation, so one
274
+ * degenerate figure is one sentence naming one option; what the single home buys is
275
+ * that the sentence is built the same way whichever field it is about.
276
+ *
277
+ * ## The one CROSS-FIELD rule, and the one deliberately absent
278
+ *
279
+ * `restartMinDelay <= restartMaxDelay` is checked, in the spirit of
280
+ * `ReadPostgresConfig`'s own cross-field filter: the names assert an ordering, and
281
+ * inverting it is silently absorbed rather than rejected. `Schedule.union` takes the
282
+ * SHORTER of its two arms, so a min above the max deletes the exponential
283
+ * entirely — every restart sleeps exactly `restartMaxDelay`, the documented sawtooth
284
+ * never happens, and the no-progress budget is off by however far apart the two
285
+ * figures are. Equality is allowed: min == max is a deliberate flat pacing, not a
286
+ * contradiction.
287
+ *
288
+ * `restartResetAfter` is deliberately NOT cross-checked against the delays,
289
+ * although a reset threshold below `restartMinDelay` does flatten the backoff (the
290
+ * schedule resets before it can grow). The difference is that no ordering is implied
291
+ * there — the two figures measure different things, elapsed retrying time against
292
+ * one sleep — so a short reset is a tuning choice expressible another way
293
+ * (min == max), whereas min above max is a statement that contradicts itself.
294
+ * Rejecting the former would be inventing a policy rather than enforcing a rule.
295
+ *
296
+ * The words `invalid tuning` are this sentence's own, not the caller's preamble's:
297
+ * the preamble says which projection is mis-WIRED, and this says the wiring fault is
298
+ * a mis-set figure rather than a bad pair of layers or a mis-built slice. They are a
299
+ * CONSTANT rather than a literal at each of the two return sites below, so the phrase
300
+ * an operator greps for is written once even though the rule is one function.
301
+ */
302
+ const MIS_TUNED = 'invalid tuning —';
303
+ const tuningFault = tuning => {
304
+ const checked = Either.all({
305
+ batchWindow: positiveFiniteDuration('batchWindow', tuning.batchWindow, 'batchWindow is how long a PARTIAL batch waits before committing anyway: ' + 'at zero Stream.groupedWithin spins, emitting empty chunks without ever ' + 'pulling the subscription; infinite means a partial batch never flushes, ' + 'so a lone live event waits for batchSize-1 more events that may never ' + 'come.'),
306
+ restartMinDelay: positiveFiniteDuration('restartMinDelay', tuning.restartMinDelay, 'restartMinDelay is the BASE Schedule.exponential multiplies: at zero ' + 'every delay in the series is zero and the supervisor restarts in a hot ' + 'loop; infinite means the first restart never arrives.'),
307
+ restartMaxDelay: positiveFiniteDuration('restartMaxDelay', tuning.restartMaxDelay, 'restartMaxDelay is the backoff CEILING, unioned with the exponential: ' + 'Schedule.union takes the shorter arm, so at zero it caps every sleep to ' + 'nothing and the supervisor restarts in a hot loop; infinite removes the ' + 'cap and lets the exponential grow without bound.'),
308
+ restartResetAfter: positiveFiniteDuration('restartResetAfter', tuning.restartResetAfter, 'restartResetAfter is the elapsed-retrying threshold at which the backoff ' + 'starts over: at zero it resets on every decision so the backoff never ' + 'grows, and infinite means it never resets — either way the sawtooth is ' + "gone, and the no-progress budget's default is sized in WALL CLOCK " + 'against that sawtooth, so it would no longer buy the minutes it ' + 'documents.')
309
+ });
310
+ if (Either.isLeft(checked)) {
311
+ return `${MIS_TUNED} ${checked.left}`;
312
+ }
313
+ if (Duration.greaterThan(checked.right.restartMinDelay, checked.right.restartMaxDelay)) {
314
+ return `${MIS_TUNED} restartMinDelay ` + `(${Duration.format(checked.right.restartMinDelay)}) is greater than ` + `restartMaxDelay (${Duration.format(checked.right.restartMaxDelay)}). ` + 'Schedule.union takes the SHORTER of its two arms, so an inverted pair ' + 'silences the exponential entirely: every restart would sleep exactly ' + 'restartMaxDelay, the documented sawtooth would not happen, and the ' + 'no-progress budget would be wrong by however far apart the two figures ' + 'are. Swap them, or raise the ceiling.';
315
+ }
316
+ return undefined;
317
+ };
318
+ /**
319
+ * The composed subscription query against the STORE's servable grammar:
320
+ * `undefined` when the store would serve it, otherwise `core`'s own sentence about
321
+ * why it would not.
322
+ *
323
+ * ## Why `core`'s `assertServableQuery` and not a predicate of our own
324
+ *
325
+ * It is the very function every engine applies inside `read`/`subscribe`/`append`,
326
+ * so calling it is what makes ONE grammar rather than two that agree until one is
327
+ * edited. A local re-implementation would be a second definition of servability
328
+ * living in a package that does not own the concept, free to drift in either
329
+ * direction: too strict and it refuses a query the store would happily serve, too
330
+ * lax and the violation resurfaces at the first stream pull, inside the forked
331
+ * fibre, as a `PipelineDied` that only becomes visible as a `ProjectionStalled`
332
+ * once the breaker trips on a checkpoint that never moved — several backoff sleeps
333
+ * later, describing a stall rather than the mis-built slice that caused it.
334
+ *
335
+ * ## Why the THROW is converted rather than propagated
336
+ *
337
+ * `assertServableQuery` throws, because its own callers are store methods where a
338
+ * throw inside an `Effect` is already the defect it should be. Here it is one of
339
+ * five rules whose results compose, so letting it throw would give this gate two
340
+ * exit routes — a returned sentence for four rules and an exception for the
341
+ * fifth — and every caller would then need both a `??` chain and a `try`. Catching
342
+ * it keeps ONE exit route, and the caller keeps one `Effect.die`.
343
+ *
344
+ * The message is passed through VERBATIM, which is the point of the conversion
345
+ * rather than a detail of it: the violation goes on being reported in the store's
346
+ * vocabulary, in the same words the same rule produces when a store rejects the
347
+ * same query, so an author who meets it twice meets it once. The `instanceof`
348
+ * fallback is for the shape of the contract rather than for anything observed —
349
+ * `throw` admits any value, and `String(error)` is the honest reading of one this
350
+ * module cannot inspect.
351
+ */
352
+ const servableQueryFault = wiring => {
353
+ try {
354
+ assertServableQuery(wiring.query);
355
+ return undefined;
356
+ } catch (error) {
357
+ return error instanceof Error ? error.message : String(error);
358
+ }
359
+ };
360
+ /**
361
+ * The read side's OWN per-slice rule: `undefined` when every slice carries at least
362
+ * one tag, otherwise the sentence naming the first slice that does not.
363
+ *
364
+ * Checked slice by slice because the composed grammar above cannot see it: a SINGLE
365
+ * tagless slice composes to one type-only item, which IS servable, so the store
366
+ * would run that subscription happily. It is still a programming error, for two
367
+ * reasons. It does not COMPOSE — adding a second, tagged slice later turns today's
368
+ * accepted query into a tagless-plus-tagged union the store rejects, so the mistake
369
+ * would surface as a break in an unrelated change rather than at the slice that
370
+ * caused it. And a read model that genuinely is global should SAY so with an
371
+ * explicit `system:…` tag, which is the convention every other boundary in the repo
372
+ * already follows (CONTEXT); saying it by omission reads as a forgotten tag.
373
+ *
374
+ * The sentence names the offending SLICE, the rule and the fix, and deliberately
375
+ * not the projection: the caller's preamble carries that, for every rule at once,
376
+ * together with the partition where the caller has one worth naming.
377
+ */
378
+ const taglessSliceFault = wiring => {
379
+ for (const [name, slice] of Object.entries(wiring.slices)) {
380
+ const untagged = slice.query.items.length === 0 || slice.query.items.some(item => item.tags.length === 0);
381
+ if (untagged) {
382
+ return `read-model slice '${name}' carries NO tags. Every read-model slice ` + 'must carry at least one tag: build it with .build({ tags: [...] }), ' + 'and give a genuinely GLOBAL slice an explicit system tag ' + "(tagIdentity('system').make('…')) rather than no tag at all. A " + 'tagless slice is servable on its own — it is the single type-only fast ' + 'path — which is exactly why it is rejected here rather than by the ' + 'store: composing it with any tagged slice later would produce an ' + 'unservable tagless-plus-tagged union query, so the mistake would ' + 'surface far from its cause.';
383
+ }
384
+ }
385
+ return undefined;
386
+ };
387
+ /**
388
+ * Apply a PER-ENTRY rule to every entry in turn, returning the first sentence one of
389
+ * them yields — prefixed with the offending index where, and only where, there is
390
+ * more than one entry to index into.
391
+ *
392
+ * The condition is what keeps a single wiring's message byte-identical to what it
393
+ * was before the two callers shared this chain, and it keeps the module internally
394
+ * consistent: an index appears exactly where there is a set. The collision rung
395
+ * needs no such condition because it cannot fire below N = 2, so its indices are
396
+ * always meaningful.
397
+ *
398
+ * The prefix copies the collision rung's own vocabulary — `materialisations[i]` —
399
+ * rather than inventing a second way to name an entry, so an author who meets both
400
+ * meets one.
401
+ */
402
+ const perEntry = (entries, rule) => {
403
+ for (let index = 0; index < entries.length; index += 1) {
404
+ const fault = rule(entries[index]);
405
+ if (fault !== undefined) {
406
+ return entries.length > 1 ? `materialisations[${index}] — ${fault}` : fault;
407
+ }
408
+ }
409
+ return undefined;
410
+ };
411
+ /**
412
+ * Judge ONE CALL's whole wiring: `undefined` when it is sound, otherwise the
413
+ * sentence describing the FIRST rule it breaks.
414
+ *
415
+ * Five rules, `??`-chained in the outward-in order the module doc argues for and
416
+ * fixes. `??` rather than an array of predicates or a collected list of every
417
+ * violation, for two reasons: the first fault is the one to fix — the later rules
418
+ * judge a wiring whose wider decisions are already known to be wrong, so their
419
+ * verdicts would be noise — and a chain of five named calls is the whole
420
+ * implementation, which is what makes the declared order checkable against the code
421
+ * rather than merely asserted about it.
422
+ *
423
+ * The two PER-ENTRY rungs go through `perEntry`, which is what keeps the chain
424
+ * rule-major: every entry is judged by rung 2 before any entry is judged by rung 3.
425
+ *
426
+ * Every field is a RESOLVED value: see the module doc for why (and for why the two
427
+ * integer options are not here at all).
428
+ */
429
+ export const projectionWiringFault = wiring => materialisationCollisionFault(wiring) ?? perEntry(wiring.materialisations, entry => durabilityOrderingFault({
430
+ eventLogDurability: wiring.eventLogDurability,
431
+ viewDurability: entry.viewDurability
432
+ })) ?? perEntry(wiring.materialisations, tuningFault) ?? servableQueryFault(wiring) ?? taglessSliceFault(wiring);
433
+ /**
434
+ * The SET-LEVEL rung: `undefined` when no two materialisations of one read model
435
+ * would fight over one cursor, otherwise the sentence naming the offending pair and
436
+ * the fix.
437
+ *
438
+ * It is the FIRST rung in the declared order despite being the last symbol in this
439
+ * file, which is layout rather than precedence: the four per-wiring predicates sit
440
+ * above the chain that composes them, and this one — the newest and by far the
441
+ * longest — sits below it rather than pushing them apart. The module doc argues the
442
+ * order.
443
+ *
444
+ * FILE-PRIVATE, like every other rung: the chain is the only entry point, so a
445
+ * caller cannot reach one rule without the four that are declared to precede or
446
+ * follow it. It keeps its NAME because ADR-0007 cites the predicate by name.
447
+ *
448
+ * ## The rule
449
+ *
450
+ * A fault when two entries share the SAME store REFERENCE — compared by identity,
451
+ * never called, which is why the field is typed `object` — and their resolved
452
+ * `CheckpointKey`s are equal in both halves. Both conditions are needed and
453
+ * neither is sufficient, which is the whole content of the rule and the reason it
454
+ * takes resolved keys rather than a caller's raw options.
455
+ *
456
+ * Why the pairing is a defect: a `CheckpointKey` addresses ONE cursor within ONE
457
+ * store, so two runners over that pair are two writers over one scalar guarded
458
+ * compare-and-set. Every batch races it; one wins, the loser's `commit` fails
459
+ * `CheckpointSuperseded` and its own view writes are aborted with it; the loser then
460
+ * resynchronises from the winner's position and does it again. Nothing errors out to
461
+ * an operator — the supervisor's breaker deliberately does NOT trip, because the
462
+ * shared signal keeps moving, which is exactly the competing-writer case
463
+ * `superviseOnProgress` is built to absorb — so the observable result is two views
464
+ * each holding an arbitrary subset of the events, indefinitely, at the schedule's
465
+ * rate. It is the "one MATERIALISATION, one runner" invariant of ADR-0007 — which
466
+ * that record's own amendment is careful to word that way rather than per READ
467
+ * MODEL, a read model being materialisable N ways on purpose — broken in the one way
468
+ * a single `runProjection` call cannot break it.
469
+ *
470
+ * ## PAIRWISE, deliberately
471
+ *
472
+ * Two nested loops over N, where N is the number of ways one read model is
473
+ * materialised — two or three in every shape this exists for, and bounded by how many
474
+ * view stores an application has. A map keyed on the store plus the flattened key
475
+ * would be asymptotically better and worse in every way that matters here: it would
476
+ * need a key flattening (with the separator-injection question `mapKey` in
477
+ * `inMemoryProjectionStore.ts` had to settle) and a composite map key that cannot
478
+ * hold an object identity, to save microseconds on a list of three at construction
479
+ * time. Pairwise says the rule in the shape the rule is written in.
480
+ *
481
+ * ## The two bounds, which are DECISIONS and not oversights
482
+ *
483
+ * It does not reject equal keys across DIFFERENT stores. That is the sound and
484
+ * intended shape, and the reason this rung needs the store identity at all: one read
485
+ * model realised N ways shares ONE `ProjectionId`, because a `CheckpointKey` is a
486
+ * lookup identifier within one store rather than a global name. Two stores holding
487
+ * the same key are two materialisations of one read model, each with its own cursor,
488
+ * which is precisely what `runProjections` is for —
489
+ * `packages/read-postgres/test/courseRosterGraduation.test.ts` asserts that key
490
+ * equality across an in-memory and a SQL store as its acceptance criterion.
491
+ *
492
+ * And it cannot detect two SEPARATELY CONSTRUCTED stores pointed at one physical
493
+ * namespace — two `makeSqlProjectionStore` calls on the same schema and table
494
+ * prefix, say. Reference equality is blind to that by construction, and the
495
+ * same class of limit is already recorded for the SQL backend's own schema isolation
496
+ * in `packages/read-postgres/src/internal/ddl.ts`, which can reject an identifier it
497
+ * would truncate but cannot know what another config points at. What reference
498
+ * equality does catch is the realistic mistake: ONE store value passed twice, which
499
+ * is what the ergonomic shape of the call invites — a caller who has one store to
500
+ * hand writes it into both entries.
501
+ *
502
+ * The sentence names the two entries by INDEX and the partition they share, states
503
+ * what the pairing would do, and gives both fixes — the one that is almost always
504
+ * wanted (one materialisation whose single `apply` writes both views inside the one
505
+ * `commit`) and the escape hatch that makes genuine cohabitation legal (a distinct
506
+ * `partition`). It names no projection: the caller's preamble carries that, for every
507
+ * rule at once.
508
+ */
509
+ const materialisationCollisionFault = wiring => {
510
+ const {
511
+ materialisations
512
+ } = wiring;
513
+ for (let left = 0; left < materialisations.length; left += 1) {
514
+ for (let right = left + 1; right < materialisations.length; right += 1) {
515
+ const one = materialisations[left];
516
+ const other = materialisations[right];
517
+ if (one.cursorIdentity === other.cursorIdentity && one.key.projection === other.key.projection && one.key.partition === other.key.partition) {
518
+ return `colliding materialisations — materialisations[${left}] and ` + `materialisations[${right}] share ONE ProjectionStore under the SAME ` + `CheckpointKey (partition '${one.key.partition}'), so they would be two ` + 'runners over one cursor. Every batch would race the same guarded ' + "advance: one wins, the loser's commit fails CheckpointSuperseded and " + 'its own view writes are aborted with it, and because the shared ' + 'checkpoint keeps moving the supervisor never gives up — so the two ' + 'views would each hold an arbitrary subset of the events, ' + 'indefinitely, with nothing on any error channel. SEVERAL VIEWS IN ONE ' + 'STORE is a supported shape and this is not how to reach it: it wants ' + 'ONE materialisation whose single apply writes both views inside the ' + 'one commit, under one checkpoint. Where two materialisations genuinely ' + 'must cohabit one store, give one of them its own partition — a ' + 'distinct PartitionId is a distinct CheckpointKey and therefore a ' + 'cursor of its own.';
519
+ }
520
+ }
521
+ }
522
+ return undefined;
523
+ };
524
+ //# sourceMappingURL=projectionWiringFault.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projectionWiringFault.js","names":["assertServableQuery","Duration","Either","Option","durabilityOrderingFault","positiveFiniteDuration","name","input","role","decoded","decodeUnknown","isNone","left","String","isFinite","value","isZero","right","MIS_TUNED","tuningFault","tuning","checked","all","batchWindow","restartMinDelay","restartMaxDelay","restartResetAfter","isLeft","greaterThan","format","undefined","servableQueryFault","wiring","query","error","Error","message","taglessSliceFault","slice","Object","entries","slices","untagged","items","length","some","item","tags","perEntry","rule","index","fault","projectionWiringFault","materialisationCollisionFault","materialisations","entry","eventLogDurability","viewDurability","one","other","cursorIdentity","key","projection","partition"],"sources":["../../src/projectionWiringFault.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,109 @@
1
+ import { Effect, Layer } from 'effect';
2
+ import { buildAndFork, prepareProjection, resolveProjection } from "./ProjectionRunner.js";
3
+ /**
4
+ * Start the daemon maintaining `readModel`, forked into the caller's `Scope`.
5
+ *
6
+ * The returned effect is INFALLIBLE (`E = never`): starting a projection cannot
7
+ * fail, because everything that can go wrong once it is running is the
8
+ * supervisor's business and lives on the fibre's error channel.
9
+ *
10
+ * ## What can go wrong BEFORE the fork is a programming error, and those are defects
11
+ *
12
+ * Which wirings are refused on the READ SIDE'S rules, why each one is a wiring
13
+ * nothing at runtime would ever notice, and the order they are judged in are all
14
+ * `projectionWiringFault`'s — one gate over five rules, whose only relation to this
15
+ * function is that it is the one place they can be applied before anything exists to
16
+ * be wrong. The gate judges a SET of materialisations, and this function supplies a
17
+ * SINGLETON: the two PER-ENTRY rungs judge that one wiring, the two that are
18
+ * functions of the shared query and slice record judge it once and would do so
19
+ * whatever N was, and the collision rung is a property of the set and is vacuous over
20
+ * one entry, having no sibling to be paired with. That is also why nothing here carries
21
+ * an entry index — the gate names one only where there is a set to index into.
22
+ * A further refusal is the CODEC's and fires just below the gate; `prepareProjection`
23
+ * owns that ordering for both entry points, and the gate's doc says why the rule
24
+ * stays in the codec. This function keeps the two claims that are its own.
25
+ *
26
+ * The verdict is a DEFECT rather than a value on the error channel, because no
27
+ * application can handle its own mis-wiring: the only repair is to change the
28
+ * wiring, and a channel value would ask every caller to write a handler for a
29
+ * mistake that has already been made by the time the program runs. `Effect.die`
30
+ * with an `Error` is the same classification the store gives a mis-built query
31
+ * through `assertServableQuery`, which is one of the rules.
32
+ *
33
+ * And it lands on THIS effect before the `forkScoped` at the foot of `buildAndFork`:
34
+ * the gate is consulted while the daemon is still being
35
+ * assembled, so a rejected wiring has read no checkpoint, opened no subscription
36
+ * and left no half-started fibre behind. That is what makes the defect safe to
37
+ * raise rather than merely early, and every construction case in this package
38
+ * asserts the stored checkpoint is still `ORIGIN` afterwards to keep it true.
39
+ *
40
+ * The preamble on the message is written HERE, once, naming the projection and the
41
+ * partition — the gate returns the sentence about the RULE, and this function knows
42
+ * WHO broke it. The single `Effect.die` that joins them is `prepareProjection`'s,
43
+ * which is why there is one for both entry points rather than one per rule.
44
+ */
45
+ export const runProjection = (readModel, options) => Effect.gen(function* () {
46
+ // ## RESOLVE — every value the gate has to judge, and every figure that runs
47
+ //
48
+ // `resolveProjection` is a pure function shared with `runProjections`, so the
49
+ // defaults, the key and the view's durability are read in ONE place for both
50
+ // callers: the figure that was judged cannot differ from the figure that runs.
51
+ const view = readModel.store;
52
+ const resolved = resolveProjection(view, options);
53
+ // ## PREPARE — the log's durability, the merged query, the gate, the decode
54
+ //
55
+ // The shared middle, and the same call `runProjections` makes: it reads
56
+ // `EventLogDurability` from context, composes the query, consults the gate and
57
+ // only then builds the decode stage — an order `prepareProjection` owns on behalf
58
+ // of both entry points, and whose reasons are on it rather than restated here.
59
+ //
60
+ // The entry list is a SINGLETON, over which the collision rung has no pair to
61
+ // find. Its `cursorIdentity` is what that rung compares by reference — the BOUND
62
+ // view here, which `runProjections` could not use and does not, since a lone
63
+ // entry can only ever be compared with itself and the choice is therefore free at
64
+ // this arity.
65
+ //
66
+ // The preamble is this function's own half of the defect message and the only
67
+ // half it writes: the gate returns the sentence about the RULE, and this names
68
+ // the projection and the partition whose wiring broke it. No entry index, because
69
+ // there is no set to index into.
70
+ const prepared = yield* prepareProjection(readModel.slices, [{
71
+ ...resolved,
72
+ cursorIdentity: view
73
+ }], `runProjection: invalid wiring for projection ` + `'${resolved.key.projection}' (partition ` + `'${resolved.key.partition}')`);
74
+ // ## BUILD AND FORK — the pipeline, the read side's own rules now satisfied
75
+ return yield* buildAndFork(resolved, {
76
+ query: prepared.query,
77
+ decode: prepared.decode,
78
+ apply: readModel.apply,
79
+ options
80
+ });
81
+ });
82
+ /**
83
+ * `runProjection` as a `Layer` whose own scope owns the daemon.
84
+ *
85
+ * It provides NOTHING (`ROut = never`): a projection is a background process, not
86
+ * a service, and nothing should be able to depend on it as one — a read model is
87
+ * queried through its view store, never through the runner. What the `Layer` buys
88
+ * is lifecycle: dropped into an application's layer graph, the daemon starts when
89
+ * the graph is built and is interrupted when it is released, alongside the store
90
+ * layers it reads from, with no bespoke startup or shutdown code.
91
+ *
92
+ * Discarding the runner discards its fibre too, which is correct and has one
93
+ * consequence worth wiring for: under this `Layer` — the recommended shape — a
94
+ * `ProjectionStalled` has no fibre to be awaited on, so it reaches the outside
95
+ * world only through the supervisor's `Effect.logError` and through `onStalled`.
96
+ * That hook is the reason a stall need not be a log line somebody happens to grep
97
+ * for; pass one in `options` if a stalled projection should fail a health check,
98
+ * exit non-zero, or page.
99
+ *
100
+ * `Layer.scopedDiscard` discharges the `Scope` the fork needs; the store, its
101
+ * `EventLogDurability` declaration, the `Serializer`, the slices' requirements and
102
+ * the read model's own `R` stay on the layer's inputs, so they are satisfied the
103
+ * same way every other layer's are. `EventLogDurability` being an INPUT is what
104
+ * makes the wiring read correctly: the declaration is provided once, alongside the
105
+ * `DcbEventStore` layer it describes, and every projection layer in the graph is
106
+ * then satisfied from that one declaration.
107
+ */
108
+ export const projectionLayer = (readModel, options) => Layer.scopedDiscard(runProjection(readModel, options));
109
+ //# sourceMappingURL=runProjection.js.map