Fuaran.UI.ServerDriven
0.80.0
Prefix Reserved
dotnet add package Fuaran.UI.ServerDriven --version 0.80.0
NuGet\Install-Package Fuaran.UI.ServerDriven -Version 0.80.0
<PackageReference Include="Fuaran.UI.ServerDriven" Version="0.80.0" />
<PackageVersion Include="Fuaran.UI.ServerDriven" Version="0.80.0" />
<PackageReference Include="Fuaran.UI.ServerDriven" />
paket add Fuaran.UI.ServerDriven --version 0.80.0
#r "nuget: Fuaran.UI.ServerDriven, 0.80.0"
#:package Fuaran.UI.ServerDriven@0.80.0
#addin nuget:?package=Fuaran.UI.ServerDriven&version=0.80.0
#tool nuget:?package=Fuaran.UI.ServerDriven&version=0.80.0
Fuaran.UI.ServerDriven
The transport-agnostic core for server-driven interactivity over a Fuaran tree — the third client tier (alongside the Fable client renderer and the Phase 143 hydrateRoot hydration), HTMX / Phoenix-LiveView / Blazor-Server-shaped: keep the Elmish update loop on the server, ship one tiny generic JS shim to the browser, and patch the DOM in place.
The loop: browser event on a [data-fuaran-node-id] element → server runs update → re-renders the tree → diffs old→new into a TreeOp list (Fuaran.UI.OpStream.Replay.TreeOpDiff) → lowers each op to a DomPatch (rendering HTML fragments via Fuaran.UI.Renderer.Server for structure-adding ops) → sends the patch → the shim applies it (targeted, no full re-render, no flash).
What's here
DomPatch— the lowered, closure-free, browser-applyable patch vocabulary (8 primitives:SetAttr/RemoveAttr/SetText/ReplaceFragment/InsertFragment/RemoveNode/ReorderChildren/MoveNode). The shim's entire instruction set. Tagged-object camelCase JSON.ClientEffect— the parallel channel for client-only effects the server decides but the shim performs (WriteToClipboard/Navigate/Focus/Download/ReadFileBody), because they are inherently browser-side and have no DOM-mutation form.Lowering—TreeOp → DomPatch(Lowering.lower : renderFragment -> newTree -> TreeOp list -> DomPatch list). Structural ops lower directly (RemoveNode/ReorderChildren/MoveNode/InsertChild→InsertFragment); content ops re-render the changed node (looked up in the post-apply tree) into a targetedReplaceFragment;Batchflattens. The HTML renderer is injected (renderFragment : Node<'Msg> -> string) rather than a hardRenderer.Serverdependency — keeps the core Fable-clean + dependency-light and dodges theNode<obj>cast; the host wiresRender.render.content/fuaran-live-patch.js— the generic browser shim (shipped as package content undercontent/). App-agnostic, framework-free vanilla JS (~a few KB): event delegation on[data-fuaran-node-id], a transport adapter (connect/send— the client mirror ofIFuaranLiveChannel, default SSE-push + POST-receive), theDomPatchapplier (addresses by node id;MoveNode/ReorderChildrenrelocate the live element, identity-preserving), and theClientEffectperformer. Auto-starts from<script src="fuaran-live-patch.js" data-fuaran-live-stream="/live/stream" data-fuaran-live-send="/live/event">, or callFuaranLive.start(config, adapterFactory?)explicitly (a WebSocket adapter is a drop-in — only the adapter object changes; the patch/effect/delegation core is transport-identical).start()is restartable — hosts that swap the live tree per page state call it repeatedly: each call closes the previous stream and re-points the once-wired document-level delegation at the new transport (no leakedEventSource, no duplicate sends). Click payloads bridge layout interactivity server-side: a tab-header click carriespayload.index(from the server renderer'sdata-tab-index), a disclosure summary click carriespayload.open(from the<details>state). Browser-verified viasamples/server-driven(tabs + disclosure + repeatedstart()); no headless unit tests. It also publishes two read-only members for an inspecting DevTools relay peer —treeSource(the string"upstream": this page holds no tree) andisConnected()(whether the stream is up right now, never a promise that a dispatched request will be answered). Both are FACTS such a peer previously had to infer, from this global merely existing and from the reconnecting-banner styling attribute; publishing them turns an inference into a contract. Nothing tree-shaped is exposed, deliberately: a tree reconstructed from the patches this shim has applied would carry the shim's idea of the tree rather than the host's, and no client could tell that it had received one.
Live Transform sources — the per-session store (opt-in)
A TransformSource.Live binding runs a pipeline over a state-bound table and is read again on every
write. LiveTransformStore evaluates it incrementally — prime once, then advance the primed state
against what the edit changed — and the renderer consults one through BindingSources.LiveTransforms.
Somebody has to construct it and hold it for the life of a session; LiveTransform.initSession is
that composition:
let session, store =
LiveTransform.initSession
(LiveTransformOptions.identifiedBy "id") // the host's options
(fun () -> mySources) // read once per render
Render.render // the host renderer, as a function of its sources
DriverServices.createPermissive // how the host builds its services
update view initialModel
| Option | What it declares | Default |
|---|---|---|
Capacity |
How many live-Transform sites one session keeps primed at a time. At the bound the least recently used site is evicted, which is correctness-neutral — the next evaluation of an evicted site re-primes and answers the same table, having paid for it. |
64 (LiveTransformDefaults.Capacity) |
IdentityColumn |
The column whose value identifies a row — the key an edit stream addresses rows by. Nothing in a rendered tree declares one, so the host declares it here, once, for every site the session serves. | "" — none declared. Correct on every site and restricting on none: a store with no row identity evaluates through the seam's reference path and answers exactly what a full evaluation answers, losing only the saving. |
LiveTransformOptions.defaults is both of the above; LiveTransformOptions.identifiedBy "<column>" is
the one-line opt-in for a host that knows its key column.
The store is the session's. initSession mints one per call, reachable only from that session's own
render closure and the handle it returns — there is no registry and no shared default, so two sessions
cannot see each other's primed results. Drop the session and the store goes with it. It holds no
unmanaged resource, so it is not IDisposable; an explicit session end that wants the primed tables
released early calls store.Clear(), which is correctness-neutral. Not opting in at all is today's path
exactly: every render evaluates the pipeline in full.
Roadmap (Phase 152)
- ✅ Track A — the
TreeOp-emitting diff (Fuaran.UI.OpStream.Replay.TreeOpDiff). - Track B (in progress) — ✅ the
DomPatch/ClientEffectwire vocabularies (this package), ✅ theTreeOp → DomPatchlowering (renderer injected), ✅ the generic JS shim. Remaining: the granularSetText/SetAttrlowering follow-on (currently content ops re-render the node viaReplaceFragment— correct + targeted, just not field-level). - Track C (in progress) — ✅ the G1 inbound trust boundary (
Validation.fs— the non-negotiable default-deny gate: node-exists / event-legitimate-for-kind / payload-in-bounds / dispatch-policy-gated, mirroring the clientrunActiongate server-side), ✅ the per-connection driver (Driver.fs—LiveSession+step: validate → interpret →update→ re-view → diff → lower →DomPatch/ClientEffect, with the server-closure win + the server-executable/client-only split). Remaining: the explicit per-fieldBinding.Local/CommitLocalform-buffer protocol (the floor — client-buffered, server-sees-the-flush — already holds). - Track D ✅ — the
IFuaranLiveChanneltransport seam +Frame+InMemoryChannel+LiveConnection(Channel.fs), transport-agnostic reconnect replay (LiveConnection.Resync), the SSE frame wire encoding (FrameWire.fs), and both backends:Fuaran.UI.ServerDriven.AspNetCore(SSE+POST, the v1 default — verified end-to-end via the sample) andFuaran.UI.ServerDriven.WebSocket(the lower-latency drop-in; its structural identity to the SSE backend proves the seam is transport-neutral). - Track E ✅ —
fuaran-dotnet/docs/SERVER_DRIVEN.md(architecture + transport analysis + the per-arm tables) andsamples/server-driven(an SSR counter made live via the shim + the SSE backend, no client bundle).
No platform-SDK dependency appears anywhere here — the SSE framing is implemented here, not depended on.
Apache-2.0 licensed — see the repo LICENSE.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | 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
- Fable.Core (>= 5.0.0)
- FSharp.Core (>= 10.1.300)
- Fuaran.Core.Column (>= 0.21.0)
- Fuaran.Core.DataFrame (>= 0.21.0)
- Fuaran.Core.Function (>= 0.21.0)
- Fuaran.Core.Ops (>= 0.21.0)
- Fuaran.Core.OpStream (>= 0.21.0)
- Fuaran.Core.Tree (>= 0.21.0)
- Fuaran.Core.Validator (>= 0.21.0)
- Fuaran.Core.Wire (>= 0.21.0)
- Fuaran.UI (>= 0.80.0)
- Fuaran.UI.Ops (>= 0.80.0)
- Fuaran.UI.OpStream.Replay (>= 0.80.0)
- Fuaran.UI.Renderer.Core (>= 0.80.0)
NuGet packages (5)
Showing the top 5 NuGet packages that depend on Fuaran.UI.ServerDriven:
| Package | Downloads |
|---|---|
|
Fuaran.UI.ServerDriven.AspNetCore
Fuaran server-driven backend 1 (v1 default) — SSE-push + POST-receive over ASP.NET. The thin transport glue around the Fuaran.UI.ServerDriven core: an SSE IFuaranLiveChannel, the POST inbound parser, the connection registry, and the GET-stream / POST-event endpoint wiring (mapFuaranLive). Default because SSE+POST rides natively through proxies/CDNs/WAFs, gets per-event HTTP governance for free, and maps reconnect 1:1 onto the journal via Last-Event-ID. No platform-SDK dependency (the SSE framing is self-contained). Apache-2.0 licensed. |
|
|
Fuaran.UI.ServerDriven.WebSocket
Fuaran server-driven backend 2 — one bidirectional WebSocket channel over ASP.NET. The lower-latency transport for measured high-frequency interaction (per-keystroke, drag, live cursors). A first-class second backend whose structural identity to the SSE backend (differing only in channel + endpoint glue) is the architectural-integrity check that IFuaranLiveChannel is genuinely transport-neutral. No platform-SDK dependency. Apache-2.0 licensed. |
|
|
Fuaran.Program.Bounded
Fuaran.Program.Bounded — the bounded program interpreter: a total fold of the wire-representable action set over a program's state store, with the no-closure-invocation invariant that makes an emitted tree safe to run untrusted. Placement-neutral by construction (the same interpreter drives a server session and a browser client), plus the server placement's loop with its per-interaction resource budget. Apache-2.0 licensed. |
|
|
Fuaran.Program.Runtime
Fuaran.Program.Runtime — the client placement of the bounded program loop: run a wire-decoded program tree interactively in the browser with no hand-authored update function, no message type and no server. Shares the interpreter with the server placement rather than reimplementing it, so the same tree behaves identically at both. Renderer, effect performer, op sink and live channel are injected seams. Apache-2.0 licensed. |
|
|
Fuaran.Program.Server
Fuaran.Program.Server — the server-logic placement of the bounded program loop: run a named, host-registered handler as data behind a program tree's call action, with a closed server-effect vocabulary (query, op-apply, host call, patch, notify) behind a default-deny gate. Shares the interpreter, the resource budget and the binding re-resolution pass with the other placements rather than reimplementing them. Carries the codec for the handler declared form, the effect vocabulary and the outcome report, certified against their own conformance corpus. Apache-2.0 licensed. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.80.0 | 0 | 9/9/2026 |
| 0.79.0 | 5 | 9/9/2026 |
| 0.78.1 | 128 | 9/8/2026 |
| 0.78.0 | 61 | 9/7/2026 |
| 0.77.0 | 159 | 9/6/2026 |
| 0.76.0 | 66 | 9/6/2026 |
| 0.75.0 | 142 | 9/4/2026 |
| 0.71.0 | 58 | 9/4/2026 |
| 0.70.0 | 102 | 9/4/2026 |
| 0.46.0 | 116 | 8/28/2026 |
| 0.39.0 | 114 | 8/27/2026 |
| 0.35.0 | 227 | 8/24/2026 |
| 0.32.0 | 130 | 8/23/2026 |
| 0.31.0 | 136 | 8/21/2026 |
| 0.30.0 | 116 | 8/21/2026 |
| 0.29.0 | 123 | 8/19/2026 |
| 0.28.0 | 120 | 8/18/2026 |
| 0.27.0 | 114 | 8/18/2026 |
| 0.26.0 | 125 | 8/18/2026 |
| 0.18.0 | 117 | 8/10/2026 |