Frank.Datastar
7.3.3
dotnet add package Frank.Datastar --version 7.3.3
NuGet\Install-Package Frank.Datastar -Version 7.3.3
<PackageReference Include="Frank.Datastar" Version="7.3.3" />
<PackageVersion Include="Frank.Datastar" Version="7.3.3" />
<PackageReference Include="Frank.Datastar" />
paket add Frank.Datastar --version 7.3.3
#r "nuget: Frank.Datastar, 7.3.3"
#:package Frank.Datastar@7.3.3
#addin nuget:?package=Frank.Datastar&version=7.3.3
#tool nuget:?package=Frank.Datastar&version=7.3.3
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
datastarcustom operation for SSE streaming - Helper Functions: Clean
Datastar.*helper functions for common operations - Stream-Based Rendering: Zero-allocation HTML streaming via
TextWriterfor 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
TextWriteroutput (e.g., HoxRender.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
- Primary:
Datastar.patchElements- Send HTML to update the UI - Supporting:
Datastar.tryReadSignals- Read form inputs to decide what HTML to send - Rare:
Datastar.patchSignals- Update minimal client state (counters, flags) - 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:
- Frank: Provides the computation expression framework for defining HTTP resources
- A native SSE implementation: Writes Datastar's SSE event grammar directly via ASP.NET Core's
IBufferWriter<byte>API — no external Datastar SDK dependency - 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.
Related Projects
Sample applications: Frank.Datastar.Basic, Frank.Datastar.Hox, and Frank.Datastar.Oxpecker.
See the project repository for the complete guide.
License
| Product | Versions 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. |
-
net10.0
- Frank (>= 7.3.3)
- FSharp.Core (>= 10.1.302)
-
net8.0
- Frank (>= 7.3.3)
- FSharp.Core (>= 10.1.302)
-
net9.0
- Frank (>= 7.3.3)
- FSharp.Core (>= 10.1.302)
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.