@systemfsoftware/effect-daemon-spec 1.0.0 → 3.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
@@ -93,6 +93,17 @@ const Noop = Layer.succeed(DaemonReporter, DaemonReporter.of({
93
93
  onExhausted: () => Effect.void
94
94
  }));
95
95
  //#endregion
96
+ //#region src/LoopTags.ts
97
+ /**
98
+ * Discriminants on the public Worker loop variants.
99
+ *
100
+ * These tags are part of the published type identity of `poll`, `stream`,
101
+ * and `subscription`. They are not unpublished wiring.
102
+ */
103
+ const PollLoopTag = { _tag: "Poll" };
104
+ const StreamLoopTag = { _tag: "Stream" };
105
+ const SubscriptionLoopTag = { _tag: "Subscription" };
106
+ //#endregion
96
107
  //#region src/DaemonPoll.ts
97
108
  const poll$1 = (opts) => {
98
109
  if (typeof opts.prereq === "undefined") {
@@ -101,7 +112,7 @@ const poll$1 = (opts) => {
101
112
  [WorkerTypeId]: WorkerTypeId,
102
113
  name: opts.name,
103
114
  loop: {
104
- _tag: "Poll",
115
+ ...PollLoopTag,
105
116
  gate,
106
117
  interval: opts.interval
107
118
  },
@@ -117,7 +128,7 @@ const poll$1 = (opts) => {
117
128
  [WorkerTypeId]: WorkerTypeId,
118
129
  name: opts.name,
119
130
  loop: {
120
- _tag: "Poll",
131
+ ...PollLoopTag,
121
132
  gate,
122
133
  interval: opts.interval
123
134
  },
@@ -133,7 +144,7 @@ const stream$1 = (opts) => ({
133
144
  [WorkerTypeId]: WorkerTypeId,
134
145
  name: opts.name,
135
146
  loop: {
136
- _tag: "Stream",
147
+ ...StreamLoopTag,
137
148
  stream: opts.stream
138
149
  },
139
150
  child: opts.child ?? {},
@@ -147,7 +158,7 @@ const subscription$1 = (opts) => ({
147
158
  [WorkerTypeId]: WorkerTypeId,
148
159
  name: opts.name,
149
160
  loop: {
150
- _tag: "Subscription",
161
+ ...SubscriptionLoopTag,
151
162
  acquire: Effect.asVoid(opts.acquire)
152
163
  },
153
164
  child: opts.child ?? {},
@@ -156,9 +167,6 @@ const subscription$1 = (opts) => ({
156
167
  lock: opts.lock
157
168
  });
158
169
  //#endregion
159
- //#region src/LeaderLock.ts
160
- const isModeNone = (lock) => lock.mode === "none";
161
- //#endregion
162
170
  //#region src/LeaderLock.schema.ts
163
171
  var LeaderLockNotAcquired = class extends Schema.TaggedError()("LeaderLockNotAcquired", { key: Schema.String }) {};
164
172
  var LeaderLockInfraError = class extends Schema.TaggedError()("LeaderLockInfraError", {
@@ -193,7 +201,35 @@ const LeaderLockFromPrimitive = Layer.effect(LeaderLock, Effect.gen(function* ()
193
201
  })) });
194
202
  }));
195
203
  //#endregion
204
+ //#region src/internal/AllocateSupervisorHealth.ts
205
+ /** @internal */
206
+ const allocateSupervisorHealth = (name, children) => Effect.gen(function* () {
207
+ const ready = yield* Latch.make(false);
208
+ const healthy = yield* Latch.make(true);
209
+ const paused = yield* Latch.make(true);
210
+ yield* Metric.update(Metric.withAttributes(healthStateGauge, {
211
+ daemon: name,
212
+ latch: "ready"
213
+ }), 0);
214
+ yield* Metric.update(Metric.withAttributes(healthStateGauge, {
215
+ daemon: name,
216
+ latch: "healthy"
217
+ }), 1);
218
+ yield* Metric.update(Metric.withAttributes(healthStateGauge, {
219
+ daemon: name,
220
+ latch: "paused"
221
+ }), 1);
222
+ return {
223
+ name,
224
+ ready,
225
+ healthy,
226
+ paused,
227
+ children
228
+ };
229
+ });
230
+ //#endregion
196
231
  //#region src/internal/AllocateWorkerHealth.ts
232
+ /** @internal */
197
233
  const allocateWorkerHealth = (name) => Effect.gen(function* () {
198
234
  const ready = yield* Latch.make(false);
199
235
  const healthy = yield* Latch.make(true);
@@ -292,87 +328,10 @@ const buildSubscriptionLoop = (worker, loop, health, readyGauge) => {
292
328
  });
293
329
  return wrapSpan(worker, body.pipe(Effect.asVoid));
294
330
  };
331
+ /** @internal */
295
332
  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);
296
333
  //#endregion
297
- //#region src/internal/WithLeaderLockExecutor.ts
298
- function withLeaderLock(self, options, lock) {
299
- const acquire = Effect.gen(function* () {
300
- const out = yield* lock.withLock(options.key, self);
301
- if (Option.isSome(out)) return out.value;
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);
303
- });
304
- const retryOnce = (current) => Effect.retry(acquire, {
305
- schedule: current,
306
- while: Predicate.isTagged("LeaderLockNotAcquired")
307
- }).pipe(Effect.catchTag("LeaderLockNotAcquired", () => retryOnce(current)));
308
- const backoff = options.acquireRetryBackoff;
309
- if (options.mode === "required" && backoff !== null && typeof backoff === "object") return retryOnce(backoff);
310
- return acquire;
311
- }
312
- //#endregion
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,
331
- mode: "required",
332
- acquireRetryBackoff: binding.spec.acquireRetryBackoff
333
- }, binding.lock);
334
- return withLeaderLock(self, {
335
- key: binding.spec.key,
336
- mode: "optional"
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);
345
- yield* Effect.forkScoped(locked.pipe(Effect.orDie));
346
- return health;
347
- });
348
- //#endregion
349
- //#region src/internal/AllocateSupervisorHealth.ts
350
- const allocateSupervisorHealth = (name, children) => Effect.gen(function* () {
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);
366
- return {
367
- name,
368
- ready,
369
- healthy,
370
- paused,
371
- children
372
- };
373
- });
374
- //#endregion
375
- //#region src/internal/BuildDynamicExecutor.ts
334
+ //#region src/RunDynamic.ts
376
335
  const buildDynamic = (spec, health) => Effect.gen(function* () {
377
336
  const state = yield* Ref.make({
378
337
  nextId: 0,
@@ -467,18 +426,24 @@ const dynamic$1 = (spec) => Effect.gen(function* () {
467
426
  });
468
427
  //#endregion
469
428
  //#region src/internal/IntensityWindow.ts
429
+ /** @internal */
470
430
  const isWithinWindow = (now, windowMillis) => (t) => now - t <= windowMillis;
471
431
  const keepWithin = (now, windowMillis) => (ts) => ts.filter(isWithinWindow(now, windowMillis));
432
+ /** @internal */
472
433
  const pruneTimestamps = (ts, now, windowMillis) => keepWithin(now, windowMillis)(ts);
434
+ /** @internal */
473
435
  const recordTimestamp = (ts, now, windowMillis) => [now, ...pruneTimestamps(ts, now, windowMillis)];
436
+ /** @internal */
474
437
  const exceedsRestarts = (count, restarts) => count > restarts;
475
438
  //#endregion
476
439
  //#region src/internal/Intensity.ts
440
+ /** @internal */
477
441
  const neverExceeds = {
478
442
  record: Effect.void,
479
443
  isExceeded: Effect.succeed(false),
480
444
  count: Effect.succeed(0)
481
445
  };
446
+ /** @internal */
482
447
  const make = (restarts, window) => Effect.gen(function* () {
483
448
  const windowMillis = Duration.toMillis(window);
484
449
  const timestamps = yield* Ref.make([]);
@@ -504,8 +469,61 @@ const make = (restarts, window) => Effect.gen(function* () {
504
469
  });
505
470
  //#endregion
506
471
  //#region src/internal/RaceForExit.ts
472
+ /** @internal */
507
473
  const raceForExit = (fibers) => Effect.raceAll(fibers.map((f, idx) => Fiber.await(f).pipe(Effect.map((exit) => [idx, exit]))));
508
474
  //#endregion
475
+ //#region src/internal/RestartDecision.schema.ts
476
+ /** @internal */
477
+ const RestartStrategy = Schema.Literals([
478
+ "one_for_one",
479
+ "one_for_all",
480
+ "rest_for_one"
481
+ ]);
482
+ /**
483
+ * The cross-field invariant the command carries: a failed child's index addresses one of
484
+ * the children that exist.
485
+ *
486
+ * It is a named function rather than an inline `Schema.filter` arrow because naming it makes
487
+ * it reachable by this file's property block, which an inline arrow is not. It is not
488
+ * exported: a `*.schema.ts` declares schemas and the vocabulary they are built from, never
489
+ * loose functions, and nothing outside this module needs it.
490
+ *
491
+ * It lives here rather than beside the decision because the decision now imports
492
+ * `DecideInput` as a *value* — `Workflow.make` constrains its command argument on the class
493
+ * itself — so the dependency between these two modules has to run one way only. With the
494
+ * predicate on the other side, both load orders reach a temporal dead zone: whichever module
495
+ * evaluates first suspends on the other, and the name it needs at module scope is not yet
496
+ * initialised.
497
+ */
498
+ const failedIndexAddressesAChild = (input) => input.failedIndex < input.totalChildren;
499
+ /**
500
+ * The command's field map and cross-field check, named so the class below extends a binding
501
+ * rather than an inline factory call. An anonymous base adds a new `ae-forgotten-export`
502
+ * `*_base` warning to the committed API report, which this package fixes at the source
503
+ * instead of suppressing.
504
+ */
505
+ const DecideInputBase = Schema.Struct({
506
+ strategy: RestartStrategy,
507
+ totalChildren: Schema.Int.pipe(Schema.check(Schema.isBetween({
508
+ minimum: 1,
509
+ maximum: MAX_CHILDREN_CEILING
510
+ }))),
511
+ failedIndex: Schema.Int.pipe(Schema.check(Schema.isBetween({
512
+ minimum: 0,
513
+ maximum: MAX_CHILDREN_CEILING
514
+ }))),
515
+ exitSuccess: Schema.Boolean,
516
+ intensityExceeded: Schema.Boolean
517
+ }).pipe(Schema.check(Schema.makeFilter(failedIndexAddressesAChild, { message: "failedIndex must be < totalChildren" })));
518
+ /**
519
+ * The restart command. A `Schema.Class` rather than a `Schema.Struct` because `Workflow.make`
520
+ * takes the command's class as its first argument, and a struct carries no `identifier` and
521
+ * no `extend` — the constraint refuses it. Every field schema and the cross-field check are
522
+ * the ones the struct carried.
523
+ */
524
+ /** @internal */
525
+ var DecideInput = class extends Schema.Class("DecideInput")(DecideInputBase) {};
526
+ //#endregion
509
527
  //#region src/internal/RestartDecision.workflow.ts
510
528
  /**
511
529
  * The child indices a restart covers, by supervision strategy.
@@ -513,59 +531,38 @@ const raceForExit = (fibers) => Effect.raceAll(fibers.map((f, idx) => Fiber.awai
513
531
  * A pure total function: the one part of the restart decision that is computation rather
514
532
  * than dispatch, so it lives in the decision cell beside the `Workflow.make` it serves.
515
533
  */
534
+ /** @internal */
516
535
  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;
526
536
  const RestartDecisionTypeId = Symbol.for("@systemfsoftware/effect-daemon/RestartDecision");
537
+ /** @internal */
527
538
  var RestartDecisionContinue = class extends S.TaggedClass()("Continue", {}) {
528
539
  [RestartDecisionTypeId] = RestartDecisionTypeId;
529
540
  };
541
+ /** @internal */
530
542
  var RestartDecisionRestart = class extends S.TaggedClass()("Restart", { indices: S.NonEmptyArray(S.Int) }) {
531
543
  [RestartDecisionTypeId] = RestartDecisionTypeId;
532
544
  };
545
+ /** @internal */
533
546
  var RestartDecisionExhausted = class extends S.TaggedError()("Exhausted", {}) {
534
547
  [RestartDecisionTypeId] = RestartDecisionTypeId;
535
548
  };
536
- const decideRestart = Workflow.make((command) => Match$1.value(command).pipe(Match$1.when({ exitSuccess: true }, () => Result$1.succeed(RestartDecisionContinue.make())), Match$1.when({
549
+ /** @internal */
550
+ const decideRestart = Workflow.make(DecideInput, (command) => Match$1.value(command).pipe(Match$1.when({ exitSuccess: true }, () => Result$1.succeed(RestartDecisionContinue.make())), Match$1.when({
537
551
  exitSuccess: false,
538
552
  intensityExceeded: true
539
553
  }, () => Result$1.fail(RestartDecisionExhausted.make())), Match$1.orElse(() => Result$1.succeed(RestartDecisionRestart.make({ indices: restartIndicesFor(command.strategy, command.failedIndex, command.totalChildren) })))));
540
554
  //#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" })));
560
- //#endregion
561
555
  //#region src/internal/SupervisionEpoch.schema.ts
562
556
  const EpochStepTypeId = Symbol.for("@systemfsoftware/effect-daemon/EpochStep");
557
+ /** @internal */
563
558
  var StopEpoch = class extends Schema.TaggedClass()("StopEpoch", {}) {
564
559
  [EpochStepTypeId] = EpochStepTypeId;
565
560
  };
561
+ /** @internal */
566
562
  var RestartEpoch = class extends Schema.TaggedClass()("RestartEpoch", {}) {
567
563
  [EpochStepTypeId] = EpochStepTypeId;
568
564
  };
565
+ /** @internal */
569
566
  var CooldownEpoch = class extends Schema.TaggedClass()("CooldownEpoch", {}) {
570
567
  [EpochStepTypeId] = EpochStepTypeId;
571
568
  };
@@ -575,14 +572,63 @@ Schema.Union([
575
572
  CooldownEpoch
576
573
  ]);
577
574
  const SupervisionEpochResultTypeId = Symbol.for("@systemfsoftware/effect-daemon/SupervisionEpochResult");
575
+ /** @internal */
578
576
  var StopSupervision = class extends Schema.TaggedClass()("StopSupervision", {}) {
579
577
  [SupervisionEpochResultTypeId] = SupervisionEpochResultTypeId;
580
578
  };
579
+ /** @internal */
581
580
  var ContinueSupervision = class extends Schema.TaggedClass()("ContinueSupervision", {}) {
582
581
  [SupervisionEpochResultTypeId] = SupervisionEpochResultTypeId;
583
582
  };
584
583
  Schema.Union([StopSupervision, ContinueSupervision]);
585
584
  //#endregion
585
+ //#region src/WithLeaderLock.ts
586
+ /**
587
+ * Runs `self` while holding the named leader lock.
588
+ */
589
+ function withLeaderLock(self, options, lock) {
590
+ const acquire = Effect.gen(function* () {
591
+ const out = yield* lock.withLock(options.key, self);
592
+ if (Option.isSome(out)) return out.value;
593
+ return yield* Match.value(options.mode).pipe(Match.when("required", () => Effect.fail(LeaderLockNotAcquired.make({ key: options.key }))), Match.when("optional", () => Effect.void), Match.exhaustive);
594
+ });
595
+ const retryOnce = (current) => Effect.retry(acquire, {
596
+ schedule: current,
597
+ while: Predicate.isTagged("LeaderLockNotAcquired")
598
+ }).pipe(Effect.catchTag("LeaderLockNotAcquired", () => retryOnce(current)));
599
+ const backoff = options.acquireRetryBackoff;
600
+ if (options.mode === "required" && backoff !== null && typeof backoff === "object") return retryOnce(backoff);
601
+ return acquire;
602
+ }
603
+ //#endregion
604
+ //#region src/internal/WithLockByModeExecutor.ts
605
+ /**
606
+ * Runs `self` under whichever lock discipline the binding asks for.
607
+ *
608
+ * An absent lock adapter and a `mode: 'none'` spec are one case, not two: a spec that
609
+ * declines the lock and a composition root that supplies no adapter both leave the
610
+ * body unwrapped, and nothing downstream can tell them apart from the effect that
611
+ * comes back. `unlocked` is that single case; `locked` dispatches on the spec's mode
612
+ * to the existing `withLeaderLock` calls. There is no null arm because the null state
613
+ * is now a constructor choice, not a runtime case.
614
+ *
615
+ * The worker and the supervisor make the same choice over the same cases, so it is
616
+ * made here once rather than in each of them.
617
+ */
618
+ /** @internal */
619
+ const withLockByMode = (self, binding) => {
620
+ if (binding.kind === "unlocked") return self;
621
+ if (binding.spec.mode === "required") return withLeaderLock(self, {
622
+ key: binding.spec.key,
623
+ mode: "required",
624
+ acquireRetryBackoff: binding.spec.acquireRetryBackoff
625
+ }, binding.lock);
626
+ return withLeaderLock(self, {
627
+ key: binding.spec.key,
628
+ mode: "optional"
629
+ }, binding.lock);
630
+ };
631
+ //#endregion
586
632
  //#region src/internal/SupervisorBodyExecutor.ts
587
633
  const handleExhausted = (ctx, cause) => Effect.gen(function* () {
588
634
  yield* Effect.andThen(ctx.health.healthy.close, Metric.update(Metric.withAttributes(healthStateGauge, {
@@ -796,6 +842,7 @@ const bootChild = (child, reporter) => Effect.gen(function* () {
796
842
  childPolicy: {}
797
843
  };
798
844
  });
845
+ /** @internal */
799
846
  const supervisor$1 = (s, reporter, binding) => Effect.gen(function* () {
800
847
  const booted = yield* Effect.forEach(s.children, (child) => bootChild(child, reporter));
801
848
  const health = yield* allocateSupervisorHealth(s.name, booted.map((b) => b.health));
@@ -805,34 +852,70 @@ const supervisor$1 = (s, reporter, binding) => Effect.gen(function* () {
805
852
  return health;
806
853
  });
807
854
  //#endregion
808
- //#region src/internal/SupervisionLeader.ts
809
- const LeaderConfig = Context.Reference("@systemfsoftware/effect-daemon-spec/LeaderConfig", { defaultValue: () => ({
810
- backoffBase: Duration.seconds(1),
811
- intensity: UnboundedIntensity.make(),
812
- cooldown: Duration.zero
813
- }) });
855
+ //#region src/LeaderLock.ts
856
+ const isModeNone = (lock) => lock.mode === "none";
814
857
  //#endregion
815
- //#region src/internal/SupervisionTask.ts
816
- const TaskConfig = Context.Reference("@systemfsoftware/effect-daemon-spec/TaskConfig", { defaultValue: () => ({
817
- backoffBase: Duration.seconds(1),
818
- intensity: UnboundedIntensity.make(),
819
- cooldown: Duration.zero
820
- }) });
858
+ //#region src/RunSupervisor.ts
859
+ /**
860
+ * The supervisor: acquires the `DaemonReporter` and — unless the lock mode is none —
861
+ * the `LeaderLock` capabilities at the composition root, then hands them down to the
862
+ * supervisor body via the lock binding. The body itself only ever sees the service
863
+ * values.
864
+ */
865
+ const supervisor = (s) => Effect.gen(function* () {
866
+ const reporter = yield* DaemonReporter;
867
+ let binding;
868
+ if (isModeNone(s.lock)) binding = { kind: "unlocked" };
869
+ else {
870
+ const lock = yield* LeaderLock;
871
+ binding = {
872
+ kind: "locked",
873
+ spec: s.lock,
874
+ lock
875
+ };
876
+ }
877
+ return yield* supervisor$1(s, reporter, binding);
878
+ });
821
879
  //#endregion
822
- //#region src/internal/SupervisionWorker.ts
823
- const WorkerConfig = Context.Reference("@systemfsoftware/effect-daemon-spec/WorkerConfig", { defaultValue: () => ({
824
- backoffBase: Duration.seconds(10),
825
- intensity: BoundedIntensity.make({
826
- restarts: 10,
827
- window: Duration.seconds(60)
828
- }),
829
- cooldown: Duration.seconds(30)
830
- }) });
880
+ //#region src/DaemonWorkerExecutor.ts
881
+ const worker$2 = (w, binding) => Effect.gen(function* () {
882
+ const health = yield* allocateWorkerHealth(w.name);
883
+ const loop = buildWorkerLoop(w, health, healthStateGauge).pipe(Effect.orDie);
884
+ const locked = withLockByMode(loop, binding);
885
+ yield* Effect.forkScoped(locked.pipe(Effect.orDie));
886
+ return health;
887
+ });
888
+ //#endregion
889
+ //#region src/RunWorker.ts
890
+ /**
891
+ * Boots a worker. The leader-lock capability is acquired here, at the composition
892
+ * root, and handed down as part of the lock binding: the executor behind this
893
+ * entry point never sees the tag. A worker whose lock is `{ mode: 'none' }`
894
+ * takes no lock at all.
895
+ */
896
+ const worker = (w) => Effect.gen(function* () {
897
+ let binding;
898
+ if (isModeNone(w.lock)) binding = { kind: "unlocked" };
899
+ else {
900
+ const lock = yield* LeaderLock;
901
+ binding = {
902
+ kind: "locked",
903
+ spec: w.lock,
904
+ lock
905
+ };
906
+ }
907
+ return yield* worker$2(w, binding);
908
+ });
831
909
  //#endregion
832
910
  //#region src/SupervisionCustom.ts
833
911
  const custom = (policy) => Effect.succeed(policy);
834
912
  //#endregion
835
913
  //#region src/SupervisionLeader.ts
914
+ const LeaderConfig = Context.Reference("@systemfsoftware/effect-daemon-spec/LeaderConfig", { defaultValue: () => ({
915
+ backoffBase: Duration.seconds(1),
916
+ intensity: UnboundedIntensity.make(),
917
+ cooldown: Duration.zero
918
+ }) });
836
919
  const leader$1 = (config, cap) => Effect.succeed({
837
920
  intensity: config.intensity,
838
921
  backoff: cappedBackoff(config.backoffBase, cap),
@@ -840,6 +923,11 @@ const leader$1 = (config, cap) => Effect.succeed({
840
923
  });
841
924
  //#endregion
842
925
  //#region src/SupervisionTask.ts
926
+ const TaskConfig = Context.Reference("@systemfsoftware/effect-daemon-spec/TaskConfig", { defaultValue: () => ({
927
+ backoffBase: Duration.seconds(1),
928
+ intensity: UnboundedIntensity.make(),
929
+ cooldown: Duration.zero
930
+ }) });
843
931
  const task$1 = (config, budget) => Effect.succeed({
844
932
  intensity: config.intensity,
845
933
  backoff: Schedule.exponential(config.backoffBase).pipe(Schedule.jittered, Schedule.upTo({ duration: budget })),
@@ -847,6 +935,14 @@ const task$1 = (config, budget) => Effect.succeed({
847
935
  });
848
936
  //#endregion
849
937
  //#region src/SupervisionWorker.ts
938
+ const WorkerConfig = Context.Reference("@systemfsoftware/effect-daemon-spec/WorkerConfig", { defaultValue: () => ({
939
+ backoffBase: Duration.seconds(10),
940
+ intensity: BoundedIntensity.make({
941
+ restarts: 10,
942
+ window: Duration.seconds(60)
943
+ }),
944
+ cooldown: Duration.seconds(30)
945
+ }) });
850
946
  const worker$1 = (config, cap) => Effect.succeed({
851
947
  intensity: config.intensity,
852
948
  backoff: cappedBackoff(config.backoffBase, cap),
@@ -895,45 +991,6 @@ const Daemon = {
895
991
  stream,
896
992
  subscription
897
993
  };
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
- });
937
994
  const run = {
938
995
  worker,
939
996
  supervisor,
@@ -956,4 +1013,4 @@ const oneForAll = (opts) => oneForAll$1(opts);
956
1013
  const oneForOne = (opts) => oneForOne$1(opts);
957
1014
  const restForOne = (opts) => restForOne$1(opts);
958
1015
  //#endregion
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 };
1016
+ export { BoundedIntensity, ChildPolicyConfig, Daemon, DaemonReporter, DynamicLimitExceeded, DynamicSpecTypeId, Intensity, IntensityConfig, LeaderConfig, LeaderLock, LeaderLockFromPrimitive, LeaderLockInfraError, LeaderLockNotAcquired, LockPolicyConfig, LockPrimitive, LockPrimitiveError, MaxChildren, Noop, PollLoopTag, StreamLoopTag, SubscriptionLoopTag, 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 };
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@systemfsoftware/effect-daemon-spec",
3
3
  "license": "Apache-2.0",
4
- "version": "1.0.0",
4
+ "version": "3.0.0",
5
5
  "author": "Ryan Lee <drdgvhbh@gmail.com>",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/systemfsoftware/systemfsoftware.git",
9
- "directory": "packages/effect-daemon-spec"
9
+ "directory": "packages/core/effect/daemon-spec"
10
10
  },
11
- "homepage": "https://github.com/systemfsoftware/systemfsoftware/tree/main/packages/effect-daemon-spec#readme",
11
+ "homepage": "https://github.com/systemfsoftware/systemfsoftware/tree/main/packages/core/effect/daemon-spec#readme",
12
12
  "bugs": "https://github.com/systemfsoftware/systemfsoftware/issues",
13
13
  "description": "Supervision-tree daemons for Effect-TS — leader election, distributed locks, restart-intensity backoff, dynamic children, and health latches as a pure typed core.",
14
14
  "keywords": [
@@ -37,31 +37,32 @@
37
37
  "dist"
38
38
  ],
39
39
  "dependencies": {
40
- "@systemfsoftware/effect-cell-types": "^2.0.0"
40
+ "@systemfsoftware/effect-cell-types": "^4.0.0"
41
41
  },
42
42
  "devDependencies": {
43
- "@effect/vitest": "4.0.0-rc.108",
43
+ "@effect/vitest": "4.0.0-rc.111",
44
44
  "@microsoft/api-extractor": "^7.58.7",
45
45
  "@systemfsoftware/arethetypeswrong-cli": "^1.1.1",
46
46
  "@types/node": "^24",
47
47
  "@vitest/coverage-v8": "^4",
48
- "effect": "4.0.0-rc.108",
48
+ "effect": "4.0.0-rc.111",
49
49
  "fast-check": "^4",
50
50
  "oxlint": "^1.77.0",
51
51
  "rimraf": "^6.1.3",
52
52
  "tsdown": "^0.22.14",
53
+ "typescript": "^7",
53
54
  "vitest": "^4.1.10",
54
- "@systemfsoftware/effect-gherkin-spec": "^1.0.0",
55
- "@systemfsoftware/effect-schema-law": "^0.7.0",
56
- "@systemfsoftware/effect-schema-vite": "^1.5.2",
55
+ "@systemfsoftware/effect-gherkin-spec": "^2.0.1",
56
+ "@systemfsoftware/effect-schema-vite": "^1.5.4",
57
57
  "@systemfsoftware/oxlint-config": "^0.1.0",
58
- "@systemfsoftware/oxlint-plugin-cell-vocabulary": "^1.1.0",
59
- "@systemfsoftware/tsconfig": "^1.3.2",
60
- "@systemfsoftware/stryker-plugins": "^0.12.0",
61
- "@systemfsoftware/vitest-config": "^0.1.0"
58
+ "@systemfsoftware/effect-schema-law": "^0.9.0",
59
+ "@systemfsoftware/stryker-plugins": "^0.13.1",
60
+ "@systemfsoftware/tsconfig": "^1.3.3",
61
+ "@systemfsoftware/vitest-config": "^0.1.0",
62
+ "@systemfsoftware/oxlint-plugin-cell-vocabulary": "^1.2.1"
62
63
  },
63
64
  "peerDependencies": {
64
- "effect": "4.0.0-rc.108"
65
+ "effect": "^4.0.0-rc.111"
65
66
  },
66
67
  "publishConfig": {
67
68
  "provenance": true