💾 Archived View for d.moonfire.us › blog › 2023 › 01 › 21 › faking-time-magic-with-nitride captured on 2023-07-22 at 16:48:29. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2023-04-26)
-=-=-=-=-=-=-
It's been a while since I've talked about MfGames.Nitride[1] and I thought I would do a short post about working with time-sensitive posts. In most cases, this is dating blob posts but it can also be how I dole out weekly chapters while building up a buffer while I enjoy some down time.
All the time elements in Nitride use NodaTime[2] instead of the base class library's `DateTime`. Part of this is because it predates the advent of `LocalDate` but also because NodaTime has good abstractions around providing the time for reliable manipulations of time, an obsession with correctness, and also a deep understanding of the complexities of temporal elements.
None of these really matter, but I like the library and I felt it was a solid base. This is also one reason why Nitride is opinionated.
To use the temporal library, you need to include the NuGet package[3] into your project and then either inject the module into the class or use the extension method.
3: https://src.mfgames.com/mfgames-cil/-/packages/nuget/mfgames.nitride.temporal
NitrideBuilder builder = new NitrideBuilder(args) .UseTemporal( config => { config .WithDateTimeZone("America/Chicago") .WithDateOptionCommandLineOption(); });
The configuration is optional, but this shows the two most common options. The first sets the time zone for all the local dates to handle when your site generates even if your server or build machine is UTC or in a different time zone.
The other adds a `--date YYYY-MM-DD` option to the command line to let you choose the “now” that the site is running, such as to preview next week's post or see how everything will turn out. If the `--date` is not provided or the command line option isn't used, then “now” will be now. Well, then, but it was now then, but if you run it now, then it will now. Well, then. (Spaceballs[4] reference.)
4: https://en.wikipedia.org/wiki/Spaceballs
dotnet run -- build --date 2023-01-25
Even with the module, nothing more is going to happen than additional logging line in the output. Because this is an Entity-Component-System[5], adding time to an `Entity` is as simple as setting the instant.
5: /tags/entity-component-system/
Instant instant; var entity = new Entity().Set(instant);
Of course, this only applies to a single entry, so there are two operations included that will mass assign `Instant` components to entities. The first is `SetInstantFromComponent`. This gets a `LocalDate`, `DateTime`, `DateTimeOffset`, or `Instant` from the entity (`Model.Date` in this example) and returns it. If this returns a null, then no instant will be assigned.
public class Model { public string Access { get; set; } = "public"; public DateTime? Date { get; set; } } public Pipeline : PipelineBase { private SetInstantFromComponent<PageModel> op; public Pipeline( SetInstantFromComponent<PageModel> op) { this.op = op .WithGetDateTimeObject(entity => entity.Get<Model>().Date); } public override IAsyncEnumerable<Entity> RunAsync( IEnumerable<Entity> entities, CancellationToken cancellationToken = default) { return entities .Run(op) .ToList() .ToAsyncEnumerable(); } }
As a basic functionality, it is pretty simple. However, the most common way I need to assign an `Instant` are my blog posts which all have the following pattern:
To handle this common condition, there is a second operation: `SetInstantFromPath`.
SetInstantFromPath op; return entities .Run(op) .ToList() .ToAsyncEnumerable();
This one doesn't require any customization to it because it uses a regular expression with named groups to calculate the three components. This is the same as setting the regular expression:
return entities .Run(op.WithPathRegex(@"(?<year>\d{4})[/-](?<month>\d{2})[/-](?<day>\d{2})")) .ToList() .ToAsyncEnumerable();
Obviously the default is ISO date time, but I'm not going to support anything besides that in the defaults since you can writ a custom regular expression with named groups to handle any arbitrary format. The default also handles `2028/01-21-faking-time-magic-with-nitride.md` as well as my normal format. In the end, the `Entity` has an `Instant` component.
Of course, setting an `Instant` requires it to be used somewhere. Since it is a component, it is relatively easy to get the latest X posts from a list of entities.
var latest = entities .WhereEntity<Instant>() .OrderByDescening(entity => entity.Get<Instant>()) .Take(x) .ToList();
This is used to create feeds, but that is a topic for later since generating Atom feeds needs some more attention before I'm comfortable with it.
The instant can also be used to remove future instants via the creatively named `FilterOutFutureInstant` which has no configurations thank to the use of the `--date` parameter and `TimeService` in its dependency-injected constructor.
FilterOutFutureInstant op; return entities .Run(op) .ToList() .ToAsyncEnumerable();
In most cases, I gather up all the pages, generate the project pages so I can show future chapters but grayed out, then remove the future ones before producing the HTML so no one can “cheat” by just adding one to the page.
The above operations and concepts pretty much got me through generating d.moonfire.us[6]. On the other hand, fedran.com[7] needed something more since I don't date my chapters. Instead, they are things like fedran.com/sand-and-blood/chapter-01/[8] and they need to be sent out every week until I run out of buffer or fail to keep up.
8: //fedran.com/sand-and-blood/chapter-01/
To support that, I added a second NuGet package[9], `MfGames.Temporal.Schedules` which contains more complex operations that adjust or change an entity's attributes based on the current date. In both cases, they are driven off a “path” which may be any arbitrary component but default to the entity's Zio.Upath[10] component. They allow me to set up a schedule in a separate YAML file (could be JSON, could be on the page) and then they change.
9: https://src.mfgames.com/mfgames-cil/-/packages/nuget/mfgames.nitride.temporal
10: https://github.com/xoofx/zio
The first is `PeriodicPathRegexSchedule` which is the initial evolution of writing a schedule for static sites. It uses a regular expression to capture the numeric part of a path (`12` from `chapter-12`) and figure out how long to show it. If we use the `Model.Access` above to control access, we can easily make chapters available for subscribers and then spread them out a week at a time starting at the beginning of the year.
schedules: # Patron and Ko-Fi subscribers get it all at once - pathRegex: chapters/chapter-(\d+) scheduleStart: 2000-01-01 schedulePeriod: instant access: subscribers - pathRegex: chapters/chapter-(\d+) scheduleStart: 2023-01-01 schedulePeriod: 1 week access: public
Depending on the “now” when the site is run, all the chapters are available to patrons but they will be made public at the rate of one per week starting with the first chapter on January 1st. Of course, with regular expressions, I could easily stop posting chapters for a while by splitting the regular expression in two to cover two ranges. Or I can have two schedules that have a tier-1 and a tier-2 release with a public release a week after tier-2 gets it.
schedules: - pathRegex: chapters/chapter-(\d+) scheduleStart: 2000-01-01 schedulePeriod: tier-1 access: subscribers - pathRegex: chapters/chapter-(\d+) scheduleStart: 2023-01-01 schedulePeriod: 1 week access: tier-2 - pathRegex: chapters/chapter-(\d+) scheduleStart: 2023-01-08 schedulePeriod: 1 week access: public
This can also get complicated when I had to take breaks from chapters:
schedules: # Patron and Ko-Fi subscribers get it all at once - pathRegex: chapters/chapter-([0-1]\d|2[0-4]) scheduleStart: 2000-01-01 schedulePeriod: 2 weeks access: public - pathRegex: chapters/chapter-(2[5-9]|[3-9]\d) scheduleStart: 2023-01-01 schedulePeriod: 2 weeks access: public
Obviously, implementing this is more complicated then setting the instant from the path.
public class PageSchedule : NumericalPathSchedule { public Access { get; set; } protected override Entity Apply(Entity entity, int number, Instant instant) { var model = entity.Get<Model>(); model.Access = this.Access; return entity.Set(instant, model); } } List<PageSchedule> schedules; Ienumerable<Entity> entities; return applySchedules.WithSchedules(_ => schedules).Run(entities);
The second method for schedules is `IndexedPathRegexSchedule`. This is the most complicated to set up in C# code, but I find the easiest to comprehend in the YAML code. Instead of having a complex set of schedules and multiple regular expressions, it goes with a single regular expression to identify the numerical component and then has a dictionary of elements that say “from chapters X until the next, for chapters Y and later, do this, etc.”
schedules: pathRegex: chapters/chapter-(\d+) indexes: 1: scheduleStart: 2025-01-01 schedulePeriod: instant # means all the chapters 1-10 at once access: subscribers 10: scheduleStart: 2030-01-01 # chapter 10 on 2030-01-01, chapter 11 on 2023-01-08, etc. schedulePeriod: 1 week access: subscribers 30: schedulePeriod: never # never going to see these
This one works easier for me, mainly because Fedran doesn't have subscriber tiers or complex logic but I do have frequent pauses while posting. Also, the schedule isn't an outer list (though it could be, `ApplySchedules` applies every schedule in order so you can have two of these without a problem).
public class Schedule : IndexedSchedule { public string? Access { get; set; } protected override Entity Apply( Entity entity, int number, Instant instant) { TestModel model = entity.Get<TestModel>(); model.Access = this.Access; return entity.SetAll(instant, model); } } IndexedPathRegexSchedule<Schedule> schedules; Ienumerable<Entity> entities; return applySchedules.WithSchedules(_ => new [] { schedules }).Run(entities);
As you may notice, these don't filter out or do anything other than set the `Instant` and maybe some other components of an `Entity`. This follows the Single Responsibility Principle (SRP) but also “do one thing well”. If you want to filter it out, use an operation for that. If you want to dynamically change the contents of a page based on the current day, that's a schedule.
This is also a starting point. Everything is fluid. I have thirteen websites I'm planning on converting over to Nitride over the next year or so. The fourth site is mfgames.com when I'll start documenting all of this and probably start pushing to see if others want to use it. Right now, the only documentation comes from https://src.mfgames.com/mfgames-cil/ in the examples, read me files, and tests.
Categories:
Tags:
Below are various useful links within this site and to related sites (not all have been converted over to Gemini).
https://d.moonfire.us/blog/2023/01/21/faking-time-magic-with-nitride/