Fuaran.UI.ServerDriven
0.86.0
Prefix Reserved
dotnet add package Fuaran.UI.ServerDriven --version 0.86.0
NuGet\Install-Package Fuaran.UI.ServerDriven -Version 0.86.0
<PackageReference Include="Fuaran.UI.ServerDriven" Version="0.86.0" />
<PackageVersion Include="Fuaran.UI.ServerDriven" Version="0.86.0" />
<PackageReference Include="Fuaran.UI.ServerDriven" />
paket add Fuaran.UI.ServerDriven --version 0.86.0
#r "nuget: Fuaran.UI.ServerDriven, 0.86.0"
#:package Fuaran.UI.ServerDriven@0.86.0
#addin nuget:?package=Fuaran.UI.ServerDriven&version=0.86.0
#tool nuget:?package=Fuaran.UI.ServerDriven&version=0.86.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.32.0)
- Fuaran.Core.DataFrame (>= 0.34.0)
- Fuaran.Core.Function (>= 0.32.0)
- Fuaran.Core.Ops (>= 0.32.0)
- Fuaran.Core.OpStream (>= 0.32.0)
- Fuaran.Core.Propagation (>= 0.32.0)
- Fuaran.Core.Tree (>= 0.32.0)
- Fuaran.Core.Validator (>= 0.32.0)
- Fuaran.Core.Wire (>= 0.32.0)
- Fuaran.UI (>= 0.86.0)
- Fuaran.UI.Ops (>= 0.86.0)
- Fuaran.UI.OpStream.Replay (>= 0.86.0)
- Fuaran.UI.Renderer.Core (>= 0.86.0)
NuGet packages (4)
Showing the top 4 NuGet packages that depend on Fuaran.UI.ServerDriven:
| Package | Downloads |
|---|---|
|
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.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.Program.UI
Fuaran.Program.UI — the UI adapter for the domain-generic bounded program core: the UI witness (the UI tier's action union, node tree, binding store, tree-ops and client effects seen through the core's witness contract), the pre-0.6.0 names as aliases over it, and the UI transport loop (the bounded driver, its channel glue and the client runtime). Fable-clean. Released in the same version as the core it instantiates. Apache-2.0 licensed. |
|
|
Fuaran.Program.Server.UI
Fuaran.Program.Server.UI — the UI adapter for the server placement of the domain-generic bounded program core: the pre-0.6.0 server names as aliases over the UI witness, and the UI event step (the validation gate in front of the core's dispatch) for the server session, the durable interpreter and the operator controls. .NET only. Released in the same version as the core it instantiates. Apache-2.0 licensed. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.86.0 | 43 | 9/28/2026 |
| 0.85.0 | 155 | 9/20/2026 |
| 0.84.0 | 125 | 9/17/2026 |
| 0.83.0 | 111 | 9/15/2026 |
| 0.81.0 | 110 | 9/12/2026 |
| 0.80.0 | 174 | 9/9/2026 |
| 0.79.0 | 253 | 9/9/2026 |
| 0.78.1 | 283 | 9/8/2026 |
| 0.78.0 | 124 | 9/7/2026 |
| 0.77.0 | 216 | 9/6/2026 |
| 0.76.0 | 121 | 9/6/2026 |
| 0.75.0 | 203 | 9/4/2026 |
| 0.71.0 | 113 | 9/4/2026 |
| 0.70.0 | 163 | 9/4/2026 |
| 0.46.0 | 130 | 8/28/2026 |
| 0.39.0 | 130 | 8/27/2026 |
| 0.35.0 | 244 | 8/24/2026 |
| 0.32.0 | 139 | 8/23/2026 |
| 0.31.0 | 150 | 8/21/2026 |
| 0.30.0 | 128 | 8/21/2026 |