@systemfsoftware/effect-daemon-spec 0.7.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,31 +1,43 @@
1
- import { Array as Array$1, Cause, Clock, Context, Duration, Effect, Either, Exit, Fiber, HashMap, Layer, Match, Metric, Option, Predicate, Ref, Schedule, Schema, Scope, Stream } from "effect";
2
- import { left, right } from "effect/Either";
1
+ import { Array, Cause, Clock, Context, Duration, Effect, Equal, Exit, Fiber, HashMap, Latch, Layer, Match, Metric, Option, Predicate, Ref, Result, Schedule, Schema, Scope, Stream, pipe } from "effect";
2
+ import { Cell, Workflow } from "@systemfsoftware/effect-cell-types";
3
+ import * as Arr from "effect/Array";
3
4
  import * as Match$1 from "effect/Match";
5
+ import * as Result$1 from "effect/Result";
4
6
  import * as S from "effect/Schema";
5
- //#region src/backoff.kernel.ts
7
+ //#region src/Backoff.ts
6
8
  const cappedBackoff = (base, cap) => {
7
- const ceiling = Duration.decode(cap);
8
- return Schedule.exponential(base).pipe(Schedule.jittered, Schedule.modifyDelay((_, delay) => Duration.min(delay, ceiling)));
9
+ const ceiling = Duration.fromInputUnsafe(cap);
10
+ return Schedule.exponential(base).pipe(Schedule.jittered, Schedule.modifyDelay(({ duration }) => Effect.succeed(Duration.min(duration, ceiling))));
9
11
  };
10
12
  //#endregion
11
- //#region src/brands.kernel.ts
13
+ //#region src/Brands.ts
12
14
  const WorkerTypeId = Symbol.for("@systemfsoftware/effect-daemon/Worker");
13
15
  const SupervisorTypeId = Symbol.for("@systemfsoftware/effect-daemon/Supervisor");
14
16
  const DynamicSpecTypeId = Symbol.for("@systemfsoftware/effect-daemon/DynamicSpec");
15
17
  //#endregion
16
- //#region src/daemon-health.schema.ts
17
- var DynamicLimitExceeded = class extends Schema.TaggedError()("DynamicLimitExceeded", { limit: Schema.Int.pipe(Schema.greaterThanOrEqualTo(0)) }) {};
18
+ //#region src/DaemonHealth.schema.ts
19
+ var DynamicLimitExceeded = class extends Schema.TaggedError()("DynamicLimitExceeded", { limit: Schema.Int.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0))) }) {};
20
+ Schema.decodeUnknownExit(DynamicLimitExceeded);
18
21
  //#endregion
19
- //#region src/daemon-metrics.kernel.ts
22
+ //#region src/DaemonMetrics.ts
20
23
  const supervisorRestartsCounter = Metric.counter("app.daemon.supervisor.restart", { description: "Daemon supervisor restart count" });
21
24
  const supervisorExhaustionsCounter = Metric.counter("app.daemon.supervisor.exhaustion", { description: "Daemon supervisor exhaustion count" });
22
25
  const supervisorChildrenGauge = Metric.gauge("app.daemon.supervisor.children", { description: "Daemon supervisor current child count (dynamic supervisors only)" });
23
26
  const healthStateGauge = Metric.gauge("app.daemon.health.state", { description: "Daemon health latch open (1) or closed (0); tagged by daemon name and latch (ready | healthy | paused)" });
24
27
  //#endregion
25
- //#region src/daemon-policy.schema.ts
28
+ //#region src/SupervisorDynamic.ts
29
+ const MAX_CHILDREN_CEILING = 1e3;
30
+ const dynamic$2 = (opts) => ({
31
+ [DynamicSpecTypeId]: DynamicSpecTypeId,
32
+ name: opts.name,
33
+ child: opts.child,
34
+ maxChildren: opts.maxChildren
35
+ });
36
+ //#endregion
37
+ //#region src/DaemonPolicy.schema.ts
26
38
  const IntensityConfig = Schema.Struct({
27
- restarts: Schema.Int.pipe(Schema.greaterThanOrEqualTo(0)),
28
- window: Schema.DurationFromSelf
39
+ restarts: Schema.Int.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0))),
40
+ window: Schema.Duration
29
41
  });
30
42
  const IntensityTypeId = Symbol.for("@systemfsoftware/effect-daemon-spec/Intensity");
