@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/CHANGELOG.md +119 -0
- package/dist/effect-daemon-spec-untrimmed.d.ts +406 -0
- package/dist/effect-daemon-spec.d.ts +33 -8
- package/dist/index.d.ts +104 -80
- package/dist/index.mjs +234 -177
- package/package.json +15 -14
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
-
|
|
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/
|
|
809
|
-
const
|
|
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/
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
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/
|
|
823
|
-
const
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
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": "
|
|
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
|
|
9
|
+
"directory": "packages/core/effect/daemon-spec"
|
|
10
10
|
},
|
|
11
|
-
"homepage": "https://github.com/systemfsoftware/systemfsoftware/tree/main/packages/effect
|
|
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": "^
|
|
40
|
+
"@systemfsoftware/effect-cell-types": "^4.0.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@effect/vitest": "4.0.0-rc.
|
|
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.
|
|
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": "^
|
|
55
|
-
"@systemfsoftware/effect-schema-
|
|
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/
|
|
59
|
-
"@systemfsoftware/
|
|
60
|
-
"@systemfsoftware/
|
|
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.
|
|
65
|
+
"effect": "^4.0.0-rc.111"
|
|
65
66
|
},
|
|
66
67
|
"publishConfig": {
|
|
67
68
|
"provenance": true
|