Frank.Datastar 7.3.3

dotnet add package Frank.Datastar --version 7.3.3
                    
NuGet\Install-Package Frank.Datastar -Version 7.3.3
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Frank.Datastar" Version="7.3.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Frank.Datastar" Version="7.3.3" />
                    
Directory.Packages.props
<PackageReference Include="Frank.Datastar" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Frank.Datastar --version 7.3.3
                    
#r "nuget: Frank.Datastar, 7.3.3"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Frank.Datastar@7.3.3
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Frank.Datastar&version=7.3.3
                    
Install as a Cake Addin
#tool nuget:?package=Frank.Datastar&version=7.3.3
                    
Install as a Cake Tool

Frank.Datastar

An F# library that integrates Datastar's Server-Sent Events (SSE) capabilities with the Frank web framework through idiomatic computation expression builders.

Features

  • Single Stream Handler: Provides one datastar custom operation for SSE streaming
  • Helper Functions: Clean Datastar.* helper functions for common operations
  • Stream-Based Rendering: Zero-allocation HTML streaming via TextWriter for high-throughput scenarios
  • Type-Safe: Leverages F# type system for safe signal handling
  • Hypermedia-First: Designed around sending HTML (not managing client state)
  • HTTP Method Flexibility: Supports GET, POST, and other HTTP methods

Installation

dotnet add package Frank.Datastar

Quick Start

Basic Streaming Example

open Frank
open Frank.Builder
open Frank.Datastar

let displayTime =
    resource "/time" {
        name "DisplayTime"
        datastar (fun ctx -> task {
            let time = System.DateTime.Now.ToString("HH:mm:ss")
            do! Datastar.patchElements $"""<div id="time">{time}</div>""" ctx
        })
    }

Multiple Progressive Updates

The primary use case for Datastar is streaming multiple HTML updates:

let loadDashboard =
    resource "/dashboard" {
        name "LoadDashboard"
        datastar (fun ctx -> task {
            // Send header first
            do! Datastar.patchElements """<div id="header">Loading...</div>""" ctx

            // Fetch and send stats
            let! stats = fetchStatsAsync()
            do! Datastar.patchElements $"""<div id="stats">{renderStats stats}</div>""" ctx

            // Fetch and send activity
            let! activity = fetchActivityAsync()
            do! Datastar.patchElements $"""<div id="activity">{renderActivity activity}</div>""" ctx
        })
    }

POST with Signal Reading

[<CLIMutable>]
type FormSignals = { query: string }

let submitSearch =
    resource "/search" {
        name "SubmitSearch"
        datastar HttpMethods.Post (fun ctx -> task {
            let! signals = Datastar.tryReadSignals<FormSignals> ctx

            match signals with
            | ValueSome form ->
                let! results = searchAsync form.query
                do! Datastar.patchElements (renderResults results) ctx
            | ValueNone ->
                do! Datastar.patchElements """<div id="error">Invalid form</div>""" ctx
        })
    }

Stream-Based Rendering (High Performance)

For high-throughput scenarios or large HTML payloads, use stream-based overloads to eliminate string allocations:

// With manual TextWriter usage
let loadDashboardStreaming =
    resource "/dashboard-stream" {
        name "LoadDashboardStreaming"
        datastar (fun ctx -> task {
            do! Datastar.streamPatchElements (fun writer -> task {
                do! writer.WriteAsync("<div id='stats'>")
                do! writer.WriteAsync("Users: 1,234")
                do! writer.WriteAsync("</div>")
            }) ctx
        })
    }

// With view engine supporting TextWriter (e.g., Hox)
open Hox.Rendering

let loadUsersStreaming =
    resource "/users-stream" {
        name "LoadUsersStreaming"
        datastar (fun ctx -> task {
            let! users = fetchUsersAsync()
            let node = h("div#user-list", fragment [ for user in users do userCard user ])

            // Stream directly to response - no intermediate string allocation
            do! Datastar.streamPatchElements (fun writer ->
                Render.toTextWriter writer node
            ) ctx
        })
    }

API Reference

Datastar Philosophy

Hypermedia First: The primary pattern in Datastar is sending HTML from the server. The server is the source of truth.

