Fable.Yjs 1.0.0-beta0185

This is a prerelease version of Fable.Yjs.
There is a newer prerelease version of this package available.
See the version list below for details.
dotnet add package Fable.Yjs --version 1.0.0-beta0185
                    
NuGet\Install-Package Fable.Yjs -Version 1.0.0-beta0185
                    
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="Fable.Yjs" Version="1.0.0-beta0185" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Fable.Yjs" Version="1.0.0-beta0185" />
                    
Directory.Packages.props
<PackageReference Include="Fable.Yjs" />
                    
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 Fable.Yjs --version 1.0.0-beta0185
                    
#r "nuget: Fable.Yjs, 1.0.0-beta0185"
                    
#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 Fable.Yjs@1.0.0-beta0185
                    
#: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=Fable.Yjs&version=1.0.0-beta0185&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Fable.Yjs&version=1.0.0-beta0185&prerelease
                    
Install as a Cake Tool

Ylmish

.github/workflows/build.yml

Real-time, collaborative apps with a delightful programming model.

Here lie libraries for integrating Yjs and Elmish, via Fable and Adaptive.

😸 I like building apps with Elmish, I like sharing state with Yjs. Let's conjugate!

I want (select) changes to an Elmish model to propagate to a Yjs document, and changes to a Yjs document to reflect in the Elmish model.

Background

Data and communication

Our app data describes what is in memory while our app is running. Our state data describes what is persisted in browser storage and synchronized with peers.

Changes need to be communicated bi-directionally. That is, any changes made to state data, in Yjs, need to be made to app data, in Elmish, and (some) changes made to app data (such as through interactions with the app) need to be made to state data.

If we were to observe a running Elmish loop, we wouldn't see the operations being applied to the app's model. They're opaque to an observer because they're inside the update functions. Instead, we only have access to each successive 'model, that is, the consequence of operations.

For example, an Elmish 'model may contain a list and an interaction may add two items into that list, but from the outside, we only see the new list, not two 'add' operations.

If we observe a Yjs document, we'd see all of the operations that occur and those are shared with peers for synchronization.

For example, a Yjs Y.Array and an interaction may add two items to that array and from the outside, we can observe two add operations. (And we can combine all the operations to see the 'current' state of the array.)

We need to bridge these two worlds, that is, we need to be able to go from our changes represented as successive models to our changes represented as the operations themselves.

'model -> 'model -> 'operations
# which looks just like a classic differencing (diffing) function...
'document -> 'document -> 'delta

'Incremental computation' has already been used where people want to build apps that use immutable data structures but performantly update a mutable DOM. Part of this work has been to efficiently diff two models.

Design

We bridge Elmish and Yjs through an intermediate, incremental model using FSharp.Data.Adaptive (an fsharp implementation of incremental computing) and Adaptify (to incrementalize an existing Elmish model).

Successive Elmish models are used to update the incremental model. The (calculated) changes to the incremental model are observed and (the deltas) are applied to the Yjs document.

β”Œβ”€Elmish─┐         β”Œβ”€Adaptive─────────┐         β”Œβ”€Yjs───┐
β”‚        β”‚ --[1]-> β”‚                  β”‚ --[2]-> β”‚       β”‚
β”‚ Model  β”‚         β”‚ IncrementalModel β”‚         β”‚ Y.Doc β”‚
β”‚        β”‚ <-[4]-- β”‚                  | <-[3]-- β”‚       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”˜
App data                                    State data

Using Ylmish.Program.withYlmish:
[1] Successive Elmish models are used to update the incremental model.
[2] Changes to the incremental model are observed and (the deltas) are applied to the Yjs document.
[3] Changes to the Yjs document are observed and the deltas are applied to the incremental model
[4] Changes to the incremental model are observed and a updated Elmish model is set for each.

Schema and codec

Our app schema describes the structure of our app data, that is, what is in memory while our app is running. Our state schema describes the structure of our state data, that is, what is persisted in browser storage and synchronized with peers via the app's companion.

Our state schema must be decoupled from our app schema because:

  1. Our app schema will change over time as our app changes. The state schema must be protected from breaking changes to the app schema. So, we need an anti-corruption layer between the two schemas.
  2. Only a subset of app data needs to be persisted. So, we need to be able to select what should be persisted.

