Nexus.Stratum (0.1.0-alpha.7)

Published 2026-06-14 06:51:02 +00:00 by nexus-ci

Installation

dotnet nuget add source --name NEXUS --username your_username --password your_token 
dotnet add package --source NEXUS --version 0.1.0-alpha.7 Nexus.Stratum

About this package

STRATUM: the NEXUS storage abstraction. Typed storage contracts (event stores first) over in-memory / browser / git backings, for Blazor WASM and regular .NET.

Nexus.Stratum

The NEXUS storage abstraction: one library exposing a family of typed storage contracts over pluggable backings, for Blazor WASM and regular .NET alike. The event store is the first contract species; a blob/asset species follows when its first concrete consumer exists.

  • F# on .NET 10. License: AGPL-3.0-or-later.
  • Design doc (living, decision-cited): model.fsx.
  • Decisions and findings: NEXUS CHRONICON (events tagged stratum).
  • Browser (Blazor WASM, OPFS) durable backing: the separate Nexus.Stratum.Browser package.

The package ships per-member XML API docs (lib/net10.0/Nexus.Stratum.xml): your IDE surfaces them on hover, and an agent can read the public surface with nuget-xml.sh Nexus.Stratum <version> --surface once the package is restored. For the design rationale behind the contracts, read model.fsx and the stratum-tagged CHRONICON events — that is where the "why" lives.

Install

Published to the NEXUS org NuGet feed on Forgejo (https://forgejo.ivanthegeek.com/api/packages/NEXUS/nuget/index.json). Bind an exact pre-release pin (floating ranges do not resolve pre-release overlay versions):

<PackageReference Include="Nexus.Stratum" Version="0.1.0-alpha.7" />

For FSI / .fsx exploration, reference the package, not a consumer DLL (a class-library consumer's bin/ does not contain Nexus.Stratum.dll, only the NuGet cache does):

#r "nuget: Nexus.Stratum, 0.1.0-alpha.7"
open Nexus.Stratum

Core concepts

  • StoredEvent<'e> -- the recording envelope (Id, StoredAt, Origin) around your domain fact 'e. Its representation is private: the only constructor is StoredEvent.mint, so StoredAt/Origin are system-asserted, not forgeable.
  • EventStore<'e> -- Append / ReadAll / Subscribe. Append-only; one store instance is one logical stream. Replay order is StoredAt ascending, ties by EventId. Re-appending a value-identical event is an idempotent no-op; the same id with different content is refused loudly.
  • PayloadCodec<'e> -- the one thing your app supplies: its event DU to/from the wire. STRATUM owns everything else about the file (envelope keys, the [event] section, the TOML frame).
  • Backings -- InMemory today (tests, FSI, GUI-state stores); browser (IndexedDB/OPFS) and git-remote sync follow behind the same contract.

Quickstart

open System
open Nexus.Stratum

// 1. Your domain event DU.
type Vital =
    | GlucoseTaken of int
    | NoteAdded of string

// 2. A PayloadCodec. Decode MUST positive-match its field set --
//    FileFrame guarantees key SHAPE only and passes every [event] field
//    through, so use PayloadCodec.expectKeys to reject unknown/extra keys
//    (silent-accept is a loud-failure-doctrine violation).
let codec : PayloadCodec<Vital> =
    { Encode =
        function
        | GlucoseTaken mg -> CaseName "GlucoseTaken", [ "mg_dl", TomlInt (int64 mg) ]
        | NoteAdded text  -> CaseName "NoteAdded",    [ "text",  TomlString text ]
      Decode =
        fun caseName fields ->
            let (CaseName case) = caseName
            let find k = fields |> List.tryFind (fun (key, _) -> key = k) |> Option.map snd
            match case with
            | "GlucoseTaken" ->
                PayloadCodec.expectKeys caseName [ "mg_dl" ] fields
                |> Result.bind (fun () ->
                    match find "mg_dl" with
                    | Some (TomlInt mg) -> Ok (GlucoseTaken (int mg))
                    | _ -> Error (CodecError "GlucoseTaken: mg_dl must be an integer"))
            | "NoteAdded" ->
                PayloadCodec.expectKeys caseName [ "text" ] fields
                |> Result.bind (fun () ->
                    match find "text" with
                    | Some (TomlString t) -> Ok (NoteAdded t)
                    | _ -> Error (CodecError "NoteAdded: text must be a string"))
            | other -> Error (CodecError (sprintf "unknown case '%s'" other)) }

Mint at the composition root

Build the minter once at the composition root with the real clock and this replica's Origin, then thread the resulting 'e -> StoredEvent<'e> through the app. The clock is read once per event and asserts both StoredAt and the UUIDv7 id, so per-writer id and time order never diverge.

// Composition root (once):
let clock : Clock = fun () -> DateTimeOffset.UtcNow      // inject a fixed clock in tests
let origin = Origin (Guid.CreateVersion7())              // minted once per installation, kept in config
let recordEvent : Vital -> StoredEvent<Vital> = StoredEvent.mint clock origin

let store : EventStore<Vital> = InMemory.create ()

// Anywhere downstream:
async {
    let! result = store.Append (recordEvent (GlucoseTaken 112))
    let! all = store.ReadAll ()      // converged union, in replay order
    return result, all
}

Subscribe pushes each newly appended event exactly once (never for idempotent re-appends); handlers run inside the Append workflow, so keep them fast.

File frame

FileFrame.encode/decode map a StoredEvent<'e> to/from one TOML file per event (<uuidv7>.toml), composing your payload codec around the STRATUM-owned envelope. Both are total (Result<_, CodecError>, never throw) and name the offending input on failure.

Testing your codec

Field-set guard. Pin that an unexpected or missing payload field is refused -- PayloadCodec.expectKeys makes this the default path.

Golden tests. Mint with a fixed Clock, then reconstruct the expected file text from the minted event's own Id/StoredAt/Origin, pinning literally only the payload bytes you own (the case line and the [event] block). Do not pin a literal EventId: the envelope frame is STRATUM's golden-test responsibility (its own tests freeze it), and reconstructing from the minted event asserts the real invariant -- encode produced this event's frame -- rather than a brittle hard-coded id.

let fixedClock : Clock = fun () -> DateTimeOffset(2026, 1, 1, 12, 0, 0, TimeSpan.Zero)
let e = StoredEvent.mint fixedClock origin (GlucoseTaken 112)
let (EventId gid) = e.Id
let (Origin oid)  = e.Origin
let expected =
    [ "id        = \"" + gid.ToString("D") + "\""
      "stored_at = " + e.StoredAt.ToString("o")
      "origin    = \"" + oid.ToString("D") + "\""
      "case      = \"GlucoseTaken\""
      ""
      "[event]"
      "mg_dl = 112"
      "" ]
    |> String.concat "\n"
// Expect.equal (FileFrame.encode codec e) (Ok expected) ...

Build

Containerized, pinned SDK -- never bare dotnet:

./build         # restore + build + test
./build pack    # produce ./out/*.nupkg
./build lock    # regenerate packages.lock.json (after any PackageReference change)

Dependencies

ID Version Target Framework
FSharp.Core 10.1.301 net10.0
Details
NuGet
2026-06-14 06:51:02 +00:00
8
Ivan The Geek
63 KiB
Assets (2)
Versions (7) View all
0.1.0-alpha.7 2026-06-14
0.1.0-alpha.6 2026-06-13
0.1.0-alpha.5 2026-06-13
0.1.0-alpha.4 2026-06-13
0.1.0-alpha.3 2026-06-13