Minimal Signals: Signals should be used sparingly, primarily for:

  • Form input bindings (<input data-bind:field>)
  • Ephemeral UI state (toggle switches, tabs)
  • Passing small amounts of data to the server

The datastar Custom Operation

The only custom operation on ResourceBuilder. Starts an SSE stream and executes your handler:

// Default: GET method
resource "/endpoint" {
    datastar (fun ctx -> task {
        do! Datastar.patchElements "<div>Content</div>" ctx
    })
}

// With specific HTTP method
resource "/endpoint" {
    datastar HttpMethods.Post (fun ctx -> task {
        let! signals = Datastar.tryReadSignals<MySignals> ctx
        // Process and respond...
    })
}

Helper Functions

The Datastar module provides helper functions for use inside the datastar handler:

module Datastar =
    // PRIMARY: Send HTML to update the UI
    let patchElements (html: string) (ctx: HttpContext) : Task<unit>

    // STREAM-BASED: Zero-allocation HTML streaming
    let streamPatchElements (writer: TextWriter -> Task) (ctx: HttpContext) : Task<unit>

    // SECONDARY: Update ephemeral client state
    let patchSignals (signals: string) (ctx: HttpContext) : Task<unit>
    let streamPatchSignals (writer: TextWriter -> Task) (ctx: HttpContext) : Task<unit>

    // Remove an element by CSS selector
    let removeElement (selector: string) (ctx: HttpContext) : Task<unit>
    let streamRemoveElement (writer: TextWriter -> Task) (ctx: HttpContext) : Task<unit>

    // Execute JavaScript (use sparingly)
    let executeScript (script: string) (ctx: HttpContext) : Task<unit>
    let streamExecuteScript (writer: TextWriter -> Task) (ctx: HttpContext) : Task<unit>

    // Read and deserialize signals from request body
    let tryReadSignals<'T> (ctx: HttpContext) : Task<voption<'T>>
Stream-Based vs String-Based

Use stream-based overloads when:

  • High throughput required (1000+ events/sec)
  • Large HTML payloads (reducing allocations matters)
  • View engine supports TextWriter output (e.g., Hox Render.toTextWriter)

Use string-based API when:

  • Simple scenarios with small HTML strings
  • View engine only produces strings
  • Allocation profile is not a concern

Stream-based operations eliminate full HTML string materialization, providing 50%+ allocation reduction in high-throughput scenarios.

Usage Priority

  1. Primary: Datastar.patchElements - Send HTML to update the UI
  2. Supporting: Datastar.tryReadSignals - Read form inputs to decide what HTML to send
  3. Rare: Datastar.patchSignals - Update minimal client state (counters, flags)
  4. Special: Datastar.executeScript, Datastar.removeElement - For specific use cases

Complete Example

open System
open Frank
open Frank.Builder
open Frank.Datastar
open Microsoft.AspNetCore.Builder

[<CLIMutable>]
type SearchSignals = { query: string }

let displayDate =
    resource "/displayDate" {
        name "DisplayDate"
        datastar (fun ctx -> task {
            let today = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")
            do! Datastar.patchElements $"""<div id='target'><b>{today}</b></div>""" ctx
        })
    }

let searchItems =
    resource "/search" {
        name "SearchItems"
        datastar (fun ctx -> task {
            let query = ctx.Request.Query.["q"].ToString()

            let results =
                [ "Apple"; "Banana"; "Cherry"; "Date" ]
                |> List.filter (fun item -> item.Contains(query, StringComparison.OrdinalIgnoreCase))

            let html =
                if results.IsEmpty then
                    """<div id='results'>No results</div>"""
                else
                    let items = results |> List.map (fun r -> $"<li>{r}</li>") |> String.concat ""
                    $"""<ul id='results'>{items}</ul>"""

            do! Datastar.patchElements html ctx
        })
    }

let loadDashboard =
    resource "/dashboard" {
        name "LoadDashboard"
        datastar (fun ctx -> task {
            // Progressive loading: send updates as they become available
            do! Datastar.patchElements """<div id='header'>Dashboard</div>""" ctx
            do! Task.Delay(100)
            do! Datastar.patchElements """<div id='stats'>Users: 1,234</div>""" ctx
            do! Task.Delay(100)
            do! Datastar.patchElements """<div id='activity'>3 new items</div>""" ctx
        })
    }