If we have this decoupling, we need an explicit description of how our app schema translates to our state schema and vice versa. We need to be able to encode that app data as state data and decode state data into app data.

Design

We provide Ylmish.Adaptive.Codec for writing encoders and decoders.

β”Œβ”€Adaptive─────────────────────────────────┐
β”‚ AdaptiveModel --[1]-> AMap<string, AVal> β”‚
β”‚                                          β”‚
β”‚ AVal<Model>   <-[2]-- AMap<string, AVal> |
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
App schema                      State schema

Using Ylmish.Adaptive.Codec:
[1] .Encoder
[2] .Decoder

TODO

  1. Investigate failing Ylmish.Adaptive.Codec.roundtrip updates test.

  2. Implement adaptive-to-Y attaching (syncing), but separate directions

    Right now Y.Text.attach sets up bi-directional sync. This should be separated into single directions so that encoding can work with different schemas to decoding which is important for when schemas change over time.

    1. Write a test for the motivator of this change, that is
      1. ensuring existing (state data) maps aren't overwritten
      2. ensuring new maps can be added from the app data Added to tests in Program.fs
    2. Find another way to handle the sentinel thingo so it doesn't get into a loop. (Ideally considering running multi-threaded in .NET, though minimally we could just assume single-threaded JS for now.)
  3. Implement the actual attaching of the adaptive model to the Y.Doc in Program.withYlmish so that the tests in Program.withYlmish pass.

  4. Ylmish.Adaptive.Codec.Decoders will need access to the Elmish model.

    The app data will have elements not persisted by state data. (For example, data that is only relevant to current interactions or the current session.) This app data needs to be retained through changes to state data. Therefore, when the developer writes a decoder they need access to the current Elmish model to express how app data and state data should be merged.

    Decoder<'Element, 'Result> is already a Reader monad so this might be accomplished by tupling the Elmish model into the 'Element env parameter and implementing an 'ask' function to the Decode.object builder

  5. We're using doc.getMap () to get a Y.Map in our Program.fs tests. I'm guessing that doesn't get us a 'root' map though and that subsequent calls don't give us the same map. (What does it give us?) We might need make the developer pass in a root map instead.

  6. Consider get-or-insert semantics for nested Y types so our maps and arrays aren't overwritten by two clients initializing shared types.

    We could code around this it by only using top-level maps and arrays, representing nested types by name. For example, Y.Doc.getMap('x.y.z') to represent a map z inside a map y inside the top-level map x. Maybe the [key-value type] will support this?(https://github.com/yjs/yjs/issues/255).

  7. IndexList in FSharp.Data.Adaptive starts at 1 is probably why the Delta tests are failing

  8. Elmish has different versions for Fable and .NET. We need to use the right one.

    https://github.com/elmish/elmish#using-elmish

  9. Investigate supporting Ycs or [Yrs](https://github.com/y-crdt/y-crdt (with a FFI binding) (in addition to Yjs (./src/Fable.Yjs)).

  10. Some app data might need to be resolved with additional network requests.

    For example, the author ID might be persisted in the state data, but the author name and display image might be kept by a different service.

    It might be such a common case we should provide hooks in decoding to resolve this data. In the meantime, this could be left up to the user to do in the Elmish layer.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  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 was computed.  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 was computed.  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. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Fable.Yjs:

Package Downloads
Ylmish

Real-time, collaborative apps with a delightful programming model. Integrates Yjs and Elmish via Fable and FSharp.Data.Adaptive.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0-beta0233 525 9/26/2026
1.0.0-beta0231 542 9/23/2026
1.0.0-beta0227 228 9/22/2026
1.0.0-beta0224 45 9/22/2026
1.0.0-beta0222 5,114 8/3/2026
1.0.0-beta0219 529 7/17/2026
1.0.0-beta0218 174 7/12/2026
1.0.0-beta0190 72 7/4/2026
1.0.0-beta0185 77 7/3/2026
1.0.0-beta0183 77 7/3/2026
1.0.0-beta0177 78 6/7/2026