31
43
  var BoundedIntensity = class extends Schema.TaggedClass()("Bounded", IntensityConfig.fields) {
@@ -34,33 +46,54 @@ var BoundedIntensity = class extends Schema.TaggedClass()("Bounded", IntensityCo
34
46
  var UnboundedIntensity = class extends Schema.TaggedClass()("Unbounded", {}) {
35
47
  [IntensityTypeId] = IntensityTypeId;
36
48
  };
37
- const Intensity = Schema.Union(BoundedIntensity, UnboundedIntensity);
49
+ const Intensity = Schema.Union([BoundedIntensity, UnboundedIntensity]);
38
50
  var ChildPolicyConfig = class extends Schema.Class("ChildPolicyConfig")({
39
- restart: Schema.optional(Schema.Literal("permanent", "transient", "temporary")),
51
+ restart: Schema.optional(Schema.Literals([
52
+ "permanent",
53
+ "transient",
54
+ "temporary"
55
+ ])),
40
56
  intensity: Schema.optional(IntensityConfig)
41
57
  }) {};
42
- Schema.Class("SupervisorPolicyConfig")({
58
+ var SupervisorPolicyConfig = class extends Schema.Class("SupervisorPolicyConfig")({
43
59
  intensity: Schema.optional(IntensityConfig),
44
- cooldown: Schema.optional(Schema.DurationFromSelf)
45
- });
60
+ cooldown: Schema.optional(Schema.Duration)
61
+ }) {};
46
62
  var LockPolicyConfig = class extends Schema.Class("LockPolicyConfig")({
47
- mode: Schema.optional(Schema.Literal("none", "required", "optional")),
63
+ mode: Schema.optional(Schema.Literals([
64
+ "none",
65
+ "required",
66
+ "optional"
67
+ ])),
48
68
  key: Schema.optional(Schema.String)
49
69
  }) {};
50
70
  var TickPolicyConfig = class extends Schema.Class("TickPolicyConfig")({
51
71
  spanName: Schema.optional(Schema.String),
52
- tickTimeout: Schema.DurationFromSelf,
53
- startLogLevel: Schema.optional(Schema.Literal("debug", "info"))
72
+ tickTimeout: Schema.Duration,
73
+ startLogLevel: Schema.optional(Schema.Literals(["debug", "info"]))
54
74
  }) {};
55
- //#endregion
56
- //#region src/daemon-reporter.adapter.ts
57
- var DaemonReporter = class extends Context.Tag("@systemfsoftware/effect-daemon-spec/daemon-reporter.adapter/DaemonReporter")() {};
75
+ const MaxChildren = Schema.Int.pipe(Schema.check(Schema.isBetween({
76
+ minimum: 1,
77
+ maximum: MAX_CHILDREN_CEILING
78
+ })), Schema.brand("MaxChildren"));
79
+ Schema.Int.pipe(Schema.check(Schema.makeFilter((children) => children < 1 || children > 1e3)));
80
+ Schema.Finite.pipe(Schema.check(Schema.isBetween({
81
+ minimum: 1,
82
+ maximum: MAX_CHILDREN_CEILING
83
+ })), Schema.check(Schema.makeFilter((children) => !Number.isInteger(children))));
84
+ Schema.decodeUnknownExit(ChildPolicyConfig);
85
+ Schema.decodeUnknownExit(LockPolicyConfig);
86
+ Schema.decodeUnknownExit(TickPolicyConfig);
87
+ Schema.decodeUnknownExit(SupervisorPolicyConfig);
88
+ //#endregion
89
+ //#region src/DaemonReporterAdapter.ts
90
+ var DaemonReporter = class extends Context.Service()("@systemfsoftware/effect-daemon-spec/DaemonReporterAdapter/DaemonReporter") {};
58
91
  const Noop = Layer.succeed(DaemonReporter, DaemonReporter.of({
59
92
  onRestart: () => Effect.void,
60
93
  onExhausted: () => Effect.void
61
94
  }));
62
95
  //#endregion
63
- //#region src/daemon-poll.kernel.ts
96
+ //#region src/DaemonPoll.ts
64
97
  const poll$1 = (opts) => {
65
98
  if (typeof opts.prereq === "undefined") {
66
99
  const gate = Effect.succeed(Option.some(Effect.asVoid(opts.work)));
@@ -95,7 +128,7 @@ const poll$1 = (opts) => {
95
128
  };
96
129
  };
97
130
  //#endregion
98
- //#region src/daemon-stream.kernel.ts
131
+ //#region src/DaemonStream.ts
99
132
  const stream$1 = (opts) => ({
100
133
  [WorkerTypeId]: WorkerTypeId,
101
134
  name: opts.name,
@@ -109,7 +142,7 @@ const stream$1 = (opts) => ({
109
142
  lock: opts.lock
110
143
  });
111
144
  //#endregion
112
- //#region src/daemon-subscription.kernel.ts
145
+ //#region src/DaemonSubscription.ts
113
146
  const subscription$1 = (opts) => ({
114
147
  [WorkerTypeId]: WorkerTypeId,
115
148
  name: opts.name,
@@ -123,29 +156,32 @@ const subscription$1 = (opts) => ({
123
156
  lock: opts.lock
124
157
  });
125
158
  //#endregion
126
- //#region src/leader-lock.schema.ts
159
+ //#region src/LeaderLock.ts
160
+ const isModeNone = (lock) => lock.mode === "none";
161
+ //#endregion
162
+ //#region src/LeaderLock.schema.ts
127
163
  var LeaderLockNotAcquired = class extends Schema.TaggedError()("LeaderLockNotAcquired", { key: Schema.String }) {};
128
164
  var LeaderLockInfraError = class extends Schema.TaggedError()("LeaderLockInfraError", {
129
165
  key: Schema.String,
130
166
  cause: Schema.Unknown
131
167
  }) {};
132
168
  //#endregion
133
- //#region src/lock-primitive.schema.ts
169
+ //#region src/LockPrimitive.schema.ts
134
170
  var LockPrimitiveError = class extends Schema.TaggedError()("LockPrimitiveError", {
135
171
  key: Schema.String,
136
172
  cause: Schema.Unknown
137
173
  }) {};
138
174
  //#endregion
139
- //#region src/leader-lock.adapter.ts
140
- var LeaderLock = class LeaderLock extends Context.Tag("@systemfsoftware/effect-daemon-spec/leader-lock.adapter/LeaderLock")() {
175
+ //#region src/LeaderLockAdapter.ts
176
+ var LeaderLock = class LeaderLock extends Context.Service()("@systemfsoftware/effect-daemon-spec/LeaderLockAdapter/LeaderLock") {
141
177
  static Noop = Layer.succeed(LeaderLock, LeaderLock.of({ withLock: (_key, self) => Effect.map(self, Option.some) }));
142
178
  };
143
- var LockPrimitive = class extends Context.Tag("@systemfsoftware/effect-daemon-spec/leader-lock.adapter/LockPrimitive")() {};
179
+ var LockPrimitive = class extends Context.Service()("@systemfsoftware/effect-daemon-spec/LeaderLockAdapter/LockPrimitive") {};
144
180
  const LeaderLockFromPrimitive = Layer.effect(LeaderLock, Effect.gen(function* () {
145
181
  const primitive = yield* LockPrimitive;
146
182
  return LeaderLock.of({ withLock: (key, self) => Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
147
183
  const scope = yield* Scope.make();
148
- if (!(yield* restore(primitive.tryAcquire(key)).pipe(Scope.extend(scope), Effect.mapError((cause) => new LeaderLockInfraError({
184
+ if (!(yield* restore(primitive.tryAcquire(key)).pipe(Scope.provide(scope), Effect.mapError((cause) => LeaderLockInfraError.make({
149
185
  key,
150
186
  cause
151
187
  })), Effect.onError(() => Scope.close(scope, Exit.void))))) {
@@ -157,14 +193,23 @@ const LeaderLockFromPrimitive = Layer.effect(LeaderLock, Effect.gen(function* ()
157
193
  })) });
158
194
  }));
159
195
  //#endregion
160
- //#region src/internal/allocate-worker-health.kernel.ts
196
+ //#region src/internal/AllocateWorkerHealth.ts
161
197
  const allocateWorkerHealth = (name) => Effect.gen(function* () {
162
- const ready = yield* Effect.makeLatch(false);
163
- const healthy = yield* Effect.makeLatch(true);
164
- const paused = yield* Effect.makeLatch(true);
165
- yield* Metric.set(Metric.tagged(Metric.tagged(healthStateGauge, "daemon", name), "latch", "ready"), 0);
166
- yield* Metric.set(Metric.tagged(Metric.tagged(healthStateGauge, "daemon", name), "latch", "healthy"), 1);
167
- yield* Metric.set(Metric.tagged(Metric.tagged(healthStateGauge, "daemon", name), "latch", "paused"), 1);
198
+ const ready = yield* Latch.make(false);
199
+ const healthy = yield* Latch.make(true);
200
+ const paused = yield* Latch.make(true);
201
+ yield* Metric.update(Metric.withAttributes(healthStateGauge, {
202
+ daemon: name,
203
+ latch: "ready"
204
+ }), 0);
205
+ yield* Metric.update(Metric.withAttributes(healthStateGauge, {
206
+ daemon: name,
207
+ latch: "healthy"
208
+ }), 1);
209
+ yield* Metric.update(Metric.withAttributes(healthStateGauge, {
210
+ daemon: name,
211
+ latch: "paused"
212
+ }), 1);
168
213
  return {
169
214
  name,
170
215
  ready,
@@ -173,25 +218,22 @@ const allocateWorkerHealth = (name) => Effect.gen(function* () {
173
218
  };
174
219
  });
175
220
  //#endregion
176
- //#region src/internal/build-worker-loop.kernel.ts
221
+ //#region src/internal/BuildWorkerLoop.ts
177
222
  const applySpanAttributes = (hooks) => {
178
223
  const { spanAttributes } = hooks;
179
224
  return (effect) => {
180
225
  if (typeof spanAttributes === "undefined") return effect;
181
- return Effect.tap(effect, () => Effect.orElse(spanAttributes, () => Effect.succeed({})).pipe(Effect.flatMap(Effect.annotateCurrentSpan)));
226
+ return Effect.tap(effect, () => Effect.orElseSucceed(spanAttributes, () => ({})).pipe(Effect.flatMap(Effect.annotateCurrentSpan)));
182
227
  };
183
228
  };
184
229
  const applyTrackDuration = (hooks) => {
185
230
  const { trackDuration } = hooks;
186
231
  return (effect) => {
187
232
  if (typeof trackDuration === "undefined") return effect;
188
- return Metric.trackDuration(effect, trackDuration);
233
+ return Effect.trackDuration(effect, trackDuration);
189
234
  };
190
235
  };
191
- const applyTimeout = (timeout) => (effect) => Effect.timeoutFail(effect, {
192
- duration: timeout,
193
- onTimeout: () => new Cause.TimeoutException()
194
- });
236
+ const applyTimeout = (timeout) => (effect) => effect.pipe(Effect.timeout(timeout), Effect.catchTag("TimeoutError", () => Effect.fail(new Cause.TimeoutError())));
195
237
  const applyInnerRetry = (hooks) => {
196
238
  const { innerRetry } = hooks;
197
239
  return (effect) => {
@@ -199,7 +241,10 @@ const applyInnerRetry = (hooks) => {
199
241
  return Effect.retry(effect, innerRetry);
200
242
  };
201
243
  };
202
- const openReadyGauge = (gauge, name) => Metric.set(Metric.tagged(Metric.tagged(gauge, "daemon", name), "latch", "ready"), 1);
244
+ const openReadyGauge = (gauge, name) => Metric.update(Metric.withAttributes(gauge, {
245
+ daemon: name,
246
+ latch: "ready"
247
+ }), 1);
203
248
  const buildPollTick = (worker, health, gate, readyGauge) => {
204
249
  const spanName = worker.tick.spanName ?? "daemon.tick";
205
250
  const withSpanAttrs = applySpanAttributes(worker.tickHooks);
@@ -213,7 +258,7 @@ const buildPollTick = (worker, health, gate, readyGauge) => {
213
258
  return withInnerRetry(withTimeout(Effect.andThen(health.paused.await, gate).pipe(Effect.flatMap(Option.match({
214
259
  onNone: () => Effect.void,
215
260
  onSome: runWork
216
- }))))).pipe(Effect.tap(() => Effect.zipRight(health.ready.open, openReadyGauge(readyGauge, worker.name))), Effect.asVoid);
261
+ }))))).pipe(Effect.tap(() => Effect.andThen(health.ready.open, openReadyGauge(readyGauge, worker.name))), Effect.asVoid);
217
262
  };
218
263
  const wrapSpan = (worker, effect, spanName) => {
219
264
  const finalSpan = spanName ?? "daemon.worker";
@@ -230,10 +275,10 @@ const propagateExit = (exit) => Match.value(exit).pipe(Match.tag("Failure", ({ c
230
275
  const buildStreamLoop = (worker, loop, health, readyGauge) => {
231
276
  const body = Effect.gen(function* () {
232
277
  yield* health.paused.await;
233
- const fiber = yield* Effect.forkScoped(loop.stream.pipe(Stream.tap(() => Effect.zipRight(health.ready.open, openReadyGauge(readyGauge, worker.name))), Stream.runDrain));
278
+ const fiber = yield* Effect.forkScoped(loop.stream.pipe(Stream.tap(() => Effect.andThen(health.ready.open, openReadyGauge(readyGauge, worker.name))), Stream.runDrain), { startImmediately: true });
234
279
  const ready = applyTimeout(worker.tick.tickTimeout)(health.ready.await);
235
- yield* Effect.raceFirst(ready, fiber.await.pipe(Effect.flatMap(propagateExit)));
236
- yield* fiber.await.pipe(Effect.flatMap(propagateExit));
280
+ yield* Effect.raceFirst(ready, Fiber.await(fiber).pipe(Effect.flatMap(propagateExit)));
281
+ yield* Fiber.await(fiber).pipe(Effect.flatMap(propagateExit));
237
282
  });
238
283
  const retried = applyInnerRetry(worker.tickHooks)(body);
239
284
  return wrapSpan(worker, retried.pipe(Effect.asVoid));
@@ -242,20 +287,19 @@ const buildSubscriptionLoop = (worker, loop, health, readyGauge) => {
242
287
  const body = Effect.gen(function* () {
243
288
  yield* health.paused.await;
244
289
  yield* applyTimeout(worker.tick.tickTimeout)(loop.acquire);
245
- yield* Effect.zipRight(health.ready.open, openReadyGauge(readyGauge, worker.name));
290
+ yield* Effect.andThen(health.ready.open, openReadyGauge(readyGauge, worker.name));
246
291
  return yield* Effect.never;
247
292
  });
248
293
  return wrapSpan(worker, body.pipe(Effect.asVoid));
249
294
  };
250
295
  const buildWorkerLoop = (worker, health, readyGauge) => Match.value(worker.loop).pipe(Match.tag("Poll", (loop) => buildPollLoop(worker, loop, health, readyGauge)), Match.tag("Stream", (loop) => buildStreamLoop(worker, loop, health, readyGauge)), Match.tag("Subscription", (loop) => buildSubscriptionLoop(worker, loop, health, readyGauge)), Match.exhaustive);
251
296
  //#endregion
252
- //#region src/internal/with-leader-lock.executor.ts
253
- var WithLeaderLockExecutorDeps = class extends Context.Tag("@systemfsoftware/effect-daemon-spec/internal/with-leader-lock.executor/WithLeaderLockExecutorDeps")() {};
254
- function withLeaderLock(self, options) {
297
+ //#region src/internal/WithLeaderLockExecutor.ts
298
+ function withLeaderLock(self, options, lock) {
255
299
  const acquire = Effect.gen(function* () {
256
- const out = yield* (yield* WithLeaderLockExecutorDeps).withLock(options.key, self);
300
+ const out = yield* lock.withLock(options.key, self);
257
301
  if (Option.isSome(out)) return out.value;
258
- return yield* Match.value(options.mode).pipe(Match.when("required", () => Effect.fail(new LeaderLockNotAcquired({ key: options.key }))), Match.when("optional", () => Effect.void), Match.exhaustive);
302
+ return yield* Match.value(options.mode).pipe(Match.when("required", () => Effect.fail(LeaderLockNotAcquired.make({ key: options.key }))), Match.when("optional", () => Effect.void), Match.exhaustive);
259
303
  });
260
304
  const retryOnce = (current) => Effect.retry(acquire, {
261
305
  schedule: current,
@@ -266,37 +310,59 @@ function withLeaderLock(self, options) {
266
310
  return acquire;
267
311
  }
268
312
  //#endregion
269
- //#region src/leader-lock.kernel.ts
270
- const isModeNone = (lock) => lock.mode === "none";
271
- Context.Tag("@systemfsoftware/effect-daemon-spec/daemon-worker.executor/DaemonWorkerExecutorDeps")();
272
- const worker = (w) => Effect.gen(function* () {
273
- const health = yield* allocateWorkerHealth(w.name);
274
- const loop = buildWorkerLoop(w, health, healthStateGauge).pipe(Effect.orDie);
275
- const lockNone = isModeNone(w.lock);
276
- const required = !lockNone && w.lock.mode === "required";
277
- let locked;
278
- if (lockNone) locked = loop;
279
- else if (required) locked = withLeaderLock(loop, {
280
- key: w.lock.key,
313
+ //#region src/internal/WithLockByModeExecutor.ts
314
+ /**
315
+ * Runs `self` under whichever lock discipline the binding asks for.
316
+ *
317
+ * An absent lock adapter and a `mode: 'none'` spec are one case, not two: a spec that
318
+ * declines the lock and a composition root that supplies no adapter both leave the
319
+ * body unwrapped, and nothing downstream can tell them apart from the effect that
320
+ * comes back. `unlocked` is that single case; `locked` dispatches on the spec's mode
321
+ * to the existing `withLeaderLock` calls. There is no null arm because the null state
322
+ * is now a constructor choice, not a runtime case.
323
+ *
324
+ * The worker and the supervisor make the same choice over the same cases, so it is
325
+ * made here once rather than in each of them.
326
+ */
327
+ const withLockByMode = (self, binding) => {
328
+ if (binding.kind === "unlocked") return self;
329
+ if (binding.spec.mode === "required") return withLeaderLock(self, {
330
+ key: binding.spec.key,
281
331
  mode: "required",
282
- acquireRetryBackoff: w.lock.acquireRetryBackoff
283
- });
284
- else locked = withLeaderLock(loop, {
285
- key: w.lock.key,
332
+ acquireRetryBackoff: binding.spec.acquireRetryBackoff
333
+ }, binding.lock);
334
+ return withLeaderLock(self, {
335
+ key: binding.spec.key,
286
336
  mode: "optional"
287
- });
337
+ }, binding.lock);
338
+ };
339
+ //#endregion
340
+ //#region src/DaemonWorkerExecutor.ts
341
+ const worker$2 = (w, binding) => Effect.gen(function* () {
342
+ const health = yield* allocateWorkerHealth(w.name);
343
+ const loop = buildWorkerLoop(w, health, healthStateGauge).pipe(Effect.orDie);
344
+ const locked = withLockByMode(loop, binding);
288
345
  yield* Effect.forkScoped(locked.pipe(Effect.orDie));
289
346
  return health;
290
347
  });
291
348
  //#endregion
292
- //#region src/internal/allocate-supervisor-health.kernel.ts
349
+ //#region src/internal/AllocateSupervisorHealth.ts
293
350
  const allocateSupervisorHealth = (name, children) => Effect.gen(function* () {
294
- const ready = yield* Effect.makeLatch(false);
295
- const healthy = yield* Effect.makeLatch(true);
296
- const paused = yield* Effect.makeLatch(true);
297
- yield* Metric.set(Metric.tagged(Metric.tagged(healthStateGauge, "daemon", name), "latch", "ready"), 0);
298
- yield* Metric.set(Metric.tagged(Metric.tagged(healthStateGauge, "daemon", name), "latch", "healthy"), 1);
299
- yield* Metric.set(Metric.tagged(Metric.tagged(healthStateGauge, "daemon", name), "latch", "paused"), 1);
351
+ const ready = yield* Latch.make(false);
352
+ const healthy = yield* Latch.make(true);
353
+ const paused = yield* Latch.make(true);
354
+ yield* Metric.update(Metric.withAttributes(healthStateGauge, {
355
+ daemon: name,
356
+ latch: "ready"
357
+ }), 0);
358
+ yield* Metric.update(Metric.withAttributes(healthStateGauge, {
359
+ daemon: name,
360
+ latch: "healthy"
361
+ }), 1);
362
+ yield* Metric.update(Metric.withAttributes(healthStateGauge, {
363
+ daemon: name,
364
+ latch: "paused"
365
+ }), 1);
300
366
  return {
301
367
  name,
302
368
  ready,
@@ -306,20 +372,114 @@ const allocateSupervisorHealth = (name, children) => Effect.gen(function* () {
306
372
  };
307
373
  });
308
374
  //#endregion
309
- //#region src/internal/intensity-window.kernel.ts
375
+ //#region src/internal/BuildDynamicExecutor.ts
376
+ const buildDynamic = (spec, health) => Effect.gen(function* () {
377
+ const state = yield* Ref.make({
378
+ nextId: 0,
379
+ children: HashMap.empty()
380
+ });
381
+ const startChildImpl = (args) => Effect.gen(function* () {
382
+ const childWorker = spec.child(args);
383
+ const workerHealth = yield* allocateWorkerHealth(childWorker.name);
384
+ const loop = buildWorkerLoop(childWorker, workerHealth, healthStateGauge).pipe(Effect.orDie);
385
+ const removed = yield* Latch.make(false);
386
+ const reservedId = yield* Ref.modify(state, (current) => {
387
+ if (HashMap.size(current.children) >= spec.maxChildren) return [Option.none(), current];
388
+ const children = HashMap.set(current.children, current.nextId, Equal.byReferenceUnsafe({
389
+ fiber: Option.none(),
390
+ removed
391
+ }));
392
+ return [Option.some(current.nextId), {
393
+ nextId: current.nextId + 1,
394
+ children
395
+ }];
396
+ });
397
+ if (Option.isNone(reservedId)) return yield* DynamicLimitExceeded.make({ limit: spec.maxChildren });
398
+ const id = reservedId.value;
399
+ yield* Metric.update(supervisorChildrenGauge, HashMap.size(yield* Ref.get(state).pipe(Effect.map((s) => s.children))));
400
+ const cleanup = Effect.gen(function* () {
401
+ const count = yield* Ref.modify(state, (current) => {
402
+ const children = HashMap.remove(current.children, id);
403
+ return [HashMap.size(children), {
404
+ ...current,
405
+ children
406
+ }];
407
+ });
408
+ yield* Metric.update(supervisorChildrenGauge, count);
409
+ yield* removed.open;
410
+ }).pipe(Effect.asVoid);
411
+ const fiber = yield* Effect.forkScoped(loop.pipe(Effect.ensuring(cleanup)));
412
+ const count = yield* Ref.modify(state, (current) => {
413
+ const childOpt = HashMap.get(current.children, id);
414
+ if (Option.isNone(childOpt)) return [HashMap.size(current.children), current];
415
+ const children = HashMap.set(current.children, id, Equal.byReferenceUnsafe({
416
+ ...childOpt.value,
417
+ fiber: Option.some(fiber)
418
+ }));
419
+ return [HashMap.size(children), {
420
+ nextId: current.nextId,
421
+ children
422
+ }];
423
+ });
424
+ yield* Metric.update(supervisorChildrenGauge, count);
425
+ return {
426
+ id,
427
+ removed: removed.await
428
+ };
429
+ });
430
+ const stopChildImpl = (ref) => Effect.gen(function* () {
431
+ const [stateOpt, count] = yield* Ref.modify(state, (current) => {
432
+ const found = HashMap.get(current.children, ref.id);
433
+ const children = HashMap.remove(current.children, ref.id);
434
+ return [[found, HashMap.size(children)], {
435
+ ...current,
436
+ children
437
+ }];
438
+ });
439
+ if (Option.isSome(stateOpt)) {
440
+ const { fiber, removed } = stateOpt.value;
441
+ yield* Option.match(fiber, {
442
+ onNone: () => Effect.void,
443
+ onSome: (running) => Effect.gen(function* () {
444
+ yield* Fiber.interrupt(running);
445
+ yield* Fiber.await(running);
446
+ })
447
+ });
448
+ yield* Metric.update(supervisorChildrenGauge, count);
449
+ yield* removed.open;
450
+ }
451
+ });
452
+ const countImpl = Ref.get(state).pipe(Effect.map((current) => HashMap.size(current.children)));
453
+ yield* Effect.andThen(health.ready.open, Metric.update(Metric.withAttributes(healthStateGauge, {
454
+ daemon: spec.name,
455
+ latch: "ready"
456
+ }), 1));
457
+ return {
458
+ health,
459
+ startChild: startChildImpl,
460
+ stopChild: stopChildImpl,
461
+ count: countImpl
462
+ };
463
+ });
464
+ const dynamic$1 = (spec) => Effect.gen(function* () {
465
+ const health = yield* allocateSupervisorHealth(spec.name, []);
466
+ return yield* buildDynamic(spec, health);
467
+ });
468
+ //#endregion
469
+ //#region src/internal/IntensityWindow.ts
310
470
  const isWithinWindow = (now, windowMillis) => (t) => now - t <= windowMillis;
311
471
  const keepWithin = (now, windowMillis) => (ts) => ts.filter(isWithinWindow(now, windowMillis));
312
472
  const pruneTimestamps = (ts, now, windowMillis) => keepWithin(now, windowMillis)(ts);
313
473
  const recordTimestamp = (ts, now, windowMillis) => [now, ...pruneTimestamps(ts, now, windowMillis)];
314
474
  const exceedsRestarts = (count, restarts) => count > restarts;
315
475
  //#endregion
316
- //#region src/internal/intensity.kernel.ts
476
+ //#region src/internal/Intensity.ts
317
477
  const neverExceeds = {
318
478
  record: Effect.void,
319
479
  isExceeded: Effect.succeed(false),
320
480
  count: Effect.succeed(0)
321
481
  };
322
- const boundedTracker = (restarts, window) => Effect.gen(function* () {
482
+ const make = (restarts, window) => Effect.gen(function* () {
323
483
  const windowMillis = Duration.toMillis(window);
324
484
  const timestamps = yield* Ref.make([]);
325
485
  const prune = (now) => Ref.modify(timestamps, (ts) => {
@@ -333,7 +493,8 @@ const boundedTracker = (restarts, window) => Effect.gen(function* () {
333
493
  }),
334
494
  isExceeded: Effect.gen(function* () {
335
495
  const now = yield* Clock.currentTimeMillis;
336
- return exceedsRestarts((yield* prune(now)).length, restarts);
496
+ const active = yield* prune(now);
497
+ return exceedsRestarts(active.length, restarts);
337
498
  }),
338
499
  count: Effect.gen(function* () {
339
500
  const now = yield* Clock.currentTimeMillis;
@@ -341,34 +502,27 @@ const boundedTracker = (restarts, window) => Effect.gen(function* () {
341
502
  })
342
503
  };
343
504
  });
344
- const make = (intensity) => Match.value(intensity).pipe(Match.tag("Unbounded", () => Effect.succeed(neverExceeds)), Match.tag("Bounded", ({ restarts, window }) => boundedTracker(restarts, window)), Match.exhaustive);
345
- //#endregion
346
- //#region src/internal/race-for-exit.kernel.ts
347
- const raceForExit = (fibers) => Effect.raceAll(fibers.map((f, idx) => f.await.pipe(Effect.map((exit) => [idx, exit]))));
348
505
  //#endregion
349
- //#region src/internal/restart-decision.schema.ts
350
- const RestartStrategy = Schema.Literal("one_for_one", "one_for_all", "rest_for_one");
351
- Schema.Struct({
352
- strategy: RestartStrategy,
353
- totalChildren: Schema.Int.pipe(Schema.between(1, 10)),
354
- failedIndex: Schema.Int.pipe(Schema.greaterThanOrEqualTo(0)),
355
- exitSuccess: Schema.Boolean,
356
- intensityExceeded: Schema.Boolean
357
- }).pipe(Schema.filter((s) => s.failedIndex < s.totalChildren, { message: () => "failedIndex must be < totalChildren" }), Schema.annotations({ arbitrary: () => (fc) => fc.integer({
358
- min: 1,
359
- max: 10
360
- }).chain((totalChildren) => fc.integer({
361
- min: 0,
362
- max: totalChildren - 1
363
- }).chain((failedIndex) => fc.record({
364
- strategy: fc.constantFrom("one_for_one", "one_for_all", "rest_for_one"),
365
- totalChildren: fc.constant(totalChildren),
366
- failedIndex: fc.constant(failedIndex),
367
- exitSuccess: fc.boolean(),
368
- intensityExceeded: fc.boolean()
369
- }))) }));
370
- //#endregion
371
- //#region src/internal/restart-decision.workflow.ts
506
+ //#region src/internal/RaceForExit.ts
507
+ const raceForExit = (fibers) => Effect.raceAll(fibers.map((f, idx) => Fiber.await(f).pipe(Effect.map((exit) => [idx, exit]))));
508
+ //#endregion
509
+ //#region src/internal/RestartDecision.workflow.ts
510
+ /**
511
+ * The child indices a restart covers, by supervision strategy.
512
+ *
513
+ * A pure total function: the one part of the restart decision that is computation rather
514
+ * than dispatch, so it lives in the decision cell beside the `Workflow.make` it serves.
515
+ */
516
+ const restartIndicesFor = (strategy, failedIndex, total) => Match$1.value(strategy).pipe(Match$1.when("one_for_one", () => [failedIndex]), Match$1.when("one_for_all", () => Arr.range(0, total - 1)), Match$1.when("rest_for_one", () => Arr.range(failedIndex, total - 1)), Match$1.exhaustive);
517
+ /**
518
+ * The cross-field invariant the decode input carries: a failed child's index addresses one of
519
+ * the children that exist.
520
+ *
521
+ * It lives here rather than inline in `Schema.filter` because naming it makes it reachable by
522
+ * a property test, which an inline arrow is not. The schema imports the name to build its
523
+ * filter; the decision file is where the invariant is owned.
524
+ */
525
+ const failedIndexAddressesAChild = (input) => input.failedIndex < input.totalChildren;
372
526
  const RestartDecisionTypeId = Symbol.for("@systemfsoftware/effect-daemon/RestartDecision");
373
527
  var RestartDecisionContinue = class extends S.TaggedClass()("Continue", {}) {
374
528
  [RestartDecisionTypeId] = RestartDecisionTypeId;
@@ -379,13 +533,32 @@ var RestartDecisionRestart = class extends S.TaggedClass()("Restart", { indices:
379
533
  var RestartDecisionExhausted = class extends S.TaggedError()("Exhausted", {}) {
380
534
  [RestartDecisionTypeId] = RestartDecisionTypeId;
381
535
  };
382
- const restartIndicesFor = (strategy, failedIndex, total) => Match$1.value(strategy).pipe(Match$1.when("one_for_one", () => [failedIndex]), Match$1.when("one_for_all", () => [0, ...Array.from({ length: Math.max(0, total - 1) }, (_, i) => i + 1)]), Match$1.when("rest_for_one", () => [failedIndex, ...Array.from({ length: Math.max(0, total - failedIndex - 1) }, (_, i) => failedIndex + 1 + i)]), Match$1.exhaustive);
383
- const decideRestart = (input) => Match$1.value(input).pipe(Match$1.when({ exitSuccess: true }, () => right(new RestartDecisionContinue())), Match$1.when({
536
+ const decideRestart = Workflow.make((command) => Match$1.value(command).pipe(Match$1.when({ exitSuccess: true }, () => Result$1.succeed(RestartDecisionContinue.make())), Match$1.when({
384
537
  exitSuccess: false,
385
538
  intensityExceeded: true
386
- }, () => left(new RestartDecisionExhausted())), Match$1.orElse(() => right(new RestartDecisionRestart({ indices: restartIndicesFor(input.strategy, input.failedIndex, input.totalChildren) }))));
539
+ }, () => Result$1.fail(RestartDecisionExhausted.make())), Match$1.orElse(() => Result$1.succeed(RestartDecisionRestart.make({ indices: restartIndicesFor(command.strategy, command.failedIndex, command.totalChildren) })))));
540
+ //#endregion
541
+ //#region src/internal/RestartDecision.schema.ts
542
+ const RestartStrategy = Schema.Literals([
543
+ "one_for_one",
544
+ "one_for_all",
545
+ "rest_for_one"
546
+ ]);
547
+ Schema.Struct({
548
+ strategy: RestartStrategy,
549
+ totalChildren: Schema.Int.pipe(Schema.check(Schema.isBetween({
550
+ minimum: 1,
551
+ maximum: MAX_CHILDREN_CEILING
552
+ }))),
553
+ failedIndex: Schema.Int.pipe(Schema.check(Schema.isBetween({
554
+ minimum: 0,
555
+ maximum: MAX_CHILDREN_CEILING
556
+ }))),
557
+ exitSuccess: Schema.Boolean,
558
+ intensityExceeded: Schema.Boolean
559
+ }).pipe(Schema.check(Schema.makeFilter(failedIndexAddressesAChild, { message: "failedIndex must be < totalChildren" })));
387
560
  //#endregion
388
- //#region src/internal/supervision-epoch.schema.ts
561
+ //#region src/internal/SupervisionEpoch.schema.ts
389
562
  const EpochStepTypeId = Symbol.for("@systemfsoftware/effect-daemon/EpochStep");
390
563
  var StopEpoch = class extends Schema.TaggedClass()("StopEpoch", {}) {
391
564
  [EpochStepTypeId] = EpochStepTypeId;
@@ -396,7 +569,11 @@ var RestartEpoch = class extends Schema.TaggedClass()("RestartEpoch", {}) {
396
569
  var CooldownEpoch = class extends Schema.TaggedClass()("CooldownEpoch", {}) {
397
570
  [EpochStepTypeId] = EpochStepTypeId;
398
571
  };
399
- Schema.Union(StopEpoch, RestartEpoch, CooldownEpoch);
572
+ Schema.Union([
573
+ StopEpoch,
574
+ RestartEpoch,
575
+ CooldownEpoch
576
+ ]);
400
577
  const SupervisionEpochResultTypeId = Symbol.for("@systemfsoftware/effect-daemon/SupervisionEpochResult");
401
578
  var StopSupervision = class extends Schema.TaggedClass()("StopSupervision", {}) {
402
579
  [SupervisionEpochResultTypeId] = SupervisionEpochResultTypeId;
@@ -404,76 +581,115 @@ var StopSupervision = class extends Schema.TaggedClass()("StopSupervision", {})
404
581
  var ContinueSupervision = class extends Schema.TaggedClass()("ContinueSupervision", {}) {
405
582
  [SupervisionEpochResultTypeId] = SupervisionEpochResultTypeId;
406
583
  };
407
- Schema.Union(StopSupervision, ContinueSupervision);
584
+ Schema.Union([StopSupervision, ContinueSupervision]);
408
585
  //#endregion
409
- //#region src/internal/supervisor-body.executor.ts
410
- var SupervisorBodyExecutorDeps = class extends Context.Tag("@systemfsoftware/effect-daemon-spec/internal/supervisor-body.executor/SupervisorBodyExecutorDeps")() {};
586
+ //#region src/internal/SupervisorBodyExecutor.ts
411
587
  const handleExhausted = (ctx, cause) => Effect.gen(function* () {
412
- yield* Effect.zipRight(ctx.health.healthy.close, Metric.set(Metric.tagged(Metric.tagged(healthStateGauge, "daemon", ctx.name), "latch", "healthy"), 0));
588
+ yield* Effect.andThen(ctx.health.healthy.close, Metric.update(Metric.withAttributes(healthStateGauge, {
589
+ daemon: ctx.name,
590
+ latch: "healthy"
591
+ }), 0));
413
592
  yield* ctx.reportExhausted(cause);
414
- return new CooldownEpoch();
593
+ return CooldownEpoch.make();
415
594
  });
416
595
  const handleRestart = (ctx, cause, onSignal) => Effect.gen(function* () {
417
596
  yield* ctx.reportRestart(cause);
418
597
  yield* onSignal;
419
- return new RestartEpoch();
598
+ return RestartEpoch.make();
420
599
  });
421
- const reopenHealthyAfterCooldown = (ctx) => Effect.zipRight(ctx.health.healthy.open, Metric.set(Metric.tagged(Metric.tagged(healthStateGauge, "daemon", ctx.name), "latch", "healthy"), 1));
600
+ /**
601
+ * The restart decision, as a description whose phases chain by type and read in the order
602
+ * they run.
603
+ *
604
+ * The read is a bump-and-report: recording a restart is how the current rate is obtained, so
605
+ * the mutation is a product gathered across the read's interior rather than a write standing
606
+ * before a decision. That is what keeps this one layer instead of two, and it is why
607
+ * `intensity.record` sits where it does.
608
+ *
609
+ * `encode` is the identity because nothing needs shaping — the decision is already what the
610
+ * write consumes — and the write only dispatches over the tags the decision produced.
611
+ *
612
+ * A description is built per failure because the write needs that failure's context, and the
613
+ * phase signatures hand the command to the read alone. Restarts are rare, so the allocation is
614
+ * paid only when a supervised child has actually died.
615
+ */
616
+ const restartDescription = (spec) => pipe(Cell.read((intensity) => Effect.andThen(intensity.record, intensity.isExceeded)), Cell.decode((intensityExceeded) => Result.succeed({
617
+ strategy: spec.strategy,
618
+ totalChildren: spec.totalChildren,
619
+ failedIndex: spec.failedIndex,
620
+ exitSuccess: false,
621
+ intensityExceeded
622
+ })), Cell.decide(decideRestart), Cell.encode((outcome) => outcome), Cell.write((outcome) => Result.match(outcome, {
623
+ onFailure: () => handleExhausted(spec.ctx, spec.cause),
624
+ onSuccess: (right) => Match.value(right).pipe(Match.tag("Continue", () => Effect.succeed(StopEpoch.make())), Match.tag("Restart", (decision) => handleRestart(spec.ctx, spec.cause, spec.onRestart(decision))), Match.exhaustive)
625
+ })));
626
+ const reopenHealthyAfterCooldown = (ctx) => Effect.andThen(ctx.health.healthy.open, Metric.update(Metric.withAttributes(healthStateGauge, {
627
+ daemon: ctx.name,
628
+ latch: "healthy"
629
+ }), 1));
422
630
  const runSupervisionEpochWithBackoff = (attempt, ctx) => Effect.gen(function* () {
423
- const driver = yield* Schedule.driver(ctx.policy.backoff);
631
+ const advance = yield* Schedule.toStep(ctx.policy.backoff);
424
632
  const loop = () => Effect.gen(function* () {
425
- const step = yield* attempt.pipe(Effect.scoped);
426
- return yield* Match.value(step).pipe(Match.tag("StopEpoch", () => Effect.succeed(new StopSupervision())), Match.tag("CooldownEpoch", () => Effect.gen(function* () {
633
+ const epochStep = yield* attempt.pipe(Effect.scoped);
634
+ return yield* Match.value(epochStep).pipe(Match.tag("StopEpoch", () => Effect.succeed(StopSupervision.make())), Match.tag("CooldownEpoch", () => Effect.gen(function* () {
427
635
  yield* Effect.sleep(ctx.policy.cooldown);
428
636
  yield* reopenHealthyAfterCooldown(ctx);
429
- return new ContinueSupervision();
637
+ return ContinueSupervision.make();
430
638
  })), Match.tag("RestartEpoch", () => Effect.gen(function* () {
431
- const stepped = yield* Effect.either(driver.next(void 0));
432
- if (Either.isLeft(stepped)) return new StopSupervision();
639
+ const now = yield* Clock.currentTimeMillis;
640
+ const pulled = yield* Effect.result(advance(now, void 0));
641
+ if (Result.isFailure(pulled)) return StopSupervision.make();
642
+ const [, delay] = pulled.success;
643
+ yield* Effect.sleep(delay);
433
644
  return yield* loop();
434
645
  })), Match.exhaustive);
435
646
  });
436
647
  return yield* loop();
437
648
  });
438
649
  const openAllReady = (ctx) => Effect.gen(function* () {
439
- yield* Effect.yieldNow();
650
+ yield* Effect.yieldNow;
440
651
  yield* Effect.forEach(ctx.booted, (b) => b.health.ready.await, { concurrency: "unbounded" });
441
- yield* Effect.zipRight(ctx.health.ready.open, Metric.set(Metric.tagged(Metric.tagged(healthStateGauge, "daemon", ctx.name), "latch", "ready"), 1));
652
+ yield* Effect.andThen(ctx.health.ready.open, Metric.update(Metric.withAttributes(healthStateGauge, {
653
+ daemon: ctx.name,
654
+ latch: "ready"
655
+ }), 1));
656
+ });
657
+ /**
658
+ * Records one restart against the child's intensity tracker and reports whether the
659
+ * child's restart budget is now exceeded. A child without a bounded intensity policy
660
+ * never hits the budget.
661
+ */
662
+ const isChildIntensityBudgetDone = (tracker) => Option.match(tracker, {
663
+ onNone: () => Effect.succeed(false),
664
+ onSome: (ci) => Effect.gen(function* () {
665
+ yield* ci.record;
666
+ return yield* ci.isExceeded;
667
+ })
442
668
  });
443
669
  const superviseChild = (ctx, child, idx) => Effect.gen(function* () {
444
- const childIntensityOpt = yield* Option.match(Option.fromNullable(child.childPolicy.intensity), {
670
+ const childIntensityOpt = yield* Option.match(Option.fromNullishOr(child.childPolicy.intensity), {
445
671
  onNone: () => Effect.succeed(Option.none()),
446
- onSome: (cfg) => Effect.map(make(new BoundedIntensity(cfg)), Option.some)
672
+ onSome: (cfg) => Effect.map(make(cfg.restarts, cfg.window), Option.some)
447
673
  });
448
674
  const loop = () => Effect.gen(function* () {
449
675
  const supIntensity = yield* ctx.intensityEff;
450
676
  const attempt = Effect.gen(function* () {
451
677
  yield* ctx.health.paused.await;
452
- const fiber = yield* Effect.forkScoped(child.run);
678
+ const fiber = yield* Effect.forkScoped(child.run, { startImmediately: true });
453
679
  const exit = yield* Fiber.await(fiber);
454
680
  if (!Exit.isSuccess(exit)) {
455
- if (child.childPolicy.restart === "temporary") return new StopEpoch();
456
- if (yield* Option.match(childIntensityOpt, {
457
- onNone: () => Effect.succeed(false),
458
- onSome: (ci) => Effect.gen(function* () {
459
- yield* ci.record;
460
- return yield* ci.isExceeded;
461
- })
462
- })) return new StopEpoch();
463
- yield* supIntensity.record;
464
- const decision = decideRestart({
681
+ if (child.childPolicy.restart === "temporary") return StopEpoch.make();
682
+ if (yield* isChildIntensityBudgetDone(childIntensityOpt)) return StopEpoch.make();
683
+ return yield* Cell.apply(restartDescription({
465
684
  strategy: "one_for_one",
466
- exitSuccess: false,
467
- intensityExceeded: yield* supIntensity.isExceeded,
468
685
  failedIndex: idx,
469
- totalChildren: ctx.booted.length
470
- });
471
- return yield* Either.match(decision, {
472
- onLeft: () => handleExhausted(ctx, exit.cause),
473
- onRight: (right) => Match.value(right).pipe(Match.tag("Continue", () => Effect.succeed(new StopEpoch())), Match.tag("Restart", () => handleRestart(ctx, exit.cause, Effect.void)), Match.exhaustive)
474
- });
686
+ totalChildren: ctx.booted.length,
687
+ ctx,
688
+ cause: exit.cause,
689
+ onRestart: () => Effect.void
690
+ }), supIntensity);
475
691
  }
476
- return new StopEpoch();
692
+ return StopEpoch.make();
477
693
  });
478
694
  const epochResult = yield* runSupervisionEpochWithBackoff(attempt, ctx);
479
695
  return yield* Match.value(epochResult).pipe(Match.tag("ContinueSupervision", () => loop()), Match.tag("StopSupervision", () => Effect.void), Match.exhaustive);
@@ -481,54 +697,44 @@ const superviseChild = (ctx, child, idx) => Effect.gen(function* () {
481
697
  return yield* loop();
482
698
  });
483
699
  const runIndependent = (ctx) => Effect.gen(function* () {
484
- const fibers = yield* Effect.forEach(ctx.booted, (child, childIdx) => Effect.forkScoped(superviseChild(ctx, child, childIdx)));
485
- yield* Effect.yieldNow();
700
+ const fibers = yield* Effect.forEach(ctx.booted, (child, childIdx) => Effect.forkScoped(superviseChild(ctx, child, childIdx), { startImmediately: true }));
701
+ yield* Effect.yieldNow;
486
702
  yield* openAllReady(ctx);
487
703
  yield* Effect.forEach(fibers, (f) => Fiber.await(f), { concurrency: "unbounded" });
488
704
  });
489
705
  const runGroup = (strategy, ctx) => Effect.gen(function* () {
490
706
  const loop = () => Effect.gen(function* () {
491
707
  const intensity = yield* ctx.intensityEff;
492
- const childIntensityTrackers = yield* Effect.forEach(ctx.booted, (b) => Option.match(Option.fromNullable(b.childPolicy.intensity), {
708
+ const childIntensityTrackers = yield* Effect.forEach(ctx.booted, (b) => Option.match(Option.fromNullishOr(b.childPolicy.intensity), {
493
709
  onNone: () => Effect.succeed(Option.none()),
494
- onSome: (cfg) => Effect.map(make(new BoundedIntensity(cfg)), Option.some)
710
+ onSome: (cfg) => Effect.map(make(cfg.restarts, cfg.window), Option.some)
495
711
  }));
496
712
  const cursor = yield* Ref.make(0);
497
713
  const attempt = Effect.gen(function* () {
498
714
  yield* ctx.health.paused.await;
499
715
  const startIdx = yield* Ref.get(cursor);
500
716
  const slice = ctx.booted.slice(startIdx);
501
- const fibers = yield* Effect.forEach(slice, (c) => Effect.forkScoped(c.run));
502
- yield* Effect.yieldNow();
503
- yield* Effect.forkScoped(openAllReady(ctx));
717
+ const fibers = yield* Effect.forEach(slice, (c) => Effect.forkScoped(c.run, { startImmediately: true }));
718
+ yield* Effect.yieldNow;
719
+ yield* Effect.forkScoped(openAllReady(ctx), { startImmediately: true });
504
720
  const [failedOffset, firstExit] = yield* raceForExit(fibers);
505
721
  if (!Exit.isSuccess(firstExit)) {
506
722
  const failedIdx = startIdx + failedOffset;
507
- const failedBootedOpt = Option.fromNullable(ctx.booted[failedIdx]);
508
- if (Option.isNone(failedBootedOpt)) return new StopEpoch();
509
- if (failedBootedOpt.value.childPolicy.restart === "temporary") return new StopEpoch();
510
- const cIntForFailed = Option.flatten(Array$1.get(childIntensityTrackers, failedIdx));
511
- if (yield* Option.match(cIntForFailed, {
512
- onNone: () => Effect.succeed(false),
513
- onSome: (cInt) => Effect.gen(function* () {
514
- yield* cInt.record;
515
- return yield* cInt.isExceeded;
516
- })
517
- })) return new StopEpoch();
518
- yield* intensity.record;
519
- const decision = decideRestart({
723
+ const failedBootedOpt = Option.fromNullishOr(ctx.booted[failedIdx]);
724
+ if (Option.isNone(failedBootedOpt)) return StopEpoch.make();
725
+ if (failedBootedOpt.value.childPolicy.restart === "temporary") return StopEpoch.make();
726
+ const cIntForFailed = Option.flatten(Array.get(childIntensityTrackers, failedIdx));
727
+ if (yield* isChildIntensityBudgetDone(cIntForFailed)) return StopEpoch.make();
728
+ return yield* Cell.apply(restartDescription({
520
729
  strategy,
521
- exitSuccess: false,
522
- intensityExceeded: yield* intensity.isExceeded,
730
+ failedIndex: failedIdx,
523
731
  totalChildren: ctx.booted.length,
524
- failedIndex: failedIdx
525
- });
526
- return yield* Either.match(decision, {
527
- onLeft: () => handleExhausted(ctx, firstExit.cause),
528
- onRight: (right) => Match.value(right).pipe(Match.tag("Continue", () => Effect.succeed(new StopEpoch())), Match.tag("Restart", (restartDecision) => handleRestart(ctx, firstExit.cause, Ref.set(cursor, restartDecision.indices[0]))), Match.exhaustive)
529
- });
732
+ ctx,
733
+ cause: firstExit.cause,
734
+ onRestart: (decision) => Ref.set(cursor, decision.indices[0])
735
+ }), intensity);
530
736
  }
531
- return new StopEpoch();
737
+ return StopEpoch.make();
532
738
  });
533
739
  const epochResult = yield* runSupervisionEpochWithBackoff(attempt, ctx);
534
740
  return yield* Match.value(epochResult).pipe(Match.tag("ContinueSupervision", () => loop()), Match.tag("StopSupervision", () => Effect.void), Match.exhaustive);
@@ -536,24 +742,23 @@ const runGroup = (strategy, ctx) => Effect.gen(function* () {
536
742
  return yield* loop();
537
743
  });
538
744
  const superviseTree = (strategy, ctx) => Match.value(strategy).pipe(Match.when("one_for_one", () => runIndependent(ctx)), Match.when("one_for_all", () => runGroup("one_for_all", ctx)), Match.when("rest_for_one", () => runGroup("rest_for_one", ctx)), Match.exhaustive);
539
- const buildSupervisorBody = (sup, health, booted) => Effect.gen(function* () {
745
+ const intensityTracker = (intensity) => Match.value(intensity).pipe(Match.tag("Unbounded", () => Effect.succeed(neverExceeds)), Match.tag("Bounded", ({ restarts, window }) => make(restarts, window)), Match.exhaustive);
746
+ const buildSupervisorBody = (sup, health, booted, reporter) => Effect.gen(function* () {
540
747
  const policy = yield* sup.supervision;
541
- const tracker = make(policy.intensity).pipe(Effect.runSync);
748
+ const tracker = yield* intensityTracker(policy.intensity);
542
749
  const intensityEff = Effect.succeed(tracker);
543
750
  const reportRestart = (cause) => Effect.gen(function* () {
544
- const reporter = yield* SupervisorBodyExecutorDeps;
545
- yield* Metric.increment(Metric.tagged(supervisorRestartsCounter, "supervisor", sup.name));
751
+ yield* Metric.update(Metric.withAttributes(supervisorRestartsCounter, { supervisor: sup.name }), 1);
546
752
  yield* reporter.onRestart(sup.name, cause);
547
- yield* Option.match(Option.fromNullable(sup.reporter.onRestart), {
753
+ yield* Option.match(Option.fromNullishOr(sup.reporter.onRestart), {
548
754
  onNone: () => Effect.void,
549
755
  onSome: (fn) => fn(cause)
550
756
  });
551
757
  });
552
758
  const reportExhausted = (cause) => Effect.gen(function* () {
553
- const reporter = yield* SupervisorBodyExecutorDeps;
554
- yield* Metric.increment(Metric.tagged(supervisorExhaustionsCounter, "supervisor", sup.name));
759
+ yield* Metric.update(Metric.withAttributes(supervisorExhaustionsCounter, { supervisor: sup.name }), 1);
555
760
  yield* reporter.onExhausted(sup.name, cause);
556
- yield* Option.match(Option.fromNullable(sup.reporter.onExhausted), {
761
+ yield* Option.match(Option.fromNullishOr(sup.reporter.onExhausted), {
557
762
  onNone: () => Effect.void,
558
763
  onSome: (fn) => fn(cause)
559
764
  });
@@ -570,7 +775,7 @@ const buildSupervisorBody = (sup, health, booted) => Effect.gen(function* () {
570
775
  yield* Effect.andThen(health.paused.await, runStrategy);
571
776
  });
572
777
  const isWorker = (x) => WorkerTypeId in x;
573
- const bootChild = (child) => Effect.gen(function* () {
778
+ const bootChild = (child, reporter) => Effect.gen(function* () {
574
779
  if (isWorker(child)) {
575
780
  const health = yield* allocateWorkerHealth(child.name);
576
781
  const loop = buildWorkerLoop(child, health, healthStateGauge).pipe(Effect.orDie);
@@ -581,9 +786,9 @@ const bootChild = (child) => Effect.gen(function* () {
581
786
  childPolicy: child.child
582
787
  };
583
788
  }
584
- const bootedChildren = yield* Effect.forEach(child.children, bootChild);
789
+ const bootedChildren = yield* Effect.forEach(child.children, (c) => bootChild(c, reporter));
585
790
  const health = yield* allocateSupervisorHealth(child.name, bootedChildren.map((b) => b.health));
586
- const body = buildSupervisorBody(child, health, bootedChildren).pipe(Effect.orDie);
791
+ const body = buildSupervisorBody(child, health, bootedChildren, reporter).pipe(Effect.orDie);
587
792
  return {
588
793
  name: child.name,
589
794
  health,
@@ -591,171 +796,64 @@ const bootChild = (child) => Effect.gen(function* () {
591
796
  childPolicy: {}
592
797
  };
593
798
  });
594
- const supervisor = (s) => Effect.gen(function* () {
595
- const booted = yield* Effect.forEach(s.children, bootChild);
799
+ const supervisor$1 = (s, reporter, binding) => Effect.gen(function* () {
800
+ const booted = yield* Effect.forEach(s.children, (child) => bootChild(child, reporter));
596
801
  const health = yield* allocateSupervisorHealth(s.name, booted.map((b) => b.health));
597
- const body = buildSupervisorBody(s, health, booted).pipe(Effect.orDie);
598
- let locked;
599
- if (isModeNone(s.lock)) locked = body;
600
- else if (s.lock.mode === "required") locked = withLeaderLock(body, {
601
- key: s.lock.key,
602
- mode: "required",
603
- acquireRetryBackoff: s.lock.acquireRetryBackoff
604
- });
605
- else locked = withLeaderLock(body, {
606
- key: s.lock.key,
607
- mode: "optional"
608
- });
609
- yield* Effect.forkScoped(locked.pipe(Effect.orDie));
802
+ const body = buildSupervisorBody(s, health, booted, reporter).pipe(Effect.orDie);
803
+ const locked = withLockByMode(body, binding);
804
+ yield* Effect.forkScoped(locked.pipe(Effect.orDie), { startImmediately: true });
610
805
  return health;
611
806
  });
612
- Context.Tag("@systemfsoftware/effect-daemon-spec/internal/build-dynamic.executor/BuildDynamicExecutorDeps")();
613
- const buildDynamic = (spec, health) => Effect.gen(function* () {
614
- const state = yield* Ref.make({
615
- nextId: 0,
616
- children: HashMap.empty()
617
- });
618
- const startChildImpl = (args) => Effect.gen(function* () {
619
- const childWorker = spec.child(args);
620
- const loop = buildWorkerLoop(childWorker, yield* allocateWorkerHealth(childWorker.name), healthStateGauge).pipe(Effect.orDie);
621
- const removed = yield* Effect.makeLatch(false);
622
- const reservedId = yield* Ref.modify(state, (current) => {
623
- if (HashMap.size(current.children) >= spec.maxChildren) return [Option.none(), current];
624
- const children = HashMap.set(current.children, current.nextId, {
625
- fiber: Option.none(),
626
- removed
627
- });
628
- return [Option.some(current.nextId), {
629
- nextId: current.nextId + 1,
630
- children
631
- }];
632
- });
633
- if (Option.isNone(reservedId)) return yield* new DynamicLimitExceeded({ limit: spec.maxChildren });
634
- const id = reservedId.value;
635
- yield* Metric.set(supervisorChildrenGauge, HashMap.size(yield* Ref.get(state).pipe(Effect.map((s) => s.children))));
636
- const cleanup = Effect.gen(function* () {
637
- const count = yield* Ref.modify(state, (current) => {
638
- const children = HashMap.remove(current.children, id);
639
- return [HashMap.size(children), {
640
- ...current,
641
- children
642
- }];
643
- });
644
- yield* Metric.set(supervisorChildrenGauge, count);
645
- yield* removed.open;
646
- }).pipe(Effect.asVoid);
647
- const fiber = yield* Effect.forkScoped(loop.pipe(Effect.ensuring(cleanup)));
648
- const count = yield* Ref.modify(state, (current) => {
649
- const childOpt = HashMap.get(current.children, id);
650
- if (Option.isNone(childOpt)) return [HashMap.size(current.children), current];
651
- const children = HashMap.set(current.children, id, {
652
- ...childOpt.value,
653
- fiber: Option.some(fiber)
654
- });
655
- return [HashMap.size(children), {
656
- nextId: current.nextId,
657
- children
658
- }];
659
- });
660
- yield* Metric.set(supervisorChildrenGauge, count);
661
- return {
662
- id,
663
- removed: removed.await
664
- };
665
- });
666
- const stopChildImpl = (ref) => Effect.gen(function* () {
667
- const [stateOpt, count] = yield* Ref.modify(state, (current) => {
668
- const found = HashMap.get(current.children, ref.id);
669
- const children = HashMap.remove(current.children, ref.id);
670
- return [[found, HashMap.size(children)], {
671
- ...current,
672
- children
673
- }];
674
- });
675
- if (Option.isSome(stateOpt)) {
676
- const { fiber, removed } = stateOpt.value;
677
- yield* Option.match(fiber, {
678
- onNone: () => Effect.void,
679
- onSome: (running) => Effect.gen(function* () {
680
- yield* Fiber.interrupt(running);
681
- yield* Fiber.await(running);
682
- })
683
- });
684
- yield* Metric.set(supervisorChildrenGauge, count);
685
- yield* removed.open;
686
- }
687
- });
688
- const countImpl = Ref.get(state).pipe(Effect.map((current) => HashMap.size(current.children)));
689
- yield* Effect.zipRight(health.ready.open, Metric.set(Metric.tagged(Metric.tagged(healthStateGauge, "daemon", spec.name), "latch", "ready"), 1));
690
- return {
691
- health,
692
- startChild: startChildImpl,
693
- stopChild: stopChildImpl,
694
- count: countImpl
695
- };
696
- });
697
- const dynamic$2 = (spec) => Effect.gen(function* () {
698
- const health = yield* allocateSupervisorHealth(spec.name, []);
699
- return yield* buildDynamic(spec, health);
700
- });
701
807
  //#endregion
702
- //#region src/internal/supervision-leader.state.ts
703
- var LeaderConfig = class extends Context.Reference()("@systemfsoftware/effect-daemon-spec/LeaderConfig", { defaultValue: () => ({
808
+ //#region src/internal/SupervisionLeader.ts
809
+ const LeaderConfig = Context.Reference("@systemfsoftware/effect-daemon-spec/LeaderConfig", { defaultValue: () => ({
704
810
  backoffBase: Duration.seconds(1),
705
- intensity: new UnboundedIntensity(),
811
+ intensity: UnboundedIntensity.make(),
706
812
  cooldown: Duration.zero
707
- }) }) {};
813
+ }) });
708
814
  //#endregion
709
- //#region src/internal/supervision-task.state.ts
710
- var TaskConfig = class extends Context.Reference()("@systemfsoftware/effect-daemon-spec/TaskConfig", { defaultValue: () => ({
815
+ //#region src/internal/SupervisionTask.ts
816
+ const TaskConfig = Context.Reference("@systemfsoftware/effect-daemon-spec/TaskConfig", { defaultValue: () => ({
711
817
  backoffBase: Duration.seconds(1),
712
- intensity: new UnboundedIntensity(),
818
+ intensity: UnboundedIntensity.make(),
713
819
  cooldown: Duration.zero
714
- }) }) {};
820
+ }) });
715
821
  //#endregion
716
- //#region src/internal/supervision-worker.state.ts
717
- var WorkerConfig = class extends Context.Reference()("@systemfsoftware/effect-daemon-spec/WorkerConfig", { defaultValue: () => ({
822
+ //#region src/internal/SupervisionWorker.ts
823
+ const WorkerConfig = Context.Reference("@systemfsoftware/effect-daemon-spec/WorkerConfig", { defaultValue: () => ({
718
824
  backoffBase: Duration.seconds(10),
719
- intensity: new BoundedIntensity({
825
+ intensity: BoundedIntensity.make({
720
826
  restarts: 10,
721
827
  window: Duration.seconds(60)
722
828
  }),
723
829
  cooldown: Duration.seconds(30)
724
- }) }) {};
830
+ }) });
725
831
  //#endregion
726
- //#region src/supervision-custom.kernel.ts
832
+ //#region src/SupervisionCustom.ts
727
833
  const custom = (policy) => Effect.succeed(policy);
728
834
  //#endregion
729
- //#region src/supervision-leader.kernel.ts
835
+ //#region src/SupervisionLeader.ts
730
836
  const leader$1 = (config, cap) => Effect.succeed({
731
837
  intensity: config.intensity,
732
838
  backoff: cappedBackoff(config.backoffBase, cap),
733
839
  cooldown: config.cooldown
734
840
  });
735
841
  //#endregion
736
- //#region src/supervision-task.kernel.ts
842
+ //#region src/SupervisionTask.ts
737
843
  const task$1 = (config, budget) => Effect.succeed({
738
844
  intensity: config.intensity,
739
- backoff: Schedule.exponential(config.backoffBase).pipe(Schedule.jittered, Schedule.upTo(budget)),
845
+ backoff: Schedule.exponential(config.backoffBase).pipe(Schedule.jittered, Schedule.upTo({ duration: budget })),
740
846
  cooldown: config.cooldown
741
847
  });
742
848
  //#endregion
743
- //#region src/supervision-worker.kernel.ts
849
+ //#region src/SupervisionWorker.ts
744
850
  const worker$1 = (config, cap) => Effect.succeed({
745
851
  intensity: config.intensity,
746
852
  backoff: cappedBackoff(config.backoffBase, cap),
747
853
  cooldown: config.cooldown
748
854
  });
749
855
  //#endregion
750
- //#region src/supervisor-dynamic.kernel.ts
751
- const dynamic$1 = (opts) => ({
752
- [DynamicSpecTypeId]: DynamicSpecTypeId,
753
- name: opts.name,
754
- child: opts.child,
755
- maxChildren: opts.maxChildren ?? 1e3
756
- });
757
- //#endregion
758
- //#region src/supervisor-one-for-all.kernel.ts
856
+ //#region src/SupervisorOneForAll.ts
759
857
  const oneForAll$1 = (opts) => ({
760
858
  [SupervisorTypeId]: SupervisorTypeId,
761
859
  name: opts.name,
@@ -766,7 +864,7 @@ const oneForAll$1 = (opts) => ({
766
864
  reporter: opts.reporter ?? {}
767
865
  });
768
866
  //#endregion
769
- //#region src/supervisor-one-for-one.kernel.ts
867
+ //#region src/SupervisorOneForOne.ts
770
868
  const oneForOne$1 = (opts) => ({
771
869
  [SupervisorTypeId]: SupervisorTypeId,
772
870
  name: opts.name,
@@ -777,7 +875,7 @@ const oneForOne$1 = (opts) => ({
777
875
  reporter: opts.reporter ?? {}
778
876
  });
779
877
  //#endregion
780
- //#region src/supervisor-rest-for-one.kernel.ts
878
+ //#region src/SupervisorRestForOne.ts
781
879
  const restForOne$1 = (opts) => ({
782
880
  [SupervisorTypeId]: SupervisorTypeId,
783
881
  name: opts.name,
@@ -797,10 +895,49 @@ const Daemon = {
797
895
  stream,
798
896
  subscription
799
897
  };
898
+ /**
899
+ * Boots a worker. The leader-lock capability is acquired here, at the composition
900
+ * root, and handed down as part of the lock binding: the executor behind this
901
+ * entry point never sees the tag. A worker whose lock is `{ mode: 'none' }`
902
+ * takes no lock at all.
903
+ */
904
+ const worker = (w) => Effect.gen(function* () {
905
+ let binding;
906
+ if (isModeNone(w.lock)) binding = { kind: "unlocked" };
907
+ else {
908
+ const lock = yield* LeaderLock;
909
+ binding = {
910
+ kind: "locked",
911
+ spec: w.lock,
912
+ lock
913
+ };
914
+ }
915
+ return yield* worker$2(w, binding);
916
+ });
917
+ /**
918
+ * The supervisor: acquires the `DaemonReporter` and — unless the lock mode is none —
919
+ * the `LeaderLock` capabilities at the composition root, then hands them down to the
920
+ * supervisor body via the lock binding. The body itself only ever sees the service
921
+ * values.
922
+ */
923
+ const supervisor = (s) => Effect.gen(function* () {
924
+ const reporter = yield* DaemonReporter;
925
+ let binding;
926
+ if (isModeNone(s.lock)) binding = { kind: "unlocked" };
927
+ else {
928
+ const lock = yield* LeaderLock;
929
+ binding = {
930
+ kind: "locked",
931
+ spec: s.lock,
932
+ lock
933
+ };
934
+ }
935
+ return yield* supervisor$1(s, reporter, binding);
936
+ });
800
937
  const run = {
801
938
  worker,
802
939
  supervisor,
803
- dynamic: dynamic$2
940
+ dynamic: dynamic$1
804
941
  };
805
942
  const leader = (cap) => Effect.flatMap(LeaderConfig, (config) => leader$1(config, cap));
806
943
  const task = (budget) => Effect.flatMap(TaskConfig, (config) => task$1(config, budget));
@@ -811,19 +948,12 @@ const Supervision = {
811
948
  task,
812
949
  custom
813
950
  };
814
- const dynamic = (opts) => dynamic$1(opts);
951
+ const dynamic = (opts) => dynamic$2({
952
+ ...opts,
953
+ maxChildren: opts.maxChildren ?? MaxChildren.make(1e3)
954
+ });
815
955
  const oneForAll = (opts) => oneForAll$1(opts);
816
956
  const oneForOne = (opts) => oneForOne$1(opts);
817
957
  const restForOne = (opts) => restForOne$1(opts);
818
- const WithLeaderLockExecutorLive = Layer.effect(WithLeaderLockExecutorDeps, Effect.gen(function* () {
819
- return { withLock: (yield* LeaderLock).withLock };
820
- }));
821
- const SupervisorBodyExecutorLive = Layer.effect(SupervisorBodyExecutorDeps, Effect.gen(function* () {
822
- const reporter = yield* DaemonReporter;
823
- return {
824
- onRestart: reporter.onRestart,
825
- onExhausted: reporter.onExhausted
826
- };
827
- }));
828
958
  //#endregion
829
- export { BoundedIntensity, ChildPolicyConfig, Daemon, DaemonReporter, DynamicLimitExceeded, DynamicSpecTypeId, Intensity, IntensityConfig, LeaderConfig, LeaderLock, LeaderLockFromPrimitive, LeaderLockInfraError, LeaderLockNotAcquired, LockPolicyConfig, LockPrimitive, LockPrimitiveError, Noop, Supervision, SupervisorBodyExecutorDeps, SupervisorBodyExecutorLive, SupervisorTypeId, TaskConfig, TickPolicyConfig, UnboundedIntensity, WithLeaderLockExecutorDeps, WithLeaderLockExecutorLive, WorkerConfig, WorkerTypeId, cappedBackoff, dynamic, healthStateGauge, leader, oneForAll, oneForOne, poll, restForOne, run, stream, subscription, supervision, supervisor, supervisorChildrenGauge, supervisorExhaustionsCounter, supervisorRestartsCounter, task, withLeaderLock, worker };
959
+ export { BoundedIntensity, ChildPolicyConfig, Daemon, DaemonReporter, DynamicLimitExceeded, DynamicSpecTypeId, Intensity, IntensityConfig, LeaderConfig, LeaderLock, LeaderLockFromPrimitive, LeaderLockInfraError, LeaderLockNotAcquired, LockPolicyConfig, LockPrimitive, LockPrimitiveError, MaxChildren, Noop, Supervision, SupervisorTypeId, TaskConfig, TickPolicyConfig, UnboundedIntensity, WorkerConfig, WorkerTypeId, cappedBackoff, dynamic, healthStateGauge, leader, oneForAll, oneForOne, poll, restForOne, run, stream, subscription, supervision, supervisor, supervisorChildrenGauge, supervisorExhaustionsCounter, supervisorRestartsCounter, task, withLeaderLock, worker };