@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,468 @@
1
+ import { decodeSlices } from '@kairos-es/codec';
2
+ import { composeProjections, DcbEventStore } from '@kairos-es/core';
3
+ import { Array as Arr, Chunk, Data, Effect, Ref, Schema, Stream } from 'effect';
4
+ import { EventLogDurability } from "./EventLogDurability.js";
5
+ import { projectionWiringFault } from "./projectionWiringFault.js";
6
+ import { DEFAULT_RESTART_MAX_DELAY, DEFAULT_RESTART_MIN_DELAY, DEFAULT_RESTART_RESET_AFTER, superviseOnProgress } from "./superviseOnProgress.js";
7
+ /**
8
+ * Maximum events per commit: a branded POSITIVE integer.
9
+ *
10
+ * Branded because `batchSize` is the tuning figure whose invalid values are
11
+ * absorbed most quietly. `Stream.groupedWithin(0, window)` does not throw and
12
+ * does not merely under-batch: it emits EMPTY chunks in a tight loop without
13
+ * ever pulling the subscription, so a projection wired with `batchSize: 0`
14
+ * spins a CPU for ever, commits nothing, stays at `ORIGIN`, never fails, never
15
+ * restarts and logs nothing — the exact silent-stall class the supervisor below
16
+ * exists to make loud, arriving by a route the supervisor cannot see (the
17
+ * breaker only counts RESTARTS, and a spinning stream never ends a run). A
18
+ * fractional or `NaN` size is the same mistake by a different route.
19
+ *
20
+ * The SHAPE mirrors core's `ReadLimit` — `Schema.Int`, a bound, a brand, the
21
+ * same fail-at-the-boundary discipline — but deliberately NOT `ReadLimit`
22
+ * itself, which is `>= 0`. Zero is the whole difference between the two: a
23
+ * `read` capped at zero events is a meaningful (if useless) request, whereas a
24
+ * COMMIT of zero events is not a request at all. Reusing `ReadLimit` here would
25
+ * import the one value that has to be rejected.
26
+ *
27
+ * A distinct brand from `MaxNoProgressRestarts` despite the identical
28
+ * refinement, because the two are still writable side by side in one flat
29
+ * options literal, are both bare integers, and differ by an order of magnitude
30
+ * in meaning — nominality is what makes a transposition fail to typecheck rather
31
+ * than silently retune the runner. That the two now tune different mechanisms
32
+ * (this one the pipeline, that one the supervisor) is a second reason for the
33
+ * distinction, not a replacement for the first.
34
+ *
35
+ * Construct with `BatchSize.make(n)`, which throws a `ParseError` at the call
36
+ * site rather than deferring the mistake to the first stream pull.
37
+ */
38
+ export const BatchSize = /*#__PURE__*/Schema.Int.pipe(/*#__PURE__*/Schema.positive(), /*#__PURE__*/Schema.brand('BatchSize'));
39
+ /**
40
+ * Half the store's `catchUpPageSize` default (512), so a commit transaction stays
41
+ * comfortably smaller than one catch-up page while still amortising the
42
+ * round-trip across a couple of hundred events.
43
+ */
44
+ const DEFAULT_BATCH_SIZE = /*#__PURE__*/BatchSize.make(256);
45
+ /**
46
+ * An order of magnitude under the store's 1-second `pollInterval` — the live
47
+ * tail's own latency floor — so the batching window never dominates end-to-end
48
+ * lag.
49
+ */
50
+ const DEFAULT_BATCH_WINDOW = '50 millis';
51
+ /**
52
+ * RESOLVE: apply every default and read both call-independent facts off the bound
53
+ * view, so the gate can judge exactly the figures the pipeline will run.
54
+ *
55
+ * A PURE function — no `Effect`, no context, no clock — which is what lets BOTH
56
+ * runner entry points share one resolution site. That is the whole reason it exists
57
+ * as a function rather than as a paragraph of `runProjection`'s body: the gate is
58
+ * only as good as the argument it is handed, and two resolution sites are free to
59
+ * drift into approving a figure the pipeline does not use. One site, and the figure
60
+ * that was judged cannot differ from the figure that runs, for one wiring and for N
61
+ * of them alike.
62
+ *
63
+ * The five `??` defaults are resolved HERE and ONCE. `batchSize` and `batchWindow`
64
+ * go into the pipeline, the three restart durations into the supervision options,
65
+ * where `superviseOnProgress` requires all three for exactly that reason.
66
+ *
67
+ * `maxNoProgressRestarts` is deliberately absent, as it is from the gate: it is
68
+ * BRANDED, so its boundary was the caller's own `.make(...)`, nothing here has to
69
+ * resolve it, and its default stays where its wall-clock arithmetic is documented.
70
+ *
71
+ * It takes the BOUND view rather than the unbound port because `key` and
72
+ * `viewDurability` are its two reads and both are on the bound façade. The UNBOUND
73
+ * store is still the only thing that can answer "are these two entries the same
74
+ * store?" — `forKey` deliberately does not expose what it closed over — so the gate
75
+ * takes that identity as a field of its own, from whichever caller holds one.
76
+ */
77
+ export const resolveProjection = (view, options) => ({
78
+ view,
79
+ key: view.key,
80
+ viewDurability: view.durability,
81
+ batchSize: options?.batchSize ?? DEFAULT_BATCH_SIZE,
82
+ batchWindow: options?.batchWindow ?? DEFAULT_BATCH_WINDOW,
83
+ restartMinDelay: options?.restartMinDelay ?? DEFAULT_RESTART_MIN_DELAY,
84
+ restartMaxDelay: options?.restartMaxDelay ?? DEFAULT_RESTART_MAX_DELAY,
85
+ restartResetAfter: options?.restartResetAfter ?? DEFAULT_RESTART_RESET_AFTER
86
+ });
87
+ /**
88
+ * A defect surfaced from the drain, converted into a value the supervisor can act
89
+ * on.
90
+ *
91
+ * **Why the conversion exists.** `DcbEventStore.subscribe` is `E = never` by
92
+ * contract (ADR-0002: infrastructure faults are defects, not channel values), so a
93
+ * store whose live tail fails beyond its own retry budget DIES rather than failing.
94
+ * Converting that death into a typed error at the runner's boundary is what makes
95
+ * it actionable: a defect can only be caught, whereas a value can be counted,
96
+ * logged, matched on, and fed to a supervisor that decides between backoff and
97
+ * give-up.
98
+ *
99
+ * **Why the boundary is DRAIN-WIDE**, which is what the name reports. The
100
+ * conversion sits outside the whole `runDrain`, so what arrives is a defect from
101
+ * any stage of it: the subscription's own death, the codec's decode, the read
102
+ * model's `apply`, or a `ProjectionStore` implementation that dies where the port
103
+ * says it should fail. They all end the run identically, and the progress-keyed
104
+ * breaker is the right arbiter for all of them, since it restarts while the
105
+ * checkpoint moves and gives up with a logged `ProjectionStalled` when it does
106
+ * not. Narrowing the conversion to the subscription stage would instead let an
107
+ * `apply` defect escape `Effect.retry` — which sees failures, never defects — and
108
+ * kill the daemon fibre outright: no restart, no `ProjectionStalled`, no logged
109
+ * give-up, and since `runProjection` is infallible, no route to an operator at
110
+ * all. The boundary must equally not WIDEN to `catchAllCause`; the comment at the
111
+ * conversion site sets out why.
112
+ *
113
+ * `defect` is `unknown` because it is somebody else's defect and the runner must
114
+ * not pretend to know its shape.
115
+ */
116
+ export class PipelineDied extends /*#__PURE__*/Data.TaggedError('PipelineDied') {
117
+ /**
118
+ * Why this class fills `message` at all: so the fault renders ITSELF.
119
+ *
120
+ * `Data.TaggedError` sets `name` to the tag and leaves `message` empty, so an
121
+ * unfilled tagged error stringifies to its bare tag — "the pipeline died", with
122
+ * no hint of what killed it, and `PipelineDied: ECONNRESET` is not the same log
123
+ * line. Filled, `Error.prototype.toString` yields `name: message`, so
124
+ * `String(fault)`, `Cause.pretty` (which reads `message` off anything
125
+ * `instanceof Error`) and every log line built from either report the field that
126
+ * says WHY — and no table of tag-to-label outside the class has to be kept in
127
+ * step with the fields inside it.
128
+ *
129
+ * `String(this.defect)` rather than anything cleverer, for exactly the reason
130
+ * `defect` is `unknown` above: plucking a `.message`, sniffing a `_tag` or
131
+ * `JSON.stringify`-ing it would each be the runner pretending to know the shape
132
+ * of somebody else's defect, and the last of them THROWS on a `bigint`.
133
+ *
134
+ * A GETTER rather than a `message` field, which keeps `defect` the single
135
+ * declaration of the fact. Why a prototype accessor is safe here, how far filling
136
+ * the field reaches and the structured `toJSON` shape it deliberately leaves
137
+ * untouched are set out once for all four of these getters on
138
+ * `ProjectionStoreError` in `ProjectionStore.ts`.
139
+ */
140
+ get message() {
141
+ return String(this.defect);
142
+ }
143
+ }
144
+ /**
145
+ * PREPARE: the whole middle of a projection call — the log's durability, the merged
146
+ * query, the construction gate's verdict and the decode stage — between the
147
+ * per-entry RESOLVE above and the per-entry BUILD below.
148
+ *
149
+ * ## Why it is a phase of its own
150
+ *
151
+ * Everything in it is a function of the CALL rather than of any one materialisation,
152
+ * and both entry points ran these same five steps in this same order before it
153
+ * existed. `runProjection` is the N = 1 case: it hands over a singleton entry list
154
+ * and its own preamble and gets back the pair `runProjections` gets back for N.
155
+ *
156
+ * ## What the ONE body is FOR: the gate is consulted before the codec
157
+ *
158
+ * This is worth more than the duplication it removes. `decodeSlices` refuses a slice
159
+ * record carrying two DISTINCT `Schema`s under one event type, and refuses it by
160
+ * THROWING — which inside this `Effect.gen` is a defect, exactly as the gate's
161
+ * verdict is. A record wrong in BOTH ways therefore has two sentences available and
162
+ * nothing but the ORDER of two statements decides which one its author reads. The
163
+ * gate's is the right one: it is the read side's own rule about the wiring in front
164
+ * of that author, where the codec's is about a merge the write path shares and
165
+ * `decode.ts` owns.
166
+ *
167
+ * That order is now a property of ONE body — the gate call above, the `decodeSlices`
168
+ * call below — for both entry points at once. It used to be a claim two function
169
+ * bodies each made in prose, free to drift, and pinned for only one of them. It is
170
+ * pinned for both now: `test/runProjections.test.ts`'s "the GATE is consulted before
171
+ * the codec" case and `test/runner.construction.test.ts`'s sibling each drive a
172
+ * record breaking both rules and assert which sentence comes back. Neither RULE is
173
+ * under test in either — `packages/codec`'s decode suite and
174
+ * `projectionWiringFault.test.ts` own those.
175
+ *
176
+ * ## What the caller keeps
177
+ *
178
+ * The PREAMBLE, and only the preamble: `runProjection` names the projection and its
179
+ * partition, `runProjections` the projection alone, and this function joins whichever
180
+ * it was to the gate's sentence about the RULE with one `': '`. That is the division
181
+ * `projectionWiringFault`'s doc fixes — the gate knows the rule, the entry point
182
+ * knows whose wiring it is — and moving the single `Effect.die` here changes only
183
+ * WHERE the two halves meet, never which half writes which. It is built eagerly, one
184
+ * interpolation per construction, rather than as a thunk: a call that is about to
185
+ * subscribe to an event log does not need that deferred, and a thunk reads worse at
186
+ * both call sites.
187
+ *
188
+ * ## Why both derivations happen HERE
189
+ *
190
+ * The runner needs only the merged QUERY from the composition — the fold is the read
191
+ * model's own `apply` — but it must be the SAME merge a fold would use, so it comes
192
+ * from `core`'s `composeProjections` rather than a local query union that could drift
193
+ * from it. It is also the gate's input, and the gate can only judge it at
194
+ * construction because the union is a pure function of the slices.
195
+ *
196
+ * Both derivations run ONCE per call rather than once per entry or once per restart,
197
+ * so N subscriptions share a single `Query` object and N pipelines a single merged
198
+ * `type -> Schema` table. Sharing the decode stage is safe because the `Serializer`
199
+ * is read from context INSIDE it, per stream, so one stage does not freeze one
200
+ * serialiser across entries. `DecodeStage`'s own doc says which references to that
201
+ * alias hold the inference up, and which do not.
202
+ *
203
+ * The cast erases each slice's own state and event types down to `AnyProjection`. It
204
+ * is sound because `composeProjections` only ever folds a slice through its OWN
205
+ * `evolve`, never across two, so the per-slice pairing the erasure hides is the one
206
+ * thing it cannot violate — and `T`'s precise shape is recovered on the way out, in
207
+ * `CompositeState<T>`. It is unavoidable rather than merely convenient: a slice
208
+ * record is keyed by a caller's literal union, and no signature in `core` is generic
209
+ * over that union AND over each member's state, so the variance has to be discharged
210
+ * somewhere. Once, now, rather than at each entry point.
211
+ *
212
+ * ## Why `EventLogDurability` is read from CONTEXT here
213
+ *
214
+ * Because it is R2's log half, and one log is ONE fact —
215
+ * `EventLogDurability.ts` argues why it is a service declared beside the store layer
216
+ * rather than a field per read model. Reading it once for the CALL is what that
217
+ * argument already wanted, and it is deliberately not part of `resolveProjection`,
218
+ * which is per ENTRY and pure. It is this function's only requirement, and through it
219
+ * the reason both entry points declare one.
220
+ */
221
+ export const prepareProjection = (slices, materialisations, preamble) => Effect.gen(function* () {
222
+ // The LOG's half of R2, from CONTEXT rather than from any read model. Each
223
+ // entry's VIEW half already rode in on the resolved wiring.
224
+ const eventLogDurability = yield* EventLogDurability;
225
+ const composite = composeProjections(slices);
226
+ // ## ASSERT — one gate, one verdict, one `Effect.die`
227
+ //
228
+ // Five rules over four vocabularies, in the order `projectionWiringFault`
229
+ // declares and argues; this function supplies the values and classifies the
230
+ // verdict, and its caller supplied the preamble that says WHOSE wiring is wrong.
231
+ // Both entry points reach this before they fork anything, so a rejected wiring
232
+ // has read no checkpoint and opened no subscription.
233
+ const fault = projectionWiringFault({
234
+ eventLogDurability,
235
+ query: composite.query,
236
+ slices,
237
+ materialisations
238
+ });
239
+ if (fault !== undefined) {
240
+ return yield* Effect.die(new Error(`${preamble}: ${fault}`));
241
+ }
242
+ // BELOW the gate, and that is the ordering this phase exists to own.
243
+ // `decodeSlices` carries the CODEC's own construction refusal — two DISTINCT
244
+ // `Schema`s under one event type — and it THROWS, which here is a second defect;
245
+ // a record wrong in both ways must meet the gate's sentence first. Moving this
246
+ // call above the gate would still die with nothing forked, only with the wrong
247
+ // sentence, which is why the doc above names the two suites that pin it.
248
+ //
249
+ // It takes the RECORD rather than a pre-merged schema table for the same reason
250
+ // the query is composed here: the decode lookup and the subscription query
251
+ // cannot then come from different records.
252
+ const decode = decodeSlices(slices);
253
+ return {
254
+ query: composite.query,
255
+ decode
256
+ };
257
+ });
258
+ /**
259
+ * BUILD and FORK: one resolved wiring plus the three shared derivations in, one
260
+ * supervised daemon out.
261
+ *
262
+ * This is everything the two runner entry points have in common from the gate's
263
+ * verdict onwards — the subscription, the decode, the micro-batching, the guarded
264
+ * commit, the supervisor and the `forkScoped`. It is package-internal and takes no
265
+ * `ReadModel`, so `runProjections` reaches it directly rather than by re-entering
266
+ * `runProjection` per entry, which is what lets one call resolve, judge and derive
267
+ * ONCE and still run N identical pipelines. `read-postgres`'s
268
+ * `test/courseRosterGraduation.test.ts` rests on the two callers running literally
269
+ * the same pipeline, so this function staying the only build step is load-bearing.
270
+ *
271
+ * It requires no `EventLogDurability`. That service is read by `prepareProjection`
272
+ * above — the one place it is read at all — for the gate, and has no part in what
273
+ * runs; the two public entry points DECLARE it only because they run that phase.
274
+ *
275
+ * `query` and `decode` arrive as ARGUMENTS rather than being derived here from a
276
+ * slice record, which is a structural gain rather than a cost: the build step holds
277
+ * no slice record at all, so there is no second record it could derive a different
278
+ * query or a different decode table from. It is the same move `decodeSlices` itself
279
+ * made when it collapsed a two-step protocol into one call.
280
+ */
281
+ export const buildAndFork = (resolved, build) => Effect.gen(function* () {
282
+ const {
283
+ view,
284
+ key,
285
+ batchSize,
286
+ batchWindow,
287
+ restartMinDelay,
288
+ restartMaxDelay,
289
+ restartResetAfter
290
+ } = resolved;
291
+ const store = yield* DcbEventStore;
292
+ /**
293
+ * One run of the pipeline: resolve the resume point, subscribe from it, and
294
+ * drain until the stream ends, is interrupted, or dies.
295
+ *
296
+ * `Effect.scoped` per RUN, not per runner, so a restart releases the previous
297
+ * subscription's resources (the store's listener, its poll fibre) before the
298
+ * next one acquires them — otherwise every restart would leak a subscription
299
+ * into the runner's outer scope.
300
+ */
301
+ const runOnce = Effect.gen(function* () {
302
+ // Re-read on EVERY attempt. This is the resynchronisation: after a lost
303
+ // checkpoint guard the stored position is whatever the winner advanced it
304
+ // to, and the restart picks that up instead of resuming from a stale
305
+ // expectation.
306
+ const stored = yield* view.readCheckpoint;
307
+ // The expectation for the next guarded advance, threaded through the run.
308
+ // A `Ref` rather than a fold accumulator because the batch committer is a
309
+ // `Stream.mapEffect` callback; correctness rests on the single-writer
310
+ // property (concurrency 1), so there is exactly one fibre reading and
311
+ // writing this.
312
+ const expected = yield* Ref.make(stored);
313
+ const commitBatch = batch => Effect.gen(function* () {
314
+ const events = Chunk.toReadonlyArray(batch);
315
+ // Total on the empty chunk. `groupedWithin` is `aggregateWithin` over a
316
+ // `collectAllN` sink on a `spaced` schedule, so a window that elapses
317
+ // with nothing buffered has an empty collection to emit; even if a
318
+ // given version suppressed that, resting the runner's correctness on an
319
+ // undocumented internal would be a bug waiting for an upgrade. There is
320
+ // nothing to apply and no last position, and committing `expected` onto
321
+ // itself would be a pointless write that could lose its guard to a
322
+ // legitimate winner and restart the pipeline for nothing.
323
+ if (!Arr.isNonEmptyReadonlyArray(events)) {
324
+ return;
325
+ }
326
+ // The last event's position, AS IS. Never `expected + 1` and never any
327
+ // other arithmetic: positions are strictly increasing but NOT gapless
328
+ // (a rolled-back append burns ids), so the gap between two delivered
329
+ // events is normal and an increment would invent a position that either
330
+ // never existed or belongs to an event this query does not match.
331
+ const next = Arr.lastNonEmpty(events).sequenced.position;
332
+ const from = yield* Ref.get(expected);
333
+ // The port owns the bracket: the view writes go IN, so the checkpoint
334
+ // advance cannot happen outside the transaction that carries them. A
335
+ // lost guard fails here, which aborts the whole commit and restarts the
336
+ // run — `expected` is deliberately NOT advanced on a failure. There is
337
+ // no `key` argument: the store is bound to one, so the batch cannot be
338
+ // committed against a cursor other than the one it was read from.
339
+ yield* view.commit({
340
+ expected: from,
341
+ next,
342
+ viewWrites: build.apply(events)
343
+ });
344
+ yield* Ref.set(expected, next);
345
+ });
346
+ yield* store.subscribe(build.query, stored).pipe(build.decode, Stream.groupedWithin(batchSize, batchWindow), Stream.mapEffect(commitBatch, {
347
+ concurrency: 1
348
+ }), Stream.runDrain,
349
+ // `catchAllDefect`, NOT `catchAllCause`. The subscription's failure mode
350
+ // is a DEFECT (`subscribe` is `E = never`) and that is what the
351
+ // supervisor needs as a value — but the OTHER way this drain ends is
352
+ // INTERRUPTION, when the enclosing `Scope` closes at shutdown.
353
+ // `catchAllCause` would swallow that too, turning a graceful teardown
354
+ // into a `PipelineDied` that the supervisor would dutifully retry —
355
+ // restarting a subscription inside a scope that is being torn down.
356
+ // `catchAllDefect` re-fails any cause carrying no defect, so interruption
357
+ // propagates untouched and the fibre dies when it is told to.
358
+ //
359
+ // Placed outside the WHOLE drain, so it converts a defect from any stage
360
+ // — decode, `apply`, a store implementation that dies — and not only the
361
+ // subscription's own death; the fault says PIPELINE for that reason.
362
+ // `PipelineDied`'s doc carries the reasoning (in short: they all end the
363
+ // run identically, and a narrower conversion would let an `apply` defect
364
+ // bypass the supervisor entirely).
365
+ Effect.catchAllDefect(defect => new PipelineDied({
366
+ key,
367
+ defect
368
+ })));
369
+ }).pipe(Effect.scoped);
370
+ // ## SUPERVISE — one attempt in, a supervised daemon out
371
+ //
372
+ // What goes with it is the whole of what supervision knows about this runner:
373
+ // the key it reports under, the bound store's `readCheckpoint` as the progress
374
+ // signal, whatever the caller wrote in the UNRESOLVED half of its supervision
375
+ // options, and the three restart durations RESOLVED above.
376
+ //
377
+ // What DOES the work is that those three were resolved ONCE, in
378
+ // `resolveProjection`, so the supervisor receives exactly the figures the gate
379
+ // judged — the argument is on `SuperviseOnProgressOptions` in
380
+ // `superviseOnProgress.ts`, which requires all three for exactly that reason.
381
+ //
382
+ // WHICH FIELDS travel is carried by the TYPES, not by an enumeration of the
383
+ // forwardable ones. `UnresolvedSupervisionOptions` — the type
384
+ // `SuperviseOnProgressOptions` is built from — is `ProjectionSupervisionOptions`
385
+ // MINUS the resolved three, so a new OPTIONAL supervision field lands in it and
386
+ // travels through this call with no edit here, while a new RESOLVED figure lands
387
+ // in `ResolvedSupervisionTuning` instead and leaves the literal below missing a
388
+ // required property. Neither direction rests on a comment or on a test
389
+ // remembering to be extended, which is what the enumeration this replaced could
390
+ // not say.
391
+ //
392
+ // The REST-DESTRUCTURE is the other half, and it is about the OBJECT rather than
393
+ // its type. `build.options` is the caller's own FLAT record, so a raw
394
+ // `restartMinDelay` it wrote is copied by a spread whatever the spread's static
395
+ // type says it holds; discarded here, it is not in the object to be copied at
396
+ // all. Without it the resolved figure would stay on top only while it was written
397
+ // LAST in the literal, and a transposition of two adjacent lines would typecheck
398
+ // and then run the unjudged figure — a rule that has to be remembered is one an
399
+ // edit reading as pure tidying can break.
400
+ //
401
+ // The three discards are the one place a resolved field is named twice, and they
402
+ // cannot silently fall out of step: a fourth resolved figure stops the literal
403
+ // below compiling until it is named there, and its author is then one line from
404
+ // this destructure.
405
+ //
406
+ // The caller's two PIPELINE figures ride along in the copy and are inert: the
407
+ // supervisor reads its options by name and has no field either could reach.
408
+ // Excluding them would be two more discards for nothing.
409
+ //
410
+ // What is destructured is the OPTIONS and not `resolved`: that record is flat and
411
+ // also carries `view`, `viewDurability`, `batchSize` and `batchWindow`, so
412
+ // spreading it wholesale would ride four dead fields into a supervisor's options
413
+ // object — the hazard `runProjections.ts` names where it builds its entry literal
414
+ // explicitly.
415
+ //
416
+ // `?? {}` rather than the conditional spreads this replaced.
417
+ // `exactOptionalPropertyTypes` bars setting an optional key to an explicit
418
+ // `undefined`, and this sets none: spreading an object contributes only the keys
419
+ // it has, and an absent whole options object contributes none at all.
420
+ //
421
+ // The progress signal goes in as a VALUE, so nothing has been read from the
422
+ // view store yet: a runner rejected above still has not touched it.
423
+ //
424
+ // Nothing is handed in for RENDERING the runner's faults on the supervisor's
425
+ // log lines. All four set `message`, so `String(fault)` is
426
+ // `Error.prototype.toString` — `'ProjectionStoreError: ECONNRESET'` — and each
427
+ // fault labels itself wherever it is reported, from the field that says why.
428
+ // `CheckpointSuperseded`'s is the interesting one and its own doc carries the
429
+ // argument: its `expected` is a `bigint`, but a `bigint` interpolated into a
430
+ // `string` is digits, so the value class the log reduction below keeps away from
431
+ // a serialiser never reaches one through `message`.
432
+ //
433
+ // The residual error is `ProjectionStalled` and nothing else: the schedule
434
+ // never terminates on its own, so the only way out with a failure is the
435
+ // breaker giving up. `superviseOnProgress` carries that reasoning.
436
+ const {
437
+ restartMinDelay: _rawMinDelay,
438
+ restartMaxDelay: _rawMaxDelay,
439
+ restartResetAfter: _rawResetAfter,
440
+ ...supervisionOverrides
441
+ } = build.options ?? {};
442
+ const daemon = superviseOnProgress(runOnce, {
443
+ key,
444
+ progress: view.readCheckpoint,
445
+ ...supervisionOverrides,
446
+ restartMinDelay,
447
+ restartMaxDelay,
448
+ restartResetAfter
449
+ });
450
+ // ## FORK
451
+ //
452
+ // `forkScoped`, so the daemon's lifetime IS the caller's scope: no manual
453
+ // shutdown handle to forget, and scope close interrupts and awaits the fibre
454
+ // deterministically. A plain `Effect.fork` would tie it to the calling
455
+ // fibre's lifetime, which for a wiring effect that returns immediately would
456
+ // kill the daemon on the spot.
457
+ const fibre = yield* Effect.forkScoped(daemon);
458
+ // The BOUND view goes back with the fibre, which is what lets a returned handle
459
+ // name its own cursor. It is the same object the pipeline above commits through,
460
+ // not a re-binding: `runProjections` resolved it from a store its caller never
461
+ // bound, and equal keys across N entries leave it as the only discriminator.
462
+ return {
463
+ key,
464
+ store: view,
465
+ fibre
466
+ };
467
+ });
468
+ //# sourceMappingURL=ProjectionRunner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ProjectionRunner.js","names":["decodeSlices","composeProjections","DcbEventStore","Array","Arr","Chunk","Data","Effect","Ref","Schema","Stream","EventLogDurability","projectionWiringFault","DEFAULT_RESTART_MAX_DELAY","DEFAULT_RESTART_MIN_DELAY","DEFAULT_RESTART_RESET_AFTER","superviseOnProgress","BatchSize","Int","pipe","positive","brand","DEFAULT_BATCH_SIZE","make","DEFAULT_BATCH_WINDOW","resolveProjection","view","options","key","viewDurability","durability","batchSize","batchWindow","restartMinDelay","restartMaxDelay","restartResetAfter","PipelineDied","TaggedError","message","String","defect","prepareProjection","slices","materialisations","preamble","gen","eventLogDurability","composite","fault","query","undefined","die","Error","decode","buildAndFork","resolved","build","store","runOnce","stored","readCheckpoint","expected","commitBatch","batch","events","toReadonlyArray","isNonEmptyReadonlyArray","next","lastNonEmpty","sequenced","position","from","get","commit","viewWrites","apply","set","subscribe","groupedWithin","mapEffect","concurrency","runDrain","catchAllDefect","scoped","_rawMinDelay","_rawMaxDelay","_rawResetAfter","supervisionOverrides","daemon","progress","fibre","forkScoped"],"sources":["../../src/ProjectionRunner.ts"],"sourcesContent":[null],"mappings":"AAqFA,SAASA,YAAY,QAAQ,kBAAkB;AAC/C,SAEEC,kBAAkB,EAClBC,aAAa,QAIR,iBAAiB;AACxB,SACEC,KAAK,IAAIC,GAAG,EACZC,KAAK,EACLC,IAAI,EAEJC,MAAM,EAENC,GAAG,EACHC,MAAM,EAENC,MAAM,QACD,QAAQ;AACf,SAASC,kBAAkB,QAAQ,yBAAsB;AAQzD,SAEEC,qBAAqB,QAChB,4BAAyB;AAChC,SACEC,yBAAyB,EACzBC,yBAAyB,EACzBC,2BAA2B,EAI3BC,mBAAmB,QACd,0BAAuB;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,OAAO,MAAMC,SAAS,gBAAGR,MAAM,CAACS,GAAG,CAACC,IAAI,cACtCV,MAAM,CAACW,QAAQ,EAAE,eACjBX,MAAM,CAACY,KAAK,CAAC,WAAW,CAAC,CAC1B;AAGD;;;;;AAKA,MAAMC,kBAAkB,gBAAcL,SAAS,CAACM,IAAI,CAAC,GAAG,CAAC;AAEzD;;;;;AAKA,MAAMC,oBAAoB,GAA2B,WAAW;AAgIhE;;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,OAAO,MAAMC,iBAAiB,GAAGA,CAC/BC,IAA0B,EAC1BC,OAAiC,MACT;EACxBD,IAAI;EACJE,GAAG,EAAEF,IAAI,CAACE,GAAG;EACbC,cAAc,EAAEH,IAAI,CAACI,UAAU;EAC/BC,SAAS,EAAEJ,OAAO,EAAEI,SAAS,IAAIT,kBAAkB;EACnDU,WAAW,EAAEL,OAAO,EAAEK,WAAW,IAAIR,oBAAoB;EACzDS,eAAe,EAAEN,OAAO,EAAEM,eAAe,IAAInB,yBAAyB;EACtEoB,eAAe,EAAEP,OAAO,EAAEO,eAAe,IAAIrB,yBAAyB;EACtEsB,iBAAiB,EAAER,OAAO,EAAEQ,iBAAiB,IAAIpB;CAClD,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,OAAM,MAAOqB,YAAa,sBAAQ9B,IAAI,CAAC+B,WAAW,CAAC,cAAc,CAG/D;EACA;;;;;;;;;;;;;;;;;;;;;;;EAuBA,IAAaC,OAAOA,CAAA;IAClB,OAAOC,MAAM,CAAC,IAAI,CAACC,MAAM,CAAC;EAC5B;;AAqJF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6EA,OAAO,MAAMC,iBAAiB,GAAGA,CAC/BC,MAAS,EACTC,gBAAsD,EACtDC,QAAgB,KAEhBrC,MAAM,CAACsC,GAAG,CAAC,aAAS;EAClB;EACA;EACA,MAAMC,kBAAkB,GAAG,OAAOnC,kBAAkB;EAEpD,MAAMoC,SAAS,GAAG9C,kBAAkB,CAClCyC,MAAuC,CACxC;EAED;EACA;EACA;EACA;EACA;EACA;EACA;EACA,MAAMM,KAAK,GAAGpC,qBAAqB,CAAC;IAClCkC,kBAAkB;IAClBG,KAAK,EAAEF,SAAS,CAACE,KAAK;IACtBP,MAAM;IACNC;GACD,CAAC;EACF,IAAIK,KAAK,KAAKE,SAAS,EAAE;IACvB,OAAO,OAAO3C,MAAM,CAAC4C,GAAG,CAAC,IAAIC,KAAK,CAAC,GAAGR,QAAQ,KAAKI,KAAK,EAAE,CAAC,CAAC;EAC9D;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,MAAMK,MAAM,GAAkCrD,YAAY,CAAC0C,MAAM,CAAC;EAElE,OAAO;IAAEO,KAAK,EAAEF,SAAS,CAACE,KAAK;IAAEI;EAAM,CAAE;AAC3C,CAAC,CAAC;AAEJ;;;;;;;;;;;;;;;;;;;;;;;AAuBA,OAAO,MAAMC,YAAY,GAAGA,CAC1BC,QAA4B,EAC5BC,KAOC,KAMDjD,MAAM,CAACsC,GAAG,CAAC,aAAS;EAClB,MAAM;IACJnB,IAAI;IACJE,GAAG;IACHG,SAAS;IACTC,WAAW;IACXC,eAAe;IACfC,eAAe;IACfC;EAAiB,CAClB,GAAGoB,QAAQ;EACZ,MAAME,KAAK,GAAG,OAAOvD,aAAa;EAElC;;;;;;;;;EASA,MAAMwD,OAAO,GAITnD,MAAM,CAACsC,GAAG,CAAC,aAAS;IACtB;IACA;IACA;IACA;IACA,MAAMc,MAAM,GAAG,OAAOjC,IAAI,CAACkC,cAAc;IAEzC;IACA;IACA;IACA;IACA;IACA,MAAMC,QAAQ,GAAG,OAAOrD,GAAG,CAACe,IAAI,CAACoC,MAAM,CAAC;IAExC,MAAMG,WAAW,GACfC,KAAgC,IAMhCxD,MAAM,CAACsC,GAAG,CAAC,aAAS;MAClB,MAAMmB,MAAM,GAAG3D,KAAK,CAAC4D,eAAe,CAACF,KAAK,CAAC;MAE3C;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA,IAAI,CAAC3D,GAAG,CAAC8D,uBAAuB,CAACF,MAAM,CAAC,EAAE;QACxC;MACF;MAEA;MACA;MACA;MACA;MACA;MACA,MAAMG,IAAI,GAAG/D,GAAG,CAACgE,YAAY,CAACJ,MAAM,CAAC,CAACK,SAAS,CAACC,QAAQ;MACxD,MAAMC,IAAI,GAAG,OAAO/D,GAAG,CAACgE,GAAG,CAACX,QAAQ,CAAC;MAErC;MACA;MACA;MACA;MACA;MACA;MACA,OAAOnC,IAAI,CAAC+C,MAAM,CAAC;QACjBZ,QAAQ,EAAEU,IAAI;QACdJ,IAAI;QACJO,UAAU,EAAElB,KAAK,CAACmB,KAAK,CAACX,MAAM;OAC/B,CAAC;MAEF,OAAOxD,GAAG,CAACoE,GAAG,CAACf,QAAQ,EAAEM,IAAI,CAAC;IAChC,CAAC,CAAC;IAEJ,OAAOV,KAAK,CAACoB,SAAS,CAACrB,KAAK,CAACP,KAAK,EAAEU,MAAM,CAAC,CAACxC,IAAI,CAC9CqC,KAAK,CAACH,MAAM,EACZ3C,MAAM,CAACoE,aAAa,CAAC/C,SAAS,EAAEC,WAAW,CAAC,EAC5CtB,MAAM,CAACqE,SAAS,CAACjB,WAAW,EAAE;MAAEkB,WAAW,EAAE;IAAC,CAAE,CAAC,EACjDtE,MAAM,CAACuE,QAAQ;IACf;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA1E,MAAM,CAAC2E,cAAc,CAAE1C,MAAM,IAAK,IAAIJ,YAAY,CAAC;MAAER,GAAG;MAAEY;IAAM,CAAE,CAAC,CAAC,CACrE;EACH,CAAC,CAAC,CAACrB,IAAI,CAACZ,MAAM,CAAC4E,MAAM,CAAC;EAEtB;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,MAAM;IACJlD,eAAe,EAAEmD,YAAY;IAC7BlD,eAAe,EAAEmD,YAAY;IAC7BlD,iBAAiB,EAAEmD,cAAc;IACjC,GAAGC;EAAoB,CACxB,GAA4B/B,KAAK,CAAC7B,OAAO,IAAI,EAAE;EAEhD,MAAM6D,MAAM,GAIRxE,mBAAmB,CAAC0C,OAAO,EAAE;IAC/B9B,GAAG;IACH6D,QAAQ,EAAE/D,IAAI,CAACkC,cAAc;IAC7B,GAAG2B,oBAAoB;IACvBtD,eAAe;IACfC,eAAe;IACfC;GACD,CAAC;EAEF;EACA;EACA;EACA;EACA;EACA;EACA;EACA,MAAMuD,KAAK,GAAG,OAAOnF,MAAM,CAACoF,UAAU,CAACH,MAAM,CAAC;EAE9C;EACA;EACA;EACA;EACA,OAAO;IAAE5D,GAAG;IAAE6B,KAAK,EAAE/B,IAAI;IAAEgE;EAAK,CAAE;AACpC,CAAC,CAAC","ignoreList":[]}