@systemfsoftware/effect-daemon-spec 0.1.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 ADDED
@@ -0,0 +1,737 @@
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 { dual } from "effect/Function";
3
+ import "effect/Schema";
4
+ //#region src/backoff.ts
5
+ const cappedBackoff = (base, cap) => {
6
+ const ceiling = Duration.decode(cap);
7
+ return Schedule.exponential(base).pipe(Schedule.jittered, Schedule.modifyDelay((_, delay) => Duration.min(delay, ceiling)));
8
+ };
9
+ //#endregion
10
+ //#region src/daemon-health.schema.ts
11
+ var DynamicLimitExceeded = class extends Schema.TaggedError()("DynamicLimitExceeded", { limit: Schema.Int.pipe(Schema.greaterThanOrEqualTo(0)) }) {};
12
+ //#endregion
13
+ //#region src/daemon-metrics.ts
14
+ const supervisorRestartsCounter = Metric.counter("app.daemon.supervisor.restart", { description: "Daemon supervisor restart count" });
15
+ const supervisorExhaustionsCounter = Metric.counter("app.daemon.supervisor.exhaustion", { description: "Daemon supervisor exhaustion count" });
16
+ const supervisorChildrenGauge = Metric.gauge("app.daemon.supervisor.children", { description: "Daemon supervisor current child count (dynamic supervisors only)" });
17
+ 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)" });
18
+ //#endregion
19
+ //#region src/daemon-policy.schema.ts
20
+ const IntensityConfig = Schema.Struct({
21
+ restarts: Schema.Int.pipe(Schema.greaterThanOrEqualTo(0)),
22
+ window: Schema.DurationFromSelf
23
+ });
24
+ const IntensityTypeId = Symbol.for("@systemfsoftware/effect-daemon-spec/Intensity");
25
+ var BoundedIntensity = class extends Schema.TaggedClass()("Bounded", IntensityConfig.fields) {
26
+ [IntensityTypeId] = IntensityTypeId;
27
+ };
28
+ var UnboundedIntensity = class extends Schema.TaggedClass()("Unbounded", {}) {
29
+ [IntensityTypeId] = IntensityTypeId;
30
+ };
31
+ const Intensity = Schema.Union(BoundedIntensity, UnboundedIntensity);
32
+ var ChildPolicyConfig = class extends Schema.Class("ChildPolicyConfig")({
33
+ restart: Schema.optional(Schema.Literal("permanent", "transient", "temporary")),
34
+ intensity: Schema.optional(IntensityConfig)
35
+ }) {};
36
+ Schema.Class("SupervisorPolicyConfig")({
37
+ intensity: Schema.optional(IntensityConfig),
38
+ cooldown: Schema.optional(Schema.DurationFromSelf)
39
+ });
40
+ var LockPolicyConfig = class extends Schema.Class("LockPolicyConfig")({
41
+ mode: Schema.optional(Schema.Literal("none", "required", "optional")),
42
+ key: Schema.optional(Schema.String)
43
+ }) {};
44
+ var TickPolicyConfig = class extends Schema.Class("TickPolicyConfig")({
45
+ spanName: Schema.optional(Schema.String),
46
+ tickTimeout: Schema.DurationFromSelf,
47
+ startLogLevel: Schema.optional(Schema.Literal("debug", "info"))
48
+ }) {};
49
+ //#endregion
50
+ //#region src/daemon-reporter.ts
51
+ var DaemonReporter = class DaemonReporter extends Context.Tag("@systemfsoftware/effect-daemon-spec/daemon-reporter/DaemonReporter")() {
52
+ static Noop = Layer.succeed(DaemonReporter, DaemonReporter.of({
53
+ onRestart: () => Effect.void,
54
+ onExhausted: () => Effect.void
55
+ }));
56
+ };
57
+ //#endregion
58
+ //#region src/daemon-spec.ts
59
+ const WorkerTypeId = Symbol.for("@systemfsoftware/effect-daemon/Worker");
60
+ const SupervisorTypeId = Symbol.for("@systemfsoftware/effect-daemon/Supervisor");
61
+ const DynamicSpecTypeId = Symbol.for("@systemfsoftware/effect-daemon/DynamicSpec");
62
+ const isWorker = (x) => WorkerTypeId in x;
63
+ const isSupervisor = (x) => SupervisorTypeId in x;
64
+ //#endregion
65
+ //#region src/daemon.ts
66
+ const make$1 = (common, loop) => ({
67
+ [WorkerTypeId]: WorkerTypeId,
68
+ name: common.name,
69
+ loop,
70
+ child: common.child ?? {},
71
+ tick: common.tick,
72
+ tickHooks: common.tickHooks ?? {},
73
+ lock: common.lock
74
+ });
75
+ const poll = (opts) => {
76
+ if (typeof opts.prereq === "undefined") return make$1(opts, {
77
+ _tag: "Poll",
78
+ gate: Effect.succeed(Option.some(Effect.asVoid(opts.work))),
79
+ interval: opts.interval
80
+ });
81
+ const { prereq, work } = opts;
82
+ return make$1(opts, {
83
+ _tag: "Poll",
84
+ gate: Effect.map(prereq, Option.map((data) => work(data))),
85
+ interval: opts.interval
86
+ });
87
+ };
88
+ const stream = (opts) => make$1(opts, {
89
+ _tag: "Stream",
90
+ stream: opts.stream
91
+ });
92
+ const subscription = (opts) => make$1(opts, {
93
+ _tag: "Subscription",
94
+ acquire: Effect.asVoid(opts.acquire)
95
+ });
96
+ const Daemon = {
97
+ poll,
98
+ stream,
99
+ subscription
100
+ };
101
+ //#endregion
102
+ //#region src/leader-lock.schema.ts
103
+ var LeaderLockNotAcquired = class extends Schema.TaggedError()("LeaderLockNotAcquired", { key: Schema.String }) {};
104
+ var LeaderLockInfraError = class extends Schema.TaggedError()("LeaderLockInfraError", {
105
+ key: Schema.String,
106
+ cause: Schema.Unknown
107
+ }) {};
108
+ //#endregion
109
+ //#region src/leader-lock.ts
110
+ var LeaderLock = class LeaderLock extends Context.Tag("@systemfsoftware/effect-daemon-spec/leader-lock/LeaderLock")() {
111
+ static Noop = Layer.succeed(LeaderLock, LeaderLock.of({ withLock: (_key, self) => Effect.map(self, Option.some) }));
112
+ };
113
+ const withLeaderLock = dual(2, (self, options) => Effect.gen(function* () {
114
+ const out = yield* (yield* LeaderLock).withLock(options.key, self);
115
+ if (Option.isSome(out)) return out.value;
116
+ return yield* Match.value(options.mode).pipe(Match.when("required", () => Effect.fail(new LeaderLockNotAcquired({ key: options.key }))), Match.when("optional", () => Effect.void), Match.exhaustive);
117
+ }));
118
+ //#endregion
119
+ //#region src/lock-primitive.schema.ts
120
+ var LockPrimitiveError = class extends Schema.TaggedError()("LockPrimitiveError", {
121
+ key: Schema.String,
122
+ cause: Schema.Unknown
123
+ }) {};
124
+ //#endregion
125
+ //#region src/lock-primitive.ts
126
+ var LockPrimitive = class extends Context.Tag("@systemfsoftware/effect-daemon-spec/lock-primitive/LockPrimitive")() {};
127
+ const LeaderLockFromPrimitive = Layer.effect(LeaderLock, Effect.gen(function* () {
128
+ const primitive = yield* LockPrimitive;
129
+ return LeaderLock.of({ withLock: (key, self) => Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
130
+ const scope = yield* Scope.make();
131
+ if (!(yield* restore(primitive.tryAcquire(key)).pipe(Scope.extend(scope), Effect.mapError((cause) => new LeaderLockInfraError({
132
+ key,
133
+ cause
134
+ })), Effect.onError(() => Scope.close(scope, Exit.void))))) {
135
+ yield* Scope.close(scope, Exit.void);
136
+ return Option.none();
137
+ }
138
+ const result = yield* restore(self).pipe(Effect.ensuring(Scope.close(scope, Exit.void)));
139
+ return Option.some(result);
140
+ })) });
141
+ }));
142
+ //#endregion
143
+ //#region src/internal/intensity-window.ts
144
+ const isWithinWindow = (now, windowMillis) => (t) => now - t <= windowMillis;
145
+ const pruneTimestamps = (ts, now, windowMillis) => ts.filter(isWithinWindow(now, windowMillis));
146
+ const recordTimestamp = (ts, now, windowMillis) => [now, ...pruneTimestamps(ts, now, windowMillis)];
147
+ const exceedsRestarts = (count, restarts) => count > restarts;
148
+ //#endregion
149
+ //#region src/internal/intensity.ts
150
+ const neverExceeds = {
151
+ record: Effect.void,
152
+ isExceeded: Effect.succeed(false),
153
+ count: Effect.succeed(0)
154
+ };
155
+ const boundedTracker = (restarts, window) => Effect.gen(function* () {
156
+ const windowMillis = Duration.toMillis(window);
157
+ const timestamps = yield* Ref.make([]);
158
+ const prune = (now) => Ref.modify(timestamps, (ts) => {
159
+ const active = pruneTimestamps(ts, now, windowMillis);
160
+ return [active, active];
161
+ });
162
+ return {
163
+ record: Effect.gen(function* () {
164
+ const now = yield* Clock.currentTimeMillis;
165
+ yield* Ref.update(timestamps, (ts) => recordTimestamp(ts, now, windowMillis));
166
+ }),
167
+ isExceeded: Effect.gen(function* () {
168
+ return exceedsRestarts((yield* prune(yield* Clock.currentTimeMillis)).length, restarts);
169
+ }),
170
+ count: Effect.gen(function* () {
171
+ return (yield* prune(yield* Clock.currentTimeMillis)).length;
172
+ })
173
+ };
174
+ });
175
+ const make = (intensity) => Match.value(intensity).pipe(Match.tag("Unbounded", () => Effect.succeed(neverExceeds)), Match.tag("Bounded", ({ restarts, window }) => boundedTracker(restarts, window)), Match.exhaustive);
176
+ //#endregion
177
+ //#region src/internal/restart-decision.schema.ts
178
+ const RestartDecisionTypeId = Symbol.for("@systemfsoftware/effect-daemon/RestartDecision");
179
+ var Continue = class extends Schema.TaggedClass()("Continue", {}) {
180
+ [RestartDecisionTypeId] = RestartDecisionTypeId;
181
+ };
182
+ var Restart = class extends Schema.TaggedClass()("Restart", { indices: Schema.NonEmptyArray(Schema.Int) }) {
183
+ [RestartDecisionTypeId] = RestartDecisionTypeId;
184
+ };
185
+ var Exhausted = class extends Schema.TaggedClass()("Exhausted", {}) {
186
+ [RestartDecisionTypeId] = RestartDecisionTypeId;
187
+ };
188
+ Schema.Union(Continue, Restart, Exhausted);
189
+ const RestartStrategy = Schema.Literal("one_for_one", "one_for_all", "rest_for_one");
190
+ Schema.Struct({
191
+ strategy: RestartStrategy,
192
+ totalChildren: Schema.Int.pipe(Schema.between(1, 10)),
193
+ failedIndex: Schema.Int.pipe(Schema.greaterThanOrEqualTo(0)),
194
+ exitSuccess: Schema.Boolean,
195
+ intensityExceeded: Schema.Boolean
196
+ }).pipe(Schema.filter((s) => s.failedIndex < s.totalChildren, { message: () => "failedIndex must be < totalChildren" }), Schema.annotations({ arbitrary: () => (fc) => fc.integer({
197
+ min: 1,
198
+ max: 10
199
+ }).chain((totalChildren) => fc.integer({
200
+ min: 0,
201
+ max: totalChildren - 1
202
+ }).chain((failedIndex) => fc.record({
203
+ strategy: fc.constantFrom("one_for_one", "one_for_all", "rest_for_one"),
204
+ totalChildren: fc.constant(totalChildren),
205
+ failedIndex: fc.constant(failedIndex),
206
+ exitSuccess: fc.boolean(),
207
+ intensityExceeded: fc.boolean()
208
+ }))) }));
209
+ //#endregion
210
+ //#region src/internal/restart-decision.strategy.ts
211
+ const restartIndicesFor = (strategy, failedIndex, total) => {
212
+ if (strategy === "one_for_one") return [failedIndex];
213
+ if (strategy === "one_for_all") return [0, ...Array.from({ length: Math.max(0, total - 1) }, (_, i) => i + 1)];
214
+ return [failedIndex, ...Array.from({ length: Math.max(0, total - failedIndex - 1) }, (_, i) => failedIndex + 1 + i)];
215
+ };
216
+ const decideRestart = (input) => {
217
+ if (input.exitSuccess) return new Continue();
218
+ if (input.intensityExceeded) return new Exhausted();
219
+ return new Restart({ indices: restartIndicesFor(input.strategy, input.failedIndex, input.totalChildren) });
220
+ };
221
+ //#endregion
222
+ //#region src/internal/supervise-index.ts
223
+ const failedIndexOf = (startIdx, failedOffset) => startIdx + failedOffset;
224
+ //#endregion
225
+ //#region src/internal/supervision-context.strategy.ts
226
+ const openAllReady = (ctx) => Effect.gen(function* () {
227
+ yield* Effect.yieldNow();
228
+ yield* Effect.forEach(ctx.booted, (b) => b.health.ready.await, { concurrency: "unbounded" });
229
+ yield* Effect.zipRight(ctx.health.ready.open, Metric.set(Metric.tagged(Metric.tagged(healthStateGauge, "daemon", ctx.name), "latch", "ready"), 1));
230
+ });
231
+ const raceForExit = (fibers) => Effect.raceAll(fibers.map((f, idx) => f.await.pipe(Effect.map((exit) => [idx, exit]))));
232
+ //#endregion
233
+ //#region src/internal/supervise-tree.strategy.ts
234
+ const EpochStepTypeId = Symbol.for("@systemfsoftware/effect-daemon/EpochStep");
235
+ var StopEpoch = class extends Schema.TaggedClass()("StopEpoch", {}) {
236
+ [EpochStepTypeId] = EpochStepTypeId;
237
+ };
238
+ var RestartEpoch = class extends Schema.TaggedClass()("RestartEpoch", {}) {
239
+ [EpochStepTypeId] = EpochStepTypeId;
240
+ };
241
+ var CooldownEpoch = class extends Schema.TaggedClass()("CooldownEpoch", {}) {
242
+ [EpochStepTypeId] = EpochStepTypeId;
243
+ };
244
+ Schema.Union(StopEpoch, RestartEpoch, CooldownEpoch);
245
+ const SupervisionEpochResultTypeId = Symbol.for("@systemfsoftware/effect-daemon/SupervisionEpochResult");
246
+ var StopSupervision = class extends Schema.TaggedClass()("StopSupervision", {}) {
247
+ [SupervisionEpochResultTypeId] = SupervisionEpochResultTypeId;
248
+ };
249
+ var ContinueSupervision = class extends Schema.TaggedClass()("ContinueSupervision", {}) {
250
+ [SupervisionEpochResultTypeId] = SupervisionEpochResultTypeId;
251
+ };
252
+ Schema.Union(StopSupervision, ContinueSupervision);
253
+ const handleExhausted = (ctx, cause) => Effect.gen(function* () {
254
+ yield* Effect.zipRight(ctx.health.healthy.close, Metric.set(Metric.tagged(Metric.tagged(healthStateGauge, "daemon", ctx.name), "latch", "healthy"), 0));
255
+ yield* ctx.reportExhausted(cause);
256
+ return new CooldownEpoch();
257
+ });
258
+ const handleRestart = (ctx, cause, onSignal) => Effect.gen(function* () {
259
+ yield* ctx.reportRestart(cause);
260
+ yield* onSignal;
261
+ return new RestartEpoch();
262
+ });
263
+ const reopenHealthyAfterCooldown = (ctx) => Effect.zipRight(ctx.health.healthy.open, Metric.set(Metric.tagged(Metric.tagged(healthStateGauge, "daemon", ctx.name), "latch", "healthy"), 1));
264
+ const runSupervisionEpochWithBackoff = (attempt, ctx) => Effect.gen(function* () {
265
+ const driver = yield* Schedule.driver(ctx.policy.backoff);
266
+ const loop = () => Effect.gen(function* () {
267
+ const step = yield* attempt.pipe(Effect.scoped);
268
+ return yield* Match.value(step).pipe(Match.tag("StopEpoch", () => Effect.succeed(new StopSupervision())), Match.tag("CooldownEpoch", () => Effect.gen(function* () {
269
+ yield* Effect.sleep(Duration.decode(ctx.policy.cooldown));
270
+ yield* reopenHealthyAfterCooldown(ctx);
271
+ return new ContinueSupervision();
272
+ })), Match.tag("RestartEpoch", () => Effect.gen(function* () {
273
+ const stepped = yield* Effect.either(driver.next(void 0));
274
+ if (Either.isLeft(stepped)) return new StopSupervision();
275
+ return yield* loop();
276
+ })), Match.exhaustive);
277
+ });
278
+ return yield* loop();
279
+ });
280
+ const superviseChild = (ctx, child, idx) => Effect.gen(function* () {
281
+ const childIntensityOpt = yield* Option.match(Option.fromNullable(child.childPolicy.intensity), {
282
+ onNone: () => Effect.succeed(Option.none()),
283
+ onSome: (cfg) => Effect.map(make(new BoundedIntensity(cfg)), Option.some)
284
+ });
285
+ const loop = () => Effect.gen(function* () {
286
+ const supIntensity = yield* make(ctx.policy.intensity);
287
+ const epochResult = yield* runSupervisionEpochWithBackoff(Effect.gen(function* () {
288
+ yield* ctx.health.paused.await;
289
+ const fiber = yield* Effect.forkScoped(child.run);
290
+ const exit = yield* Fiber.await(fiber);
291
+ if (!Exit.isSuccess(exit)) {
292
+ if (child.childPolicy.restart === "temporary") return new StopEpoch();
293
+ if (yield* Option.match(childIntensityOpt, {
294
+ onNone: () => Effect.succeed(false),
295
+ onSome: (ci) => Effect.gen(function* () {
296
+ yield* ci.record;
297
+ return yield* ci.isExceeded;
298
+ })
299
+ })) return new StopEpoch();
300
+ yield* supIntensity.record;
301
+ const decision = decideRestart({
302
+ strategy: "one_for_one",
303
+ exitSuccess: false,
304
+ intensityExceeded: yield* supIntensity.isExceeded,
305
+ failedIndex: idx,
306
+ totalChildren: ctx.booted.length
307
+ });
308
+ return yield* Match.value(decision).pipe(Match.tag("Continue", () => Effect.succeed(new StopEpoch())), Match.tag("Exhausted", () => handleExhausted(ctx, exit.cause)), Match.tag("Restart", () => handleRestart(ctx, exit.cause, Effect.void)), Match.exhaustive);
309
+ }
310
+ return new StopEpoch();
311
+ }), ctx);
312
+ return yield* Match.value(epochResult).pipe(Match.tag("ContinueSupervision", () => loop()), Match.tag("StopSupervision", () => Effect.void), Match.exhaustive);
313
+ });
314
+ return yield* loop();
315
+ });
316
+ const runIndependent = (ctx) => Effect.gen(function* () {
317
+ const fibers = yield* Effect.forEach(ctx.booted, (child, childIdx) => Effect.forkScoped(superviseChild(ctx, child, childIdx)));
318
+ yield* Effect.yieldNow();
319
+ yield* openAllReady(ctx);
320
+ yield* Effect.forEach(fibers, (f) => Fiber.await(f), { concurrency: "unbounded" });
321
+ });
322
+ const runGroup = (strategy, ctx) => Effect.gen(function* () {
323
+ const loop = () => Effect.gen(function* () {
324
+ const intensity = yield* make(ctx.policy.intensity);
325
+ const childIntensityTrackers = yield* Effect.forEach(ctx.booted, (b) => Option.match(Option.fromNullable(b.childPolicy.intensity), {
326
+ onNone: () => Effect.succeed(Option.none()),
327
+ onSome: (cfg) => Effect.map(make(new BoundedIntensity(cfg)), Option.some)
328
+ }));
329
+ const cursor = yield* Ref.make(0);
330
+ const epochResult = yield* runSupervisionEpochWithBackoff(Effect.gen(function* () {
331
+ yield* ctx.health.paused.await;
332
+ const startIdx = yield* Ref.get(cursor);
333
+ const slice = ctx.booted.slice(startIdx);
334
+ const fibers = yield* Effect.forEach(slice, (c) => Effect.forkScoped(c.run));
335
+ yield* Effect.yieldNow();
336
+ yield* Effect.forkScoped(openAllReady(ctx));
337
+ const [failedOffset, firstExit] = yield* raceForExit(fibers);
338
+ if (!Exit.isSuccess(firstExit)) {
339
+ const failedIdx = failedIndexOf(startIdx, failedOffset);
340
+ const failedBootedOpt = Option.fromNullable(ctx.booted[failedIdx]);
341
+ if (Option.isNone(failedBootedOpt)) return new StopEpoch();
342
+ if (failedBootedOpt.value.childPolicy.restart === "temporary") return new StopEpoch();
343
+ const cIntForFailed = Option.flatten(Array$1.get(childIntensityTrackers, failedIdx));
344
+ if (yield* Option.match(cIntForFailed, {
345
+ onNone: () => Effect.succeed(false),
346
+ onSome: (cInt) => Effect.gen(function* () {
347
+ yield* cInt.record;
348
+ return yield* cInt.isExceeded;
349
+ })
350
+ })) return new StopEpoch();
351
+ yield* intensity.record;
352
+ const decision = decideRestart({
353
+ strategy,
354
+ exitSuccess: false,
355
+ intensityExceeded: yield* intensity.isExceeded,
356
+ failedIndex: failedIdx,
357
+ totalChildren: ctx.booted.length
358
+ });
359
+ return yield* Match.value(decision).pipe(Match.tag("Continue", () => Effect.succeed(new StopEpoch())), Match.tag("Exhausted", () => handleExhausted(ctx, firstExit.cause)), Match.tag("Restart", (restartDecision) => handleRestart(ctx, firstExit.cause, Ref.set(cursor, restartDecision.indices[0]))), Match.exhaustive);
360
+ }
361
+ return new StopEpoch();
362
+ }), ctx);
363
+ return yield* Match.value(epochResult).pipe(Match.tag("ContinueSupervision", () => loop()), Match.tag("StopSupervision", () => Effect.void), Match.exhaustive);
364
+ });
365
+ return yield* loop();
366
+ });
367
+ 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);
368
+ //#endregion
369
+ //#region src/internal/supervisor-runtime.ts
370
+ const buildSupervisorBody = (sup, health, booted) => Effect.gen(function* () {
371
+ const policy = yield* sup.supervision;
372
+ const intensityEff = make(policy.intensity);
373
+ const reportRestart = (cause) => Effect.gen(function* () {
374
+ const reporter = yield* DaemonReporter;
375
+ yield* Metric.increment(Metric.tagged(supervisorRestartsCounter, "supervisor", sup.name));
376
+ yield* reporter.onRestart(sup.name, cause);
377
+ yield* Option.match(Option.fromNullable(sup.reporter.onRestart), {
378
+ onNone: () => Effect.void,
379
+ onSome: (fn) => fn(cause)
380
+ });
381
+ });
382
+ const reportExhausted = (cause) => Effect.gen(function* () {
383
+ const reporter = yield* DaemonReporter;
384
+ yield* Metric.increment(Metric.tagged(supervisorExhaustionsCounter, "supervisor", sup.name));
385
+ yield* reporter.onExhausted(sup.name, cause);
386
+ yield* Option.match(Option.fromNullable(sup.reporter.onExhausted), {
387
+ onNone: () => Effect.void,
388
+ onSome: (fn) => fn(cause)
389
+ });
390
+ });
391
+ const runStrategy = superviseTree(sup.strategy, {
392
+ name: sup.name,
393
+ booted,
394
+ health,
395
+ policy,
396
+ reportRestart,
397
+ reportExhausted,
398
+ intensityEff
399
+ });
400
+ yield* Effect.andThen(health.paused.await, runStrategy);
401
+ });
402
+ //#endregion
403
+ //#region src/internal/worker-loop.ts
404
+ const applySpanAttributes = (hooks) => {
405
+ const { spanAttributes } = hooks;
406
+ return (effect) => {
407
+ if (typeof spanAttributes === "undefined") return effect;
408
+ return Effect.tap(effect, () => Effect.orElse(spanAttributes, () => Effect.succeed({})).pipe(Effect.flatMap(Effect.annotateCurrentSpan)));
409
+ };
410
+ };
411
+ const applyTrackDuration = (hooks) => {
412
+ const { trackDuration } = hooks;
413
+ return (effect) => {
414
+ if (typeof trackDuration === "undefined") return effect;
415
+ return Metric.trackDuration(effect, trackDuration);
416
+ };
417
+ };
418
+ const applyTimeout = (config) => {
419
+ return (effect) => Effect.timeoutFail(effect, {
420
+ duration: config.tickTimeout,
421
+ onTimeout: () => new Cause.TimeoutException()
422
+ });
423
+ };
424
+ const applyInnerRetry = (hooks) => {
425
+ const { innerRetry } = hooks;
426
+ return (effect) => {
427
+ if (typeof innerRetry === "undefined") return effect;
428
+ return Effect.retry(effect, innerRetry);
429
+ };
430
+ };
431
+ const buildPollTick = (worker, health, gate) => {
432
+ const { tick, tickHooks } = worker;
433
+ const spanName = tick.spanName ?? "daemon.tick";
434
+ const withSpanAttrs = applySpanAttributes(tickHooks);
435
+ const withDuration = applyTrackDuration(tickHooks);
436
+ const withTimeout = applyTimeout(tick);
437
+ const withInnerRetry = applyInnerRetry(tickHooks);
438
+ const runWork = (work) => work.pipe(withSpanAttrs, withDuration).pipe(Effect.withSpan(spanName, {
439
+ root: true,
440
+ attributes: { "daemon.name": worker.name }
441
+ }), Effect.withLogSpan(spanName));
442
+ return withInnerRetry(withTimeout(Effect.andThen(health.paused.await, gate).pipe(Effect.flatMap(Option.match({
443
+ onNone: () => Effect.void,
444
+ onSome: runWork
445
+ }))))).pipe(Effect.tap(() => Effect.zipRight(health.ready.open, Metric.set(Metric.tagged(Metric.tagged(healthStateGauge, "daemon", health.name), "latch", "ready"), 1))), Effect.asVoid);
446
+ };
447
+ const wrapSpan = (worker, effect) => {
448
+ const spanName = worker.tick.spanName ?? "daemon.worker";
449
+ return effect.pipe(Effect.withSpan(spanName, {
450
+ root: true,
451
+ attributes: { "daemon.name": worker.name }
452
+ }), Effect.withLogSpan(spanName));
453
+ };
454
+ const buildPollLoop = (worker, loop, health) => {
455
+ const tick = buildPollTick(worker, health, loop.gate);
456
+ return Effect.repeat(tick, Schedule.spaced(loop.interval)).pipe(Effect.asVoid);
457
+ };
458
+ const buildStreamLoop = (worker, loop, health) => {
459
+ const body = Effect.gen(function* () {
460
+ yield* health.paused.await;
461
+ const fiber = yield* Effect.forkScoped(loop.stream.pipe(Stream.tap(() => Effect.zipRight(health.ready.open, Metric.set(Metric.tagged(Metric.tagged(healthStateGauge, "daemon", health.name), "latch", "ready"), 1))), Stream.runDrain));
462
+ const ready = applyTimeout(worker.tick)(health.ready.await);
463
+ yield* Effect.raceFirst(ready, Fiber.join(fiber));
464
+ yield* Fiber.join(fiber);
465
+ });
466
+ return wrapSpan(worker, applyInnerRetry(worker.tickHooks)(body).pipe(Effect.asVoid));
467
+ };
468
+ const buildSubscriptionLoop = (worker, loop, health) => {
469
+ return wrapSpan(worker, Effect.gen(function* () {
470
+ yield* health.paused.await;
471
+ yield* applyTimeout(worker.tick)(loop.acquire);
472
+ yield* Effect.zipRight(health.ready.open, Metric.set(Metric.tagged(Metric.tagged(healthStateGauge, "daemon", health.name), "latch", "ready"), 1));
473
+ return yield* Effect.never;
474
+ }).pipe(Effect.asVoid));
475
+ };
476
+ const buildWorkerLoop = (worker, health) => Match.value(worker.loop).pipe(Match.tag("Poll", (loop) => buildPollLoop(worker, loop, health)), Match.tag("Stream", (loop) => buildStreamLoop(worker, loop, health)), Match.tag("Subscription", (loop) => buildSubscriptionLoop(worker, loop, health)), Match.exhaustive);
477
+ //#endregion
478
+ //#region src/internal/boot.ts
479
+ const allocateWorkerHealth = (name) => Effect.gen(function* () {
480
+ const ready = yield* Effect.makeLatch(false);
481
+ const healthy = yield* Effect.makeLatch(true);
482
+ const paused = yield* Effect.makeLatch(true);
483
+ yield* Metric.set(Metric.tagged(Metric.tagged(healthStateGauge, "daemon", name), "latch", "ready"), 0);
484
+ yield* Metric.set(Metric.tagged(Metric.tagged(healthStateGauge, "daemon", name), "latch", "healthy"), 1);
485
+ yield* Metric.set(Metric.tagged(Metric.tagged(healthStateGauge, "daemon", name), "latch", "paused"), 1);
486
+ return {
487
+ name,
488
+ ready,
489
+ healthy,
490
+ paused
491
+ };
492
+ });
493
+ const allocateSupervisorHealth = (name, children) => Effect.gen(function* () {
494
+ const ready = yield* Effect.makeLatch(false);
495
+ const healthy = yield* Effect.makeLatch(true);
496
+ const paused = yield* Effect.makeLatch(true);
497
+ yield* Metric.set(Metric.tagged(Metric.tagged(healthStateGauge, "daemon", name), "latch", "ready"), 0);
498
+ yield* Metric.set(Metric.tagged(Metric.tagged(healthStateGauge, "daemon", name), "latch", "healthy"), 1);
499
+ yield* Metric.set(Metric.tagged(Metric.tagged(healthStateGauge, "daemon", name), "latch", "paused"), 1);
500
+ return {
501
+ name,
502
+ ready,
503
+ healthy,
504
+ paused,
505
+ children
506
+ };
507
+ });
508
+ const bootChild = (child) => {
509
+ if (isWorker(child)) return allocateWorkerHealth(child.name).pipe(Effect.map((health) => ({
510
+ name: child.name,
511
+ health,
512
+ run: buildWorkerLoop(child, health).pipe(Effect.orDie),
513
+ childPolicy: child.child
514
+ })));
515
+ return Effect.gen(function* () {
516
+ const bootedChildren = yield* Effect.forEach(child.children, bootChild);
517
+ const health = yield* allocateSupervisorHealth(child.name, bootedChildren.map((b) => b.health));
518
+ const run = buildSupervisorBody(child, health, bootedChildren).pipe(Effect.orDie);
519
+ return {
520
+ name: child.name,
521
+ health,
522
+ run,
523
+ childPolicy: {}
524
+ };
525
+ });
526
+ };
527
+ //#endregion
528
+ //#region src/internal/dynamic.ts
529
+ const buildDynamic = (spec, health) => Effect.gen(function* () {
530
+ const state = yield* Ref.make({
531
+ nextId: 0,
532
+ children: HashMap.empty()
533
+ });
534
+ const startChildImpl = (args) => Effect.gen(function* () {
535
+ const worker = spec.child(args);
536
+ const loop = buildWorkerLoop(worker, yield* allocateWorkerHealth(worker.name)).pipe(Effect.orDie);
537
+ const removed = yield* Effect.makeLatch(false);
538
+ const reservedId = yield* Ref.modify(state, (current) => {
539
+ if (HashMap.size(current.children) >= spec.maxChildren) return [Option.none(), current];
540
+ const children = HashMap.set(current.children, current.nextId, {
541
+ fiber: Option.none(),
542
+ removed
543
+ });
544
+ return [Option.some(current.nextId), {
545
+ nextId: current.nextId + 1,
546
+ children
547
+ }];
548
+ });
549
+ if (Option.isNone(reservedId)) return yield* new DynamicLimitExceeded({ limit: spec.maxChildren });
550
+ const id = reservedId.value;
551
+ yield* Metric.set(supervisorChildrenGauge, HashMap.size(yield* Ref.get(state).pipe(Effect.map((s) => s.children))));
552
+ const cleanup = Effect.gen(function* () {
553
+ const count = yield* Ref.modify(state, (current) => {
554
+ const children = HashMap.remove(current.children, id);
555
+ return [HashMap.size(children), {
556
+ ...current,
557
+ children
558
+ }];
559
+ });
560
+ yield* Metric.set(supervisorChildrenGauge, count);
561
+ yield* removed.open;
562
+ }).pipe(Effect.asVoid);
563
+ const fiber = yield* Effect.forkScoped(loop.pipe(Effect.ensuring(cleanup)));
564
+ const count = yield* Ref.modify(state, (current) => {
565
+ const childOpt = HashMap.get(current.children, id);
566
+ if (Option.isNone(childOpt)) return [HashMap.size(current.children), current];
567
+ const children = HashMap.set(current.children, id, {
568
+ ...childOpt.value,
569
+ fiber: Option.some(fiber)
570
+ });
571
+ return [HashMap.size(children), {
572
+ nextId: current.nextId,
573
+ children
574
+ }];
575
+ });
576
+ yield* Metric.set(supervisorChildrenGauge, count);
577
+ return {
578
+ id,
579
+ removed: removed.await
580
+ };
581
+ });
582
+ const stopChildImpl = (ref) => Effect.gen(function* () {
583
+ const [stateOpt, count] = yield* Ref.modify(state, (current) => {
584
+ const found = HashMap.get(current.children, ref.id);
585
+ const children = HashMap.remove(current.children, ref.id);
586
+ return [[found, HashMap.size(children)], {
587
+ ...current,
588
+ children
589
+ }];
590
+ });
591
+ if (Option.isSome(stateOpt)) {
592
+ const { fiber, removed } = stateOpt.value;
593
+ yield* Option.match(fiber, {
594
+ onNone: () => Effect.void,
595
+ onSome: (running) => Effect.gen(function* () {
596
+ yield* Fiber.interrupt(running);
597
+ yield* Fiber.await(running);
598
+ })
599
+ });
600
+ yield* Metric.set(supervisorChildrenGauge, count);
601
+ yield* removed.open;
602
+ }
603
+ });
604
+ const countImpl = Ref.get(state).pipe(Effect.map((current) => HashMap.size(current.children)));
605
+ yield* Effect.zipRight(health.ready.open, Metric.set(Metric.tagged(Metric.tagged(healthStateGauge, "daemon", spec.name), "latch", "ready"), 1));
606
+ return {
607
+ health,
608
+ startChild: startChildImpl,
609
+ stopChild: stopChildImpl,
610
+ count: countImpl
611
+ };
612
+ });
613
+ //#endregion
614
+ //#region src/internal/lock-gate.ts
615
+ const decideLockGate = (lock) => {
616
+ if (lock.mode === "none") return Option.none();
617
+ return Option.some({
618
+ key: lock.key,
619
+ mode: lock.mode
620
+ });
621
+ };
622
+ //#endregion
623
+ //#region src/run.ts
624
+ const applyLock = (lock, effect) => {
625
+ const gate = decideLockGate(lock);
626
+ if (Option.isNone(gate)) return effect;
627
+ const locked = withLeaderLock(effect, gate.value);
628
+ if (lock.mode !== "required") return locked;
629
+ const retryWithRestart = (eff) => Effect.retry(eff, {
630
+ schedule: lock.acquireRetryBackoff,
631
+ while: Predicate.isTagged("LeaderLockNotAcquired")
632
+ }).pipe(Effect.catchTag("LeaderLockNotAcquired", () => retryWithRestart(eff)));
633
+ return retryWithRestart(locked);
634
+ };
635
+ const isModeNone = (lock) => lock.mode === "none";
636
+ function worker(w) {
637
+ return Effect.gen(function* () {
638
+ const health = yield* allocateWorkerHealth(w.name);
639
+ const loop = buildWorkerLoop(w, health).pipe(Effect.orDie);
640
+ if (isModeNone(w.lock)) yield* Effect.forkScoped(loop);
641
+ else {
642
+ const locked = applyLock(w.lock, loop);
643
+ yield* Effect.forkScoped(locked.pipe(Effect.orDie));
644
+ }
645
+ return health;
646
+ });
647
+ }
648
+ function supervisor(s) {
649
+ return Effect.gen(function* () {
650
+ const booted = yield* Effect.forEach(s.children, bootChild);
651
+ const health = yield* allocateSupervisorHealth(s.name, booted.map((b) => b.health));
652
+ const body = buildSupervisorBody(s, health, booted).pipe(Effect.orDie);
653
+ if (isModeNone(s.lock)) yield* Effect.forkScoped(body);
654
+ else {
655
+ const locked = applyLock(s.lock, body);
656
+ yield* Effect.forkScoped(locked.pipe(Effect.orDie));
657
+ }
658
+ return health;
659
+ });
660
+ }
661
+ const dynamic$1 = (spec) => Effect.gen(function* () {
662
+ return yield* buildDynamic(spec, yield* allocateSupervisorHealth(spec.name, []));
663
+ });
664
+ const run = {
665
+ worker,
666
+ supervisor,
667
+ dynamic: dynamic$1
668
+ };
669
+ //#endregion
670
+ //#region src/supervision-preset.ts
671
+ var LeaderConfig = class extends Context.Reference()("@systemfsoftware/effect-daemon-spec/LeaderConfig", { defaultValue: () => ({
672
+ backoffBase: Duration.seconds(1),
673
+ intensity: new UnboundedIntensity(),
674
+ cooldown: Duration.zero
675
+ }) }) {};
676
+ var WorkerConfig = class extends Context.Reference()("@systemfsoftware/effect-daemon-spec/WorkerConfig", { defaultValue: () => ({
677
+ backoffBase: Duration.seconds(10),
678
+ intensity: new BoundedIntensity({
679
+ restarts: 10,
680
+ window: Duration.seconds(60)
681
+ }),
682
+ cooldown: Duration.seconds(30)
683
+ }) }) {};
684
+ var TaskConfig = class extends Context.Reference()("@systemfsoftware/effect-daemon-spec/TaskConfig", { defaultValue: () => ({
685
+ backoffBase: Duration.seconds(1),
686
+ intensity: new UnboundedIntensity(),
687
+ cooldown: Duration.zero
688
+ }) }) {};
689
+ const Supervision = {
690
+ leader: (cap) => Effect.gen(function* () {
691
+ const config = yield* LeaderConfig;
692
+ return {
693
+ intensity: config.intensity,
694
+ backoff: cappedBackoff(config.backoffBase, cap),
695
+ cooldown: config.cooldown
696
+ };
697
+ }),
698
+ worker: (cap) => Effect.gen(function* () {
699
+ const config = yield* WorkerConfig;
700
+ return {
701
+ intensity: config.intensity,
702
+ backoff: cappedBackoff(config.backoffBase, cap),
703
+ cooldown: config.cooldown
704
+ };
705
+ }),
706
+ task: (budget) => Effect.gen(function* () {
707
+ const config = yield* TaskConfig;
708
+ return {
709
+ intensity: config.intensity,
710
+ backoff: Schedule.exponential(config.backoffBase).pipe(Schedule.jittered, Schedule.upTo(budget)),
711
+ cooldown: config.cooldown
712
+ };
713
+ }),
714
+ custom: (policy) => Effect.succeed(policy)
715
+ };
716
+ //#endregion
717
+ //#region src/supervisor.ts
718
+ const makeSupervisor = (opts, strategy) => ({
719
+ [SupervisorTypeId]: SupervisorTypeId,
720
+ name: opts.name,
721
+ strategy,
722
+ children: opts.children,
723
+ supervision: opts.supervision,
724
+ lock: opts.lock,
725
+ reporter: opts.reporter ?? {}
726
+ });
727
+ const oneForOne = (opts) => makeSupervisor(opts, "one_for_one");
728
+ const oneForAll = (opts) => makeSupervisor(opts, "one_for_all");
729
+ const restForOne = (opts) => makeSupervisor(opts, "rest_for_one");
730
+ const dynamic = (opts) => ({
731
+ [DynamicSpecTypeId]: DynamicSpecTypeId,
732
+ name: opts.name,
733
+ child: opts.child,
734
+ maxChildren: opts.maxChildren ?? 1e3
735
+ });
736
+ //#endregion
737
+ export { BoundedIntensity, ChildPolicyConfig, Daemon, DaemonReporter, DynamicLimitExceeded, DynamicSpecTypeId, Intensity, IntensityConfig, IntensityTypeId, LeaderConfig, LeaderLock, LeaderLockFromPrimitive, LeaderLockInfraError, LeaderLockNotAcquired, LockPolicyConfig, LockPrimitive, LockPrimitiveError, Supervision, SupervisorTypeId, TaskConfig, TickPolicyConfig, UnboundedIntensity, WorkerConfig, WorkerTypeId, cappedBackoff, dynamic, healthStateGauge, isSupervisor, isWorker, oneForAll, oneForOne, poll, restForOne, run, stream, subscription, supervisor, supervisorChildrenGauge, supervisorExhaustionsCounter, supervisorRestartsCounter, withLeaderLock, worker };