@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,587 @@
1
+ /**
2
+ * `superviseOnProgress` — the projection runner's SUPERVISOR: retry an effect for
3
+ * as long as an EXTERNALLY OBSERVED progress signal keeps moving, and give up when
4
+ * it stops.
5
+ *
6
+ * ## The mechanism
7
+ *
8
+ * A daemon that fails has to be restarted, and something has to decide when
9
+ * restarting has become pointless. The usual answer counts failures, which
10
+ * cannot tell "this keeps failing and nothing is happening" from "this keeps
11
+ * failing and the work is getting done anyway" — and those two want opposite
12
+ * decisions. So this module keys the decision on a SIGNAL the supervised effect
13
+ * does not own: a `Position` read from somewhere else, monotone by construction,
14
+ * whose movement means work landed no matter who landed it. Two mechanisms, kept
15
+ * deliberately apart:
16
+ *
17
+ * - **The schedule decides HOW FAST to restart** — a capped, jittered,
18
+ * sawtoothing exponential that never terminates on its own.
19
+ * - **The circuit-breaker decides WHETHER to keep restarting** — by re-reading
20
+ * `progress` after every failure and counting only the CONSECUTIVE restarts
21
+ * that moved it not at all.
22
+ *
23
+ * Splitting them is what makes a competing writer behave. Two processes
24
+ * accidentally maintaining one materialisation — a deploy overlapping its
25
+ * predecessor, one store passed twice — both fail on every batch, the loser's
26
+ * guarded checkpoint advance being superseded every time; but the WINNER keeps
27
+ * advancing the shared stored position, so every one of the loser's restarts
28
+ * observes movement, its counter resets, and it resynchronises from the winner's
29
+ * position for as long as the winner keeps working: bounded duplicate effort, at
30
+ * the schedule's rate, with no hot loop and no spurious give-up. Only a signal
31
+ * that is genuinely stuck — a poison event whose apply always fails, a
32
+ * persistently broken view store — trips the breaker and fails
33
+ * `ProjectionStalled`.
34
+ *
35
+ * "Accidentally" is the word and it is doing work, because ONE read model
36
+ * materialised N ways is a DELIBERATE shape (`runProjections`) and is not this case
37
+ * at all. Those N runners share a `ProjectionId` but each owns its own checkpoint in
38
+ * its OWN store, so they never contend, no guard is ever lost between them, and
39
+ * nothing here is being relied on to make them safe. The tolerated fault above is
40
+ * two writers over ONE cursor, which is what the construction gate's collision rung
41
+ * refuses when it is a wiring rather than an overlapping deploy — this mechanism
42
+ * degrades gracefully under it and is deliberately not the place it is caught.
43
+ *
44
+ * ## Two observability channels, and which one is primary
45
+ *
46
+ * Every supervision outcome is reported twice: to a HOOK — `onRestart` on every
47
+ * restart, `onStalled` once at the give-up — and to a LOG LINE beside it. The
48
+ * hooks are the primary channel and carry the fault ITSELF, untouched. The log
49
+ * lines are the thin lossy default: every annotation on them is a string or a
50
+ * number — JSON-SAFE, that is, rather than uniformly stringy, because
51
+ * `effect@3.22`'s `Logger.json` throws outright on a `bigint` nested in an
52
+ * annotation value and `CheckpointSuperseded` carries one (the measurement, for
53
+ * each shipped logger, is at the annotations in `shouldRestart`). The one numeric
54
+ * annotation, `noProgressRestarts`, stays numeric on purpose: a serialiser has no
55
+ * quarrel with it and an aggregator can filter and graph on it, both of which a
56
+ * `String` around it would give up for nothing. The fault goes out through
57
+ * `String`, which is to say through the FAULT: `Error.prototype.toString` is
58
+ * `name: message`, so a fault with something to say sets `message` — as all four of
59
+ * the runner's own faults do, `CheckpointSuperseded` included, its `bigint`
60
+ * `expected` interpolated into a `string` and so hidden from every serialiser — and
61
+ * one with nothing to say beyond its tag renders as its tag. The supervisor needs
62
+ * no knowledge of the supervised effect's `E` to label it — it is generic in `E` —
63
+ * and no caller has to supply anything for the purpose.
64
+ *
65
+ * ## Why it is a module of its own, and why it is PACKAGE-INTERNAL
66
+ *
67
+ * Not because the mechanism is on offer for reuse. It is deliberately absent from
68
+ * `@kairos-es/read`'s entry point, and its signature says what it supervises: a
69
+ * `CheckpointKey` names the supervised thing, a `Position` is the signal, a
70
+ * `ProjectionStoreError` is what reading that signal can fail with, and
71
+ * `ProjectionStalled` is a projection-shaped failure on
72
+ * `ProjectionRunner.fibre`'s error channel.
73
+ *
74
+ * It is separate because it is the hardest thing in this package to test THROUGH
75
+ * the pipeline, and the repo has the before and after. Pinning the five-minute
76
+ * default no-progress budget — a quantity about supervision and nothing else —
77
+ * cost ~122 lines while the mechanism lived inside `runProjection`: an in-memory
78
+ * event store, a view store decorated never to commit, a read model, a log-capture
79
+ * layer, and a hand-driven loop stepping the clock some 1200 times. Against a stub
80
+ * it costs 52, and 22 of those are code: one `Effect` yielding a `Position`, one
81
+ * effect that fails, one `TestClock.adjust`, and the rest of the case is the
82
+ * arithmetic it asserts written out. (The figure is re-measured rather than
83
+ * inherited — it read ~37 until the three restart `Duration`s became explicit
84
+ * arguments and the case gained a comment saying why. The ratio is what the
85
+ * argument turns on, and that has not moved.) Every other subtle behaviour is
86
+ * pinned the same way — the
87
+ * `ORIGIN` seeding, the breaker being consulted before the sleep, the interruption
88
+ * trap on the re-read, the counter resetting on progress — because this module
89
+ * knows nothing of `subscribe`, of decoding, of micro-batching, of committing, of
90
+ * slices or of the `Serializer`. Those cases are three suites over one stub
91
+ * (`test/superviseOnProgress.{pacing,breaker,observability}.test.ts`, sharing
92
+ * `test/superviseOnProgress.fixture.ts`), split along the two mechanisms this
93
+ * section keeps apart plus the channel their outcomes leave through. A SEPARATE
94
+ * module bought all of that; a GENERAL one was never needed for any of it.
95
+ *
96
+ * And there is no second consumer here, nor one in prospect. The mechanism needs a
97
+ * caller with a failing effect worth restarting AND a progress signal owned by
98
+ * somebody else; in this library only a projection has both. A command runner's
99
+ * progress IS its own success. The store's poll retry lives in
100
+ * `@kairos-es/store-postgres` and already has a bounded retry with no supervisor
101
+ * over it. A lease renewer for competing consumers is out of scope by ADR-0007.
102
+ *
103
+ * Should such a consumer appear, generalising then is a smaller change than
104
+ * maintaining the claim now — so the cost is recorded here rather than
105
+ * re-derived: `key` becomes a bare `string` label, losing the two discrete
106
+ * `projection`/`partition` annotations an operator greps on (or forcing a generic
107
+ * annotation shape onto every caller); `Position`'s `!==` becomes an
108
+ * `Equivalence` on the signal type; `ProjectionStoreError` becomes a fourth type
109
+ * parameter, and collapsing it to `unknown` instead is what would cost the
110
+ * interruption trap its argument, since "`catchAll` touches only the typed
111
+ * `ProjectionStoreError`" stops being a claim a reader can check against the
112
+ * signature; and `ProjectionStalled` has to be renamed or made generic.
113
+ *
114
+ * ## Platform-neutral
115
+ *
116
+ * No `node:*`, no `Date`, no host RNG: the backoff is `Duration`s scheduled
117
+ * through the `Clock` and the jitter is Effect's `Random` service, so
118
+ * `TestClock` drives all of it.
119
+ */
120
+ import { type Position } from '@kairos-es/core';
121
+ import { type Duration, Effect, Schema } from 'effect';
122
+ import type { CheckpointKey, ProjectionStoreError } from './ProjectionStore.js';
123
+ /**
124
+ * Consecutive no-progress restarts before the supervisor gives up: a branded
125
+ * POSITIVE integer.
126
+ *
127
+ * Branded because at `0` the breaker's `restarts >= maxNoProgressRestarts` test
128
+ * is true on the FIRST failure, so the supervisor gives up before it has retried
129
+ * once and before `onRestart` has ever fired — a `ProjectionStalled` for a fault
130
+ * that a single 100-millisecond retry would have cleared, and the flat
131
+ * contradiction of the five-minute budget `DEFAULT_MAX_NO_PROGRESS_RESTARTS`
132
+ * documents. Negative and fractional counts fail the same test the same way.
133
+ *
134
+ * `>= 1` rather than `>= 0` for that reason: "give up immediately" is not a
135
+ * supervision policy this module offers, and a caller who wants one has
136
+ * `Fiber.interrupt`.
137
+ *
138
+ * A distinct brand from `BatchSize` despite the identical refinement, because
139
+ * the two are still writable side by side in one flat options literal, are both
140
+ * bare integers, and differ by an order of magnitude in meaning — nominality is
141
+ * what makes a transposition fail to typecheck rather than silently retune the
142
+ * runner. That the two now tune different mechanisms in different modules is a
143
+ * second reason for the distinction, not a replacement for the first.
144
+ *
145
+ * Construct with `MaxNoProgressRestarts.make(n)` — and retune it by the WALL
146
+ * CLOCK the figure buys, per `DEFAULT_MAX_NO_PROGRESS_RESTARTS`.
147
+ */
148
+ export declare const MaxNoProgressRestarts: Schema.brand<Schema.filter<typeof Schema.Int>, "MaxNoProgressRestarts">;
149
+ export type MaxNoProgressRestarts = typeof MaxNoProgressRestarts.Type;
150
+ /**
151
+ * Fast enough that a transient blip is invisible, slow enough not to spin.
152
+ *
153
+ * Exported — like the two below and unlike `DEFAULT_MAX_NO_PROGRESS_RESTARTS` —
154
+ * because `resolveProjection` is what RESOLVES it, for both entry points: the
155
+ * construction gate judges the
156
+ * figure that will actually run, so the resolution has to happen before the
157
+ * supervisor is built and the default has to be readable from there. The
158
+ * documentation stays HERE, against the sawtooth arithmetic the three of them
159
+ * define together, because that is what the figure is chosen for.
160
+ */
161
+ export declare const DEFAULT_RESTART_MIN_DELAY: Duration.DurationInput;
162
+ /**
163
+ * The store's own poll retry exhausts after roughly 40 seconds of sustained
164
+ * transient faults before `subscribe` dies, so a 30-second ceiling keeps a
165
+ * restart in the same order of magnitude as the fault it is recovering from —
166
+ * long enough not to hammer a struggling database, short enough that recovery is
167
+ * not measured in minutes.
168
+ *
169
+ * Exported for the reason `DEFAULT_RESTART_MIN_DELAY` gives.
170
+ */
171
+ export declare const DEFAULT_RESTART_MAX_DELAY: Duration.DurationInput;
172
+ /**
173
+ * A minute of ELAPSED retrying, after which the pacing starts over from
174
+ * `restartMinDelay`.
175
+ *
176
+ * `Schedule.resetAfter` keys on `Schedule.elapsed` — the time since the schedule
177
+ * (or its own last reset) began, NOT the time since the last decision, verified
178
+ * against `effect@3.22`'s `internal/schedule.ts` — and both readings of that are
179
+ * wanted. A run that stays healthy for a minute pushes `elapsed` past the
180
+ * threshold on its way to the next fault, so its backoff starts from
181
+ * `restartMinDelay` again instead of inheriting a ceiling from a fault it
182
+ * recovered from hours ago. And a SUSTAINED outage, whose attempts fail fast,
183
+ * sawtooths back down to `restartMinDelay` every cycle rather than pinning at
184
+ * `restartMaxDelay` for ever, so a store that comes back is noticed within
185
+ * `restartMinDelay` rather than up to `restartMaxDelay` later.
186
+ *
187
+ * Exported for the reason `DEFAULT_RESTART_MIN_DELAY` gives.
188
+ */
189
+ export declare const DEFAULT_RESTART_RESET_AFTER: Duration.DurationInput;
190
+ /**
191
+ * How the supervisor is tuned: how fast it restarts, how long it keeps trying,
192
+ * and where each of its two OUTCOMES is reported — a restart to `onRestart`, a
193
+ * give-up to `onStalled`.
194
+ *
195
+ * Separated from the pipeline's own tuning (`ProjectionPipelineOptions`:
196
+ * `batchSize`, `batchWindow`) because they are dials on different machines —
197
+ * one sizes a transaction and a latency ceiling, the other sizes a RECOVERY
198
+ * POLICY measured in minutes of outage — and reading them as one undifferentiated
199
+ * bag is how `maxNoProgressRestarts` gets retuned as though it were a throughput
200
+ * knob. `ProjectionRunnerOptions` still composes the two into one flat object, so
201
+ * no call site pays for the distinction; the types are what carry it.
202
+ *
203
+ * Every figure is a build-time DEFAULT, not a contract: the mechanisms are fixed
204
+ * by ADR-0007, the numbers are judgement calls sized against
205
+ * `@kairos-es/store-postgres`'s own defaults and are meant to be overridden by a
206
+ * deployment that knows its own latency budget (and by tests, which want tiny
207
+ * delays).
208
+ *
209
+ * A judgement call is still not a free choice, though: zero, negative,
210
+ * fractional and infinite figures are not "aggressive tuning", they are wirings
211
+ * whose failure mode is a hot loop or a permanently stalled daemon with nothing
212
+ * on any error channel. So this surface fails at its boundary, by TWO mechanisms
213
+ * because the field types differ — the count is BRANDED, so a bad value is
214
+ * refused at the caller's own `.make(...)`, and the three durations are judged by
215
+ * the read side's construction gate (`projectionWiringFault`, which says why they
216
+ * cannot be branded), called by `runProjection` and `runProjections` alike before a
217
+ * supervisor, an attempt or a fibre exists, where the fault lands as a defect exactly as
218
+ * `assertServableQuery`'s does.
219
+ *
220
+ * Neither mechanism is this module's own, and that is deliberate: by the time a
221
+ * figure reaches `superviseOnProgress` it has been resolved and judged, which is
222
+ * why the three appear again as REQUIRED fields on `SuperviseOnProgressOptions`.
223
+ */
224
+ export interface ProjectionSupervisionOptions {
225
+ /**
226
+ * First restart delay. Default `'100 millis'`. Positive and finite, checked at
227
+ * construction — it is the base `Schedule.exponential` multiplies, so a zero
228
+ * here zeroes the whole backoff.
229
+ */
230
+ readonly restartMinDelay?: Duration.DurationInput;
231
+ /**
232
+ * Restart delay ceiling. Default `'30 seconds'` — the same order of magnitude
233
+ * as the store's own ~40-second poll-retry budget, so the runner does not back
234
+ * off far past the lifetime of the fault it is recovering from.
235
+ *
236
+ * Positive, finite, and not below `restartMinDelay`; all three are checked at
237
+ * construction.
238
+ */
239
+ readonly restartMaxDelay?: Duration.DurationInput;
240
+ /**
241
+ * How long a run must stay up before the backoff resets to `restartMinDelay`.
242
+ * Default `'60 seconds'`. Without this, a projection that hits one fault an hour
243
+ * would still be waiting `restartMaxDelay` a day later.
244
+ *
245
+ * Positive and finite, checked at construction: both degenerate ends dissolve
246
+ * the sawtooth that `DEFAULT_MAX_NO_PROGRESS_RESTARTS`' budget is computed
247
+ * against.
248
+ */
249
+ readonly restartResetAfter?: Duration.DurationInput;
250
+ /**
251
+ * Consecutive restarts that moved the progress signal NOT AT ALL before the
252
+ * supervisor gives up with `ProjectionStalled`. Default `40`, which on the
253
+ * default pacing is about five minutes of retrying — long enough that a view
254
+ * store that is merely unavailable is back inside it.
255
+ *
256
+ * Retune it by the WALL CLOCK it buys rather than by the count: with the
257
+ * sawtoothing backoff the two are not proportional, and the arithmetic is set
258
+ * out on `DEFAULT_MAX_NO_PROGRESS_RESTARTS`.
259
+ *
260
+ * A branded positive integer: write `MaxNoProgressRestarts.make(80)`, not
261
+ * `80`.
262
+ */
263
+ readonly maxNoProgressRestarts?: MaxNoProgressRestarts;
264
+ /**
265
+ * Supervision observability seam for the NON-TERMINAL outcome: called on every
266
+ * restart, BEFORE the backoff sleep, so a metric or a trace records the restart
267
+ * at the moment it is decided rather than after the delay.
268
+ *
269
+ * THE PRIMARY CHANNEL for a restart, not a supplement to the log line beside
270
+ * it. `reason` is the RAW fault; the `reason` on the log line is that fault
271
+ * `String`ed, because a serialiser cannot take the `bigint` a
272
+ * `CheckpointSuperseded` carries (the measurement is at the annotations, in
273
+ * `shouldRestart`). So anything that needs to match on a tag, read a payload or
274
+ * count by fault type takes this hook, and the log line is the lossy convenience
275
+ * default for everyone else.
276
+ *
277
+ * Infallible and requirement-free by signature, so a hook cannot FAIL the
278
+ * daemon or widen the supervised effect's requirements. It carries no
279
+ * `position`, unlike `onStalled`: a restart is about to re-read the signal
280
+ * anyway, so where it stood at the restart is a number in flight rather than a
281
+ * fact. `onStalled` states the full rule the two share, including what happens
282
+ * when a hook dies.
283
+ */
284
+ readonly onRestart?: (info: {
285
+ readonly key: CheckpointKey;
286
+ readonly reason: unknown;
287
+ readonly noProgressRestarts: number;
288
+ }) => Effect.Effect<void>;
289
+ /**
290
+ * Supervision observability seam for the TERMINAL outcome: called ONCE, at the
291
+ * give-up, immediately after the error is logged and immediately before
292
+ * `ProjectionStalled` is raised. An application can fail a health check, exit
293
+ * non-zero, or page somebody.
294
+ *
295
+ * ## WHY, given `onRestart` already exists — the restart is the metric, the
296
+ * stall is the page
297
+ *
298
+ * The two outcomes are not equally important, and the asymmetry runs the
299
+ * OPPOSITE way to the one the older surface implied. A restart RESOLVES ITSELF:
300
+ * it is a rate to graph, and to alert on only if it climbs. A give-up does not —
301
+ * `ProjectionStalled` means a poison event or a view store broken rather than
302
+ * merely unavailable, and its own doc says both need a human. Yet under the
303
+ * wiring this library recommends (`projectionLayer`, which is
304
+ * `Layer.scopedDiscard`, so the daemon fibre is discarded ON PURPOSE — a
305
+ * projection is a background process and nothing should depend on it as a
306
+ * service) the terminal event's only route out of the process was the
307
+ * `Effect.logError` beside this call. A seam for the event that fixes itself and
308
+ * none for the event that does not is backwards; this is the missing half.
309
+ *
310
+ * ## The payload is the `ProjectionStalled` about to be raised
311
+ *
312
+ * The same four fields, carrying the same values, so a hook and a caller
313
+ * awaiting the daemon see one story rather than two. `position` is the STUCK
314
+ * signal — the position everything is failing just after, so the offending event
315
+ * is one query away — `noProgressRestarts` the budget that was spent, and
316
+ * `reason` the last underlying failure as the RAW fault rather than the reduced
317
+ * log label, because a programmatic callback can inspect a `bigint` where a log
318
+ * line cannot.
319
+ *
320
+ * With `onRestart` that makes the hooks the PRIMARY observability channel for
321
+ * both supervision outcomes, and the two log lines the thin lossy default: every
322
+ * value on those is a string or a number — JSON-SAFE, by measurement rather than
323
+ * by superstition (see the annotations in `shouldRestart`), which is the real
324
+ * bound and not "stringify everything" — whereas a hook gets the fault untouched.
325
+ *
326
+ * ## Infallible by signature, NOT defect-trapped — the same rule as `onRestart`
327
+ *
328
+ * `Effect<void>`, so a hook can neither fail the daemon nor widen the supervised
329
+ * effect's requirements. A DEFECT from a hook is a different matter and is
330
+ * deliberately left alone: it takes the fibre down exactly as one from
331
+ * `onRestart` does, because absorbing it would hide a bug in the application's
332
+ * own pager on the one path whose entire job is to make failures visible. What
333
+ * makes that safe is the ORDER — the `Effect.logError` has already landed by the
334
+ * time this is called, so the guaranteed route to an operator is never hostage
335
+ * to the hook, and a broken hook surfaces as a further defect rather than as
336
+ * silence. The one cost is that such a fibre dies with the hook's defect instead
337
+ * of failing `ProjectionStalled`, which is the honest report: the stall was
338
+ * announced, and then the announcer broke.
339
+ */
340
+ readonly onStalled?: (info: {
341
+ readonly key: CheckpointKey;
342
+ readonly noProgressRestarts: number;
343
+ readonly position: Position;
344
+ readonly reason: unknown;
345
+ }) => Effect.Effect<void>;
346
+ }
347
+ /**
348
+ * The three restart figures that are RESOLVED before a supervisor exists:
349
+ * `resolveProjection` applies each default and the construction gate judges the
350
+ * result, so what reaches `superviseOnProgress` has been both settled and checked.
351
+ *
352
+ * A type of its own rather than three fields restated wherever they are needed,
353
+ * because it is the BOUNDARY between the two halves of the surface above and both
354
+ * halves are computed from it: `SuperviseOnProgressOptions` requires exactly these,
355
+ * and `UnresolvedSupervisionOptions` below is everything else. `ResolvedProjection`
356
+ * in `ProjectionRunner.ts` EXTENDS it, so "which figures the resolve step owns" is
357
+ * ONE declaration serving both ends rather than two that happen to agree today — a
358
+ * fourth resolved figure added here is a compile error in the resolve step until it
359
+ * produces one, and another at the forwarding call site until it names one.
360
+ *
361
+ * PACKAGE-INTERNAL, like `SuperviseOnProgressOptions` itself. The half a caller
362
+ * writes is `ProjectionSupervisionOptions`, where all three stay optional.
363
+ */
364
+ export interface ResolvedSupervisionTuning {
365
+ /** First restart delay, already resolved and judged. */
366
+ readonly restartMinDelay: Duration.DurationInput;
367
+ /** Restart delay ceiling, already resolved and judged. */
368
+ readonly restartMaxDelay: Duration.DurationInput;
369
+ /** Backoff reset threshold, already resolved and judged. */
370
+ readonly restartResetAfter: Duration.DurationInput;
371
+ }
372
+ /**
373
+ * The half of `ProjectionSupervisionOptions` that NOTHING resolves — the two hooks
374
+ * and the branded count — forwarded to the supervisor exactly as the caller wrote
375
+ * it.
376
+ *
377
+ * Defined by SUBTRACTION rather than by listing its members, and that is the whole
378
+ * value of it. `buildAndFork` spreads the forwardable remainder of the caller's own
379
+ * options — this type plus the two PIPELINE figures, which are inert here because
380
+ * nothing below reads them by name — and names the resolved three beside it, so the
381
+ * split between the two halves is carried by the types: a new OPTIONAL supervision
382
+ * field lands in `ProjectionSupervisionOptions`, arrives here
383
+ * automatically and travels with that spread untouched, while a new RESOLVED figure
384
+ * lands in `ResolvedSupervisionTuning`, drops OUT of this type, and leaves the spread
385
+ * unable to supply it — a missing required property at that one call site, rather
386
+ * than an unjudged raw value winning silently on spread order.
387
+ *
388
+ * `Omit` rather than a second `extends` clause, and not by preference: an interface
389
+ * cannot extend both `ProjectionSupervisionOptions` and `ResolvedSupervisionTuning`,
390
+ * because the same property optional in one parent and required in the other is a
391
+ * conflict TypeScript refuses outright (TS2320, "not identical").
392
+ */
393
+ export type UnresolvedSupervisionOptions = Omit<ProjectionSupervisionOptions, keyof ResolvedSupervisionTuning>;
394
+ declare const ProjectionStalled_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
395
+ readonly _tag: "ProjectionStalled";
396
+ } & Readonly<A>;
397
+ /**
398
+ * The supervisor gave up: `noProgressRestarts` consecutive restarts moved the
399
+ * progress signal not at all.
400
+ *
401
+ * This is the only way a supervised daemon's fibre ever fails, and for a
402
+ * projection it means one of two things: a POISON EVENT (an `apply` or a decode
403
+ * that fails deterministically on the event at the checkpoint, so every restart
404
+ * re-reads it and dies again), or a view store still not answering after the
405
+ * WHOLE no-progress budget — broken, that is, rather than merely unavailable.
406
+ * That distinction is only as true as the budget makes it, which is why the
407
+ * default is sized at roughly five minutes of retrying rather than at a tidy
408
+ * restart count (`DEFAULT_MAX_NO_PROGRESS_RESTARTS`). Both need a human; neither
409
+ * is fixed by retrying, which is why the supervisor stops instead of retrying for
410
+ * ever and calling that resilience.
411
+ *
412
+ * Getting it in FRONT of that human is `onStalled`'s job, and the reason that
413
+ * hook exists: this failure lands on the daemon's error channel, and the wiring
414
+ * this library recommends discards the daemon. Beside the hook there is one
415
+ * `Effect.logError`, and nothing else.
416
+ *
417
+ * `position` is the stuck signal — the position everything is failing just
418
+ * after — so the offending event is one query away. `reason` is the last
419
+ * underlying failure, kept as `unknown` because it spans the supervised effect's
420
+ * whole `E`, which this module is generic in.
421
+ */
422
+ export declare class ProjectionStalled extends ProjectionStalled_base<{
423
+ readonly key: CheckpointKey;
424
+ readonly noProgressRestarts: number;
425
+ readonly position: Position;
426
+ readonly reason: unknown;
427
+ }> {
428
+ /**
429
+ * The two facts that make a stall diagnosable — how much budget was spent, and
430
+ * where it was spent — as the `Error` field that says why, so the fault renders
431
+ * ITSELF.
432
+ *
433
+ * This class needs the field more than any other fault the read side raises,
434
+ * because of WHERE it lands. `ProjectionStalled` is what `ProjectionRunner.fibre`
435
+ * fails with, which makes it the only one of them a consumer meets without going
436
+ * anywhere near the `ProjectionStore` port: `CheckpointSuperseded` and
437
+ * `ProjectionStoreError` sit on `commit`'s public signature too, but reaching them
438
+ * means calling or implementing that port, whereas the `Fiber.await` route the
439
+ * runner's own documentation recommends ends at whatever this getter says — from a
440
+ * daemon the application only forked.
441
+ *
442
+ * `Data.TaggedError` sets `name` to the tag and leaves `message` empty, and
443
+ * `Cause.pretty` substitutes its own `'An error has occurred'` for an empty one —
444
+ * so unfilled, a caller who did exactly what they were told to do reads
445
+ * `'ProjectionStalled: An error has occurred'` about the single failure this whole
446
+ * module exists to report. Filled, the same call yields a sentence naming the
447
+ * budget that was spent and the position everything is failing just after, which
448
+ * is the first thing an operator queries the log at.
449
+ *
450
+ * ## Why THESE two fields, and why interpolating them is safe
451
+ *
452
+ * `noProgressRestarts` is a `number`. `position` is a branded `bigint`, and
453
+ * TEMPLATE INTERPOLATION of a `bigint` yields its decimal digits — so what leaves
454
+ * this getter is typed `string` and carries no `bigint` for any serialiser to
455
+ * meet. That distinction is worth being exact about, because this package
456
+ * measures a real `bigint` hazard and it is easy to over-apply: `Logger.json`
457
+ * throws on a `bigint` NESTED IN AN ANNOTATION VALUE and loses the whole line, so
458
+ * the reduction in `shouldRestart` hands the logger `String(stored)`. The hazard
459
+ * is about the values a logger is handed, never about a `string` rendered from
460
+ * one — and `String(fault)` is precisely how a fault carrying a `bigint` field
461
+ * crosses that boundary intact.
462
+ *
463
+ * `reason` is deliberately NOT in the label. It spans the supervised effect's
464
+ * whole `E` and arrives as `unknown`, so anything this class said about it would
465
+ * be a guess at somebody else's value — and there is no need to guess: the raw
466
+ * fault reaches `onStalled` untouched, and the give-up log line beside it already
467
+ * carries `String(reason)`, which is the same self-rendering rule applied one
468
+ * level down.
469
+ *
470
+ * A GETTER over two existing fields rather than a `message` field, so neither can
471
+ * disagree with the label. Why a prototype accessor is safe here, how far filling
472
+ * the field reaches and the structured shape it leaves a machine to parse are set
473
+ * out once for all four of these getters on `ProjectionStoreError` in
474
+ * `ProjectionStore.ts`.
475
+ */
476
+ get message(): string;
477
+ }
478
+ /**
479
+ * Everything one supervision needs beyond the effect being supervised: WHAT is
480
+ * supervised (`key`), WHERE its progress is observed (`progress`), and the tuning
481
+ * and hooks `ProjectionSupervisionOptions` documents.
482
+ *
483
+ * One FLAT object, extending the two tuning interfaces rather than nesting either,
484
+ * for the reason `ProjectionRunnerOptions` gives at length: the
485
+ * `{ ...OPTIONS, oneField }` override this repo writes everywhere is a shallow
486
+ * spread, so a nested half would lose its siblings silently.
487
+ *
488
+ * Those two are exactly the halves of `ProjectionSupervisionOptions`:
489
+ * `UnresolvedSupervisionOptions`, which `buildAndFork` forwards by SPREADING the
490
+ * caller's own object, and `ResolvedSupervisionTuning`, which it names one by one
491
+ * beside that spread. So flatness costs this module nothing and hides nothing. A
492
+ * caller's whole unresolved half travels through in one expression — an optional
493
+ * field added there needs no edit anywhere to arrive — and a figure this module
494
+ * requires cannot arrive from that half at all: the spread's TYPE no longer carries
495
+ * one, and the OBJECT does not either, `buildAndFork` discarding the three by
496
+ * rest-destructure before it spreads what is left. The split is the types' to keep
497
+ * rather than an enumeration's, and that call site is where the runtime half of it
498
+ * is argued.
499
+ *
500
+ * FIVE fields are REQUIRED here where every field of the half a caller writes is
501
+ * optional, for two different reasons that happen to arrive at the same modifier.
502
+ *
503
+ * `key` and `progress` have no defensible default: a supervisor with no key could
504
+ * not name the thing it is reporting on, and one with no signal would be counting
505
+ * failures — the very thing this module exists not to do.
506
+ *
507
+ * The three restart `Duration`s do have defaults, and are required anyway, because
508
+ * by the time a figure arrives here `resolveProjection` has RESOLVED it against the
509
+ * defaults above and the construction gate has JUDGED the resolved value.
510
+ * Re-admitting an absent field would mean a second `?? DEFAULT` in the body below,
511
+ * and then the figure that RUNS could differ from the figure that was checked —
512
+ * which is the one failure mode a construction-time check cannot survive, since it
513
+ * would report a wiring sound and then run a different one. Required, this module
514
+ * cannot be handed a duration nothing validated. `maxNoProgressRestarts` is the
515
+ * exception that shows the rule: it stays optional and is defaulted below, because
516
+ * its brand did the judging at the caller's own `.make(...)` and there is no
517
+ * resolution for the gate to be inconsistent with.
518
+ */
519
+ export interface SuperviseOnProgressOptions extends UnresolvedSupervisionOptions, ResolvedSupervisionTuning {
520
+ /**
521
+ * Names the supervised thing. It is what both log lines, both hook payloads and
522
+ * the `ProjectionStalled` failure report, because an operator's first question
523
+ * about a stalled projection is which one.
524
+ */
525
+ readonly key: CheckpointKey;
526
+ /**
527
+ * The externally observed signal, re-read after EVERY failure — which is what
528
+ * makes the give-up decision about the world rather than about this process's
529
+ * own attempt count.
530
+ *
531
+ * A VALUE, not a thunk and not a service: nothing is read until the first
532
+ * failure, so building a supervision touches the view store not at all. That is
533
+ * what keeps the read side's construction-time defects honest — a runner the
534
+ * wiring gate rejected has read no checkpoint and opened no subscription, which
535
+ * every construction case in this package asserts.
536
+ */
537
+ readonly progress: Effect.Effect<Position, ProjectionStoreError>;
538
+ }
539
+ /**
540
+ * Supervise `run`: retry it while `options.progress` moves, give up with
541
+ * `ProjectionStalled` when it does not.
542
+ *
543
+ * Generic in the supervised effect's `A`, `E` and `R`. The supervisor reads the
544
+ * failure only through `String` and hands it to `onRestart`/`onStalled` as
545
+ * `unknown`, so it has no reason to constrain `E`; its own error channel is exactly
546
+ * `ProjectionStalled`, because the breaker collapses everything else into another
547
+ * restart; and `R` passes through untouched, nothing here adding a requirement,
548
+ * which is what keeps either entry point's requirement list the pipeline's own.
549
+ *
550
+ * ONE CALL rather than a curried supervisor later applied to an attempt. The rank-2
551
+ * intermediate bought exactly one thing: the tuning could be validated before the
552
+ * attempt existed, which fixed the ORDER a mis-tuned supervisor was reported in
553
+ * relative to the runner's other construction-time checks. That argument is now
554
+ * gone outright rather than merely outweighed — the tuning is judged by
555
+ * `projectionWiringFault` before a supervisor, an attempt or a fibre exists, in an
556
+ * order that module declares — so what a curried form would leave behind is a type
557
+ * whose only application sat a line from its construction.
558
+ *
559
+ * It validates NOTHING, which is deliberate rather than an omission. Every
560
+ * construction-time rejection on the read side has ONE home — the wiring gate, whose
561
+ * own single caller is the shared `prepareProjection` phase both entry points run
562
+ * before anything is built — and this module is
563
+ * package-internal with exactly one caller, so a check here would be a SECOND home
564
+ * for one rule: two sentences for one mistake, free to drift, each reachable only by
565
+ * whichever of the two modules its author happened to open. What arrives here has
566
+ * therefore already been resolved and judged, which is why the three restart
567
+ * `Duration`s are REQUIRED on `SuperviseOnProgressOptions` and why nothing below
568
+ * reaches for a default. The mistake still lands as a DEFECT at construction,
569
+ * exactly as `assertServableQuery`'s does; the gate is merely where.
570
+ *
571
+ * Nothing is taken for RENDERING the supervised effect's `E` on the two log lines:
572
+ * the `reason` annotation is `String(fault)`, so the fault labels itself. That is a
573
+ * real reduction rather than a floor, because `Error.prototype.toString` is
574
+ * `name: message` and `Data.TaggedError` sets `name` to the tag — so a fault that
575
+ * fills `message` renders as `'SomeTag: why'`, and one that does not renders as
576
+ * `'SomeTag'`. A caller wanting a better label sets `message` on its own error,
577
+ * where the knowledge belongs and where `Cause.pretty` and every other reader
578
+ * benefit from it too, rather than handing a table to a supervisor that would be
579
+ * the only thing to use it.
580
+ *
581
+ * The returned effect is re-runnable: the two supervision `Ref`s are created INSIDE
582
+ * it rather than here, so every run starts a fresh supervision session instead of
583
+ * inheriting the breaker state a previous run left behind.
584
+ */
585
+ export declare const superviseOnProgress: <A, E, R>(run: Effect.Effect<A, E, R>, options: SuperviseOnProgressOptions) => Effect.Effect<A, ProjectionStalled, R>;
586
+ export {};
587
+ //# sourceMappingURL=superviseOnProgress.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"superviseOnProgress.d.ts","sourceRoot":"","sources":["../../src/superviseOnProgress.ts"],"names":[],"mappings":"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"}