[<EntryPoint>]
let main args =
    webHost args {
        useDefaults
        plug StaticFileExtensions.UseStaticFiles

        resource displayDate
        resource searchItems
        resource loadDashboard
    }
    0

Architecture

Frank.Datastar is built on:

  1. Frank: Provides the computation expression framework for defining HTTP resources
  2. A native SSE implementation: Writes Datastar's SSE event grammar directly via ASP.NET Core's IBufferWriter<byte> API — no external Datastar SDK dependency
  3. ASP.NET Core: The underlying web framework

The library extends Frank's ResourceBuilder with the datastar custom operation that:

  • Starts the SSE stream automatically
  • Executes your handler function
  • Manages response headers and flushing

Hox Integration

Frank.Datastar works with Hox for type-safe HTML rendering:

open Hox
open Hox.Core
open Hox.Rendering

let userCard (user: User) =
    h("div.user-card",
        [ h($"img [src={user.Avatar}] [alt={user.Name}]", [])
          h("div.user-info",
              [ h("h3", [ Text user.Name ])
                h("p", [ Text user.Email ]) ]) ])

// String-based rendering
let loadUsers =
    resource "/users" {
        name "LoadUsers"
        datastar (fun ctx -> task {
            let! users = fetchUsersAsync()
            let node = h("div#user-list", fragment [ for user in users do userCard user ])
            let! html = Render.asString node
            do! Datastar.patchElements html ctx
        })
    }

// Stream-based rendering (zero allocations)
let loadUsersStreaming =
    resource "/users-streaming" {
        name "LoadUsersStreaming"
        datastar (fun ctx -> task {
            let! users = fetchUsersAsync()
            let node = h("div#user-list", fragment [ for user in users do userCard user ])
            // Stream directly to response - no string allocation
            do! Datastar.streamPatchElements (fun writer ->
                Render.toTextWriter writer node
            ) ctx
        })
    }

Note: Hox uses CSS selector notation for attributes: [attr=value]

Performance Tip: Use Render.toTextWriter with streamPatchElements for zero-allocation HTML streaming in high-throughput scenarios.

  • Frank - F# web framework
  • Datastar - Hypermedia framework
  • Hox - Async HTML rendering library for F#

Sample applications: Frank.Datastar.Basic, Frank.Datastar.Hox, and Frank.Datastar.Oxpecker.

See the project repository for the complete guide.

License

MIT

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
7.3.3 83 8/11/2026
7.3.2 85 8/10/2026
7.2.1 118 6/22/2026
7.2.0 149 2/10/2026
7.1.0 145 2/7/2026
7.0.0-build.0 80 2/6/2026
6.5.0 165 2/5/2026
6.4.1 142 2/4/2026
6.4.1-build.0 79 2/4/2026
6.4.0 136 2/2/2026
6.4.0-build.0 80 2/2/2026

### New in 7.3.3 (Released 2026-08-10)
**Frank.Rdf - Async IBufferWriter Streaming**
- **New: `Doc.writeJsonLdAsync doc bufferWriter`** — async overload that writes JSON-LD expanded-form directly to an `IBufferWriter<byte>` (e.g. `HttpResponse.BodyWriter`/`PipeWriter`), without intermediate string allocation or copying. Encodes UTF8 directly to the buffer for maximum efficiency in response streaming. Completes after serialization and flushing to the buffer.
- **Recommended for response streaming:** `writeJsonLdAsync` is the preferred method when serving JSON-LD from a Frank handler over HTTP. Streaming directly to `PipeWriter` avoids `AllowSynchronousIO` requirements and eliminates intermediate buffering layers that `StreamWriter` would introduce.
- **Three serialization options now available:** Use `writeJsonLdAsync` for HTTP responses (most efficient), `writeJsonLd` for flexibility with any `TextWriter`, and `toJsonLd` for testing/debugging.
- **Sample updated:** `sample/Frank.Rdf.Sample` demonstrates `Doc.writeJsonLdAsync` streaming the `application/ld+json` representation directly to the response body via `negotiate { }` content negotiation.
- **Test coverage:** comprehensive test cases for `writeJsonLdAsync` including single and multi-subject documents, language-tagged strings, and round-trip parsing.