Appearance
Changelog
All notable changes to ActualLab.Fusion are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
+HexNumber after version number is the commit hash of this version. It isn't included into the NuGet package version.
To track updates in real time, see "Fusion/🎉Releases" on Voxt.ai.
14.3.13+4d9c859d5 | npm: 14.3.5
Release date: 2026-08-07
RpcMethodAttribute.DelayAction couldn't be set on the attribute it belongs to — a nullable enum isn't a legal attribute argument type, so every [RpcMethod(DelayAction = ...)] failed to compile. Take this one only if you want to configure delayed-call handling per method; nothing else changes. NuGet-only release; the npm packages are unchanged and stay at 14.3.5.
Breaking Changes
RpcMethodAttribute.DelayActionis nowRpcDelayedCallAction, notRpcDelayedCallAction?. Only code that reads the property could compile against the old type — a customRpcOutboundCallOptions.DelayHandler, in practice. ReplacemethodDef.Attribute?.DelayAction ?? fallbackwith(methodDef.Attribute?.DelayAction ?? RpcDelayedCallAction.Default).Or(fallback).
Added
RpcDelayedCallAction.DefaultandRpcDelayedCallActionExt.Or(...)— the "use the default" sentinel and its resolver. A customDelayHandlercan useOrto fold the sentinel into whatever action it considers the default for that method.
Fixed
[RpcMethod(DelayAction = ...)]compiles. The property was declaredRpcDelayedCallAction?, and C# rejects a nullable enum as an attribute argument: "'DelayAction' is not a valid named attribute argument because it is not a valid attribute parameter type" (CS0655). So the only way to reach the value was to read it from a customDelayHandler— which is why the property shipped unusable and stayed that way. It's non-nullable now, defaulting to theRpcDelayedCallAction.Defaultsentinel and resolved throughOr(...), mirroring howRpcLocalExecutionMode.Defaultalready works. The sentinel gets its own bit rather than reusingNone, which keeps its meaning: no log, no abort, no resend.
14.3.11+3fd57618f | npm: 14.3.5
Release date: 2026-08-06
Every idle HTTP/2 RPC connection was dropped by Kestrel about five seconds into each quiet period, and on HTTP/2 that abort takes the whole connection with it — including other peers' RPC streams a reverse proxy multiplexed onto the same backend connection. Take this one if you use the HTTP transport (AddHttpServer() / AddHttpClient()); the WebSocket transport was never affected. NuGet-only release; the npm packages are unchanged and stay at 14.3.5.
Fixed
An idle HTTP/2 RPC connection is no longer aborted by Kestrel's
MinRequestBodyDataRate. An RPC connection holds its request body open for the connection's whole lifetime and writes into it only when there are outbound messages, so an idle peer sends a ~40-byte keep-alive every 10 seconds — roughly 4 B/s. Kestrel's slowloris guard defaults to 240 B/s past a 5-second grace period, so the request was aborted a few seconds into every quiet period with "Reading the request body timed out due to data arriving too slowly". The client saw only the far end of it:HttpProtocolException: net_http_http2_stream_error, INTERNAL_ERROR, 2, delivered seconds later by whatever proxy sat in between.Light traffic is the trigger rather than an innocent detail, which makes the failure look unrelated to load: a busy connection stays up, and the same connection dies once the app goes quiet. On HTTP/2 the abort is connection-level, so a single idle peer also disconnects every other RPC stream sharing that backend connection — the visible symptom is unrelated clients dropping within a second of each other.
RpcHttpServer.Invokenow clearsIHttpMinRequestBodyDataRateFeatureper request, the same way it already clearsMaxRequestBodySize; the per-request opt-out is enough on HTTP/2, so no host-wideKestrelServerLimitschange is needed.
14.3.8+f51cf2ebd | npm: 14.3.5
Release date: 2026-08-06
A pooled DbContext could return to EF's pool in a state that makes it unusable for writes, and every command that later rented that pool entry failed until the pool dropped it. Anyone running Fusion on EF Core with pooled contexts — the default — should take this one. NuGet-only release; the npm packages are unchanged and stay at 14.3.5.
Fixed
A
DbContextreturned to EF's pool with a stale connection open count no longer breaks the commands that later rent it. EF's pool-return path —RelationalConnection.ResetStateAsyncwithdisposeDbConnection: false— closes the connection but never resets its open count, and EF 11 dropped that reset from the synchronous twin as well. A context returned while its connection was still counted as open, which is what a query cancelled mid-flight leaves behind, re-enters the pool permanently "in use":SetDbConnectionrejects it with "The instance of DbConnection is currently in use", so every command that later rents it as its operation-scope context fails until the pool drops it.The symptom reads as unrelated to whatever failed: a burst of cancellations poisons a few pool entries, and those entries then break arbitrary commands on that host, in pairs, minutes apart. Reads are unaffected — only the enrollment
SetDbConnectionperforms trips the check.DbOperationScope.InitializeDbContextnow disposes the rented context's own connection before adopting the scope's one. That costs nothing:SetDbConnectionalready ran the same disposal, just after the check that rejects the context.A
DbContextrented for an operation scope is disposed if enrolling it into the scope throws. A cancellation between renting and enrolling used to leak the context along with its pool slot.
14.3.4+3d2bbffed | npm: 14.3.5
Release date: 2026-08-05
LZ4 frame compression arrives as a set of serialization format variants. Every binary format gains a -lz4 variant (compresses server → client) and a -lz4f one (both directions), so msgpack6c-lz4, mempack6-lz4f and — once RpcNerdbankSerializationFormat.Register() runs — nmsgpack6c-lz4 and friends. Compression is chosen by the format key rather than negotiated, so the handshake is untouched and the two peers cannot disagree. Nothing changes unless you opt in: the default format is still what it was, and an uncompressed direction writes exactly the frames it wrote before. Both targets were published; the npm packages carry no runtime changes this release.
Added
- LZ4 frame compression for the frame-based RPC transports (WebSocket, pipe, stream). A frame is one batch of messages, so a batch compresses as a unit and the LZ4 dictionary carries across frames. A client's outbound traffic is mostly small call headers, where compression costs more than it saves — which is why the one-directional
-lz4is the variant to reach for first. Text formats get no compressed variants: a compressed frame is binary, and a text peer reads text frames. ActualLab.CompressioninActualLab.Core—ByteCompressor/ByteDecompressorand theirLZ4ByteCompressor/LZ4ByteDecompressorimplementations, plus theK4os.Compression.LZ4dependency. The codecs are RPC-agnostic and usable on their own.RpcCompressionFormat,RpcCompressionOptionsandRpcCompressionModeinActualLab.Rpc. The format names the codec pair and carries the policy its compressor follows: the minimum frame size worth compressing, and how many frames or bytes a compression context may span before reset. Register your own format to compress with different settings.ByteCompressor.GetMaxCompressedLength/GetMaxSourceLengthare part of the codec contract, the latter inverting the former by binary search. The sender's payload budget shrinks by the worst-case expansion the codec reports, so an encoded frame still fits the limit the peer enforces.LZ4ByteCompressorcomputes that bound from its own block count in long arithmetic, since a small block over a 1 GB frame overflowsint.
Block geometry. The default block size is 16 KiB, picked by measurement. Block size does not bound the match window — that's LZ4's own 64 KiB one, and it spans blocks — so a smaller block costs flush points rather than dictionary reach; with 1 KiB blocks a repeated 40 KB payload still compresses ~67×. What it does change is how often a frame splits, and since a frame is a batch capped at FrameSize (12,000 by default), the ratio plateaus at 16 KiB: 16K, 32K and 64.5K produce byte-identical output there. Only oversized frames improve past it, and slightly (0.47% → 0.42% on the sample). MaxBlockSize is 64512 — the largest block K4os will actually use whose worst case still fits the ushort length prefix — and caps anyone wanting a bigger block via a custom RpcCompressionFormat.
Frame header. The two flags ride in the top bits of the frame's existing int32 length word rather than in a byte of their own, so a stored frame costs nothing and copies nothing. This caps a frame at RpcFrameCodec.MaxFrameSize (~1 GB), now enforced by RpcFrameBasedTransport on construction — far above the 16 MiB default. Over WebSocket, where the message length already supplies the frame length, only the header's most significant byte goes on the wire.
Resetting the compression context periodically bounds both the memory a connection pins and how far a BREACH-style probe can correlate across frames. It does not make compressing a secret alongside attacker-controlled data within one frame safe.
Changed
- The TodoApp sample's client now defaults to
msgpack6c-lz4instead ofjson5np. This is the sample's own setting — the library's default format is unchanged.
Documentation
docs/PartR-Serialization.mddocuments the compressed variants — the full format tables, how direction selection works without negotiation, and how to register a format with custom compression settings.
Infrastructure
- The build script names the solution explicitly for
restore,buildandpack. The repository root holds bothActualLab.Fusion.slnandActualLab.Fusion.CI.slnf; with no project argumentdotnetresolved the target itself and ignored-p:UseMultitargeting=true, sorestorewrote single-TFM assets whilepackbuilt all nine against them, failing the publish withNETSDK1005. - TypeScript lints with
noImplicitAnyoff, matching ActualChat. That flag disables TS's evolving-any analysis, so a barelet x;assigned inside atrystaysanyand trips theno-unsafe-*rules there. The ESLint rule sets were already identical between the two repos; only the tsconfig differed, letting code pass lint here and fail after a sync.
14.2.50+657b54d72 | npm: 14.2.23
Release date: 2026-07-31
A crash fix for .NET 11 on Apple platforms. MemberInfoExt.GetGetter and GetSetter handed out open-instance delegates bound straight to a property accessor; when that accessor is virtual or comes from an interface, the CoreCLR interpreter mis-locates the receiver and faults. Apple targets have no JIT, so the interpreter is the only fallback there — and the fault presents as a 0x8BADF00D watchdog kill with the UI already painted and touches dead, because the main thread ends up stuck in CoreCLR's own crash logger. Upgrade if you ship .NET 11 on iOS, tvOS, or Mac Catalyst with the interpreter enabled. NuGet-only release (npm stays at 14.2.23).
Fixed
MemberInfoExt.GetGetter/GetSetterno longer bind a delegate directly to a virtual or interface accessor on the affected .NET 11 Apple configurations. They now fall through to the emitted-codegen path that was already sitting right below, which yields a delegate over aDynamicMethodand takes a different, unaffected interpreter path. Works around dotnet/runtime#130840. Every other target keeps the direct-delegate fast path untouched, and below .NET 11 the check compiles away entirely.
Added
MemberInfoExt.MustAvoidOpenVirtualDelegatescontrols that workaround. It defaults totrueonly where the bug actually bites — .NET 11 exactly, with dynamic code supported (a proxy for "the interpreter is enabled"), on iOS, tvOS, or Mac Catalyst — and self-disables on .NET 12, where the runtime-side fix is expected. It's settable so the behavior can be exercised on unaffected platforms; changing it doesn't invalidate already-cached delegates. net11.0+ only.
Infrastructure
- Runtime async stays off, deliberately. .NET 11's
runtime-asyncis now wired up behind aUseRuntimeAsyncbuild property but disabled by default, so the shipped assemblies are unchanged. Fusion publishes a singlenet11.0asset that every consumer resolves, and Blazor WebAssembly is still Mono — which cannot execute runtime-async IL at all.
14.2.47+27e20cb61 | npm: 14.2.23
Release date: 2026-07-30
A startup-cost release aimed at mobile. Runtime codegen shows up on mobile startup traces: every ActivatorExt cache miss emits a DynamicMethod on Android, and on iOS — where dynamic code isn't available at all — builds an interpreted expression tree instead. This release lets an app pre-register those constructor delegates, and removes one codegen path outright. Nothing changes for apps that don't opt in; existing APIs behave as before. NuGet-only release (npm stays at 14.2.23).
Added
ActivatorExt.RegisterConstructorDelegate(arities 0–5) seeds the constructor delegate caches ahead of time. A registered constructor then costs one dictionary lookup and no reflection or codegen. The overloads are generic withwhere TResult : class, so the compiler enforces the exact-declaring-type requirement that callers' covariant casts depend on —TResulthas to be the constructor's own declaring type, not a base of it.RuntimeCodegen.OnCreateDelegatereports what still isn't registered. It fires on everyActivatorExtandMemberInfoExtdelegate cache miss — exactly the places a pre-registered delegate would have avoided codegen — so you can run the app with it set and use the callback to build the registration list.
Performance
- The non-generic
ArgumentListTypepath no longer generates code. It now builds its factory fromArgumentList.SimpleFactoriesinstead of a generated constructor delegate. The generic path is unchanged.
14.2.45+5e14b57f3 | npm: 14.2.23
Release date: 2026-07-30
Two trimming fixes, both of which matter only if you publish a fully trimmed app — a Blazor WebAssembly client with PublishTrimmed, or NativeAOT. The first one unblocks the publish itself; the second fixes a crash that was hiding behind it. Untrimmed apps are unaffected. NuGet-only release (npm stays at 14.2.23).
Fixed
- ILLink overflowed its stack on
Requirement<T>, so trimmed publishes couldn't finish.Requirement<T>and its subclasses annotatedTwith[DynamicallyAccessedMembers(All)], and requirement targets are routinely self-referential —class Account { public static Requirement<Account> MustExist; }. WalkingAccount's own fields re-required the annotation onAccount, and on .NET 11 the linker recursed until it died:"dotnet.exe" exited with code -1073741571(0xC00000FD, stack overflow), surfacing asNETSDK1144: Optimizing assemblies for size failed. The annotation is gone fromRequirement<T>;RequireExt.Require<T>still carries it, soT's members are preserved on the path that actually needs them. If you resolveRequirement<T>.MustExistwithout going throughRequire<T>, root that type yourself. - Blazor components with a custom parameter comparer threw at render time under full trimming.
ParameterComparerProvidercreates comparers reflectively, but comparer types are named only insidetypeof()— in[ParameterComparer(...)]or inKnownComparerTypes. Trimming kept the types and dropped their unused parameterless constructors, soActivatorExt.CreateInstancegot a null constructor delegate and threwNullReferenceExceptionfromFusionComponentBase.SetParametersAsync.ParameterComparerAttribute.ComparerTypeis now annotated, which roots user-declared comparers at their usage sites, and a new module initializer keeps the built-in non-generic ones. Generic comparers closed over your own types —ByIdParameterComparer<TId>and friends — are still rooted by the attribute that names them. This one was only reachable once the fix above let a trimmed publish complete at all.
14.2.41+82146381f | npm: 14.2.23
Release date: 2026-07-30
A single RPC fix, and only for the text serialization formats (json5, json5np). If you're on a binary format — mempack6c is the default — nothing here affects you. NuGet-only release (npm stays at 14.2.23).
Fixed
- Every text-format frame carried a phantom empty message. The frame-based transport reserves 4 bytes for its length prefix before the first message is written, so the write buffer never starts at offset 0 — and the text serializer used exactly that ("is the buffer empty?") to decide whether a
LF+RSdelimiter was still needed. It always thought one was, so every frame went out with a delimiter in front of its first message. The receiver dutifully treated the leading delimiter as a message, handed a lone"\n"toUtf8JsonReaderand loggedExpectedJsonTokens LineNumber: 1 | BytePositionInLine: 0— once per frame, on both peers. It then recovered and parsed the real message, so nothing was lost; the cost was a stream of spurious errors in the log and 2 wasted bytes per frame. A reader on this version also tolerates a writer on an older one, so a mixed-version pair is quiet in both directions. ArgumentDatapicked up a stray newline. The reader treated the position ofRSas the message's end, which includes theLFthe delimiter itself starts with, so every message but the last in a frame got a"\n"appended to its argument data. Harmless with JSON arguments — it's trailing whitespace there — but the payload wasn't byte-exact.
14.2.39+3b5885367 | npm: 14.2.23
Release date: 2026-07-30
.NET 11 support. Every package gains a net11.0 asset, and nothing else moves: no target framework was dropped and no third-party dependency range changed on any existing one, verified by diffing the packed .nuspec files against 14.2.34. If you stay on your current .NET, upgrading to this release is a no-op — the only new thing is a target framework you aren't using yet. NuGet-only release (npm stays at 14.2.23).
Added
net11.0target framework across the board, alongsidenet10.0down tonetstandard2.0. Its assets depend on the .NET 11 preview packages (11.0.0-preview.6.26359.118for ASP.NET Core,Microsoft.Extensions.*and EF Core where applicable), so treatnet11.0as preview-grade until .NET 11 ships.Npgsql.EntityFrameworkCore.PostgreSQL 11.0.0-preview.6backsActualLab.Fusion.EntityFramework.Npgsqlthere.EnableDotNet11build switch (TargetFrameworks.propsin the repository root). It'strueby default, which makesnet11.0the primary target for the whole repository;-p:EnableDotNet11=falseputs everything back onnet10.0. Relevant only when building Fusion from source, not to package consumers.
Changed
ActualLab.Fusion.EntityFrameworkno longer pins EF Core onnet10.0/net11.0— it declares an open[10.0.0,)range, so the app picks EF Core 10 or 11. The package is provider-agnostic, so it has no reason to decide for you. Provider packages can't offer the same freedom:Npgsql,Sqlite,SqlServerandPomeloeach cap EF Core to a single major version, so whichever provider you reference decides your EF Core version.
Infrastructure
- Fixed the
packbuild target consuming aproject.assets.jsonleft behind by a separaterestoretarget. Inside the single-process publish chain that could be a single-target-framework restore, which failed every inner build withNETSDK1005("Assets file doesn't have a target for 'net5.0'").packnow runs its own restore, so the assets can't disagree with the target frameworks being built.
14.2.34+ae17db7ca | npm: 14.2.23
Release date: 2026-07-29
The outcome of a second full security and severe-bug review of the library: 120 findings, of which both CRITICALs and all 35 HIGHs are now closed. Almost everything here is a fix on the path from a wire message to a resolved type, an allocation, or an authorization decision — treat this as a security release and read the breaking changes before upgrading. The TypeScript client gets its own round of fixes and one breaking change, so npm moves to 14.2.23 alongside NuGet.
Breaking Changes
The backend-service / backend-command gate actually works now.
RpcMethodDefcompared the command's interface against"ActualLab.CommandR.IBackendCommand", butIBackendCommandlives inActualLab.CommandR.Commands— so the ordinal comparison never matched andIsBackendcollapsed to the service-level flag alone. The marker had been decorative in every release since v10.3. An app that was (unknowingly) invoking anIBackendCommandfrom a non-backend peer will now be rejected. Migration: audit which of your commands carryIBackendCommandand route those calls through a backend peer — or drop the marker if the command was never meant to be backend-only.IKeyValueStoreis anIBackendService. It was registered as an ordinary frontend RPC service, so with the gate above dead itsSet/Removewere reachable from any peer — a shipped cross-user data read and write. Migration: client-side code must useISandboxedKeyValueStore, which is what the session- and user-scoped key constraints exist for.njson5andnjson5npare denied to clients by default. The server had no format allow-list at all — its only gate was "is this key registered" — so a client could pin?f=njson5and run Json.NET withTypeNameHandling.Autoand no binder. Client-selectable formats are now gated on all three endpoints. Migration:RpcSerializationFormatResolver.DefaultClientDeniedFormatKeys = ImmutableHashSet<string>.Empty;at startup restores the old behavior.Session options moved from
ImmutableOptionSettoPropertyBag.ImmutableOptionSetstores its values asNewtonsoftJsonSerialized<object>, so deserializing one runs Json.NET withTypeNameHandling.Autoon wire data — reproduced through the defaultmempack6andmsgpack6round-trips, because the binary formatter only ever sees strings and Newtonsoft runs inside the type's own property.SessionInfo.Options,AuthBackend_SetSessionOptions.Options,AuthBackend_SetupSession.OptionsandDbSessionInfo.Optionsare nowPropertyBag, which carries the type out of band. Member ordinals are unchanged, but the payload at the slot is not:mempack6tolerates empty options in both directions,msgpack6does not even when empty, and populated options are incompatible in every format. Legacy_Sessions.OptionsJsonrows read back as empty options — accepted, contained silent data loss. Migration: change yourImmutableOptionSetvariables toPropertyBag; its type-keyedSet<T>(value)/Get<T>()becomeKeylessSet<T>(value)/KeylessGet<T>(), and string-keyed access isSet(key, value)/Get<T>(key). Upgrade clients and servers together if you usemsgpack6.OptionSetandImmutableOptionSetare[Obsolete]. Migration: useMutablePropertyBagandPropertyBag.Every RPC size ceiling is explicit, enforced both ways, and much lower. A single pre-handshake WebSocket message could pin ~136 MB (allocated as 256 MiB) per connection, before any authentication; sizes previously fell out of whatever the buffer happened to grow to. Header and method-ref limits go 64 KiB → 1 KiB, the frame limit 33,489,152 → 16,711,680, argument data 16 MiB → 15.5 MiB, and the worst-case text envelope 12,261,961 → 244,297. An over-limit header, method reference or payload is rejected on read and the message is dropped without an error reply, so the remote sees the call as never answered. Nothing in Fusion comes near these limits — the longest real method reference measured anywhere is 59 bytes. Migration: none expected; if you genuinely move payloads above 15.5 MiB, raise
RpcByteMessageSerializer.Defaults.MaxArgumentDataSizeand its text counterpart, and see Size Limits for what else must move with it.ApiMap.EmptyandApiSet.Emptyare removed. Both are mutable collections, so a shared static instance was a corruption hazard (User.ToClientSideUser()was writing into the process-global one); turning it into a fresh-instance property only madeEmptylie in the other direction. Migration: writenew().Session.ToString()is redacted to{4-char Id prefix}:{Hash}. It used to return the raw Id — a bearer credential — so anything that formatted aSessionleaked it. Migration: usesession.Idwhere the actual value is needed; nothing about the wire representation changed.TypeScript:
resolveStreamRefsis replaced bytoRpcStream, and stream references nested in ordinary results are no longer converted automatically. The old inference replaced any string of 4–6 comma-separated parts whose first three parsed as integers with a liveRpcStream, so"1,2,3,4", a CSV row or a coordinate tuple all became streams. TypeScript has no typed method definitions, so the heuristic is unfixable in principle and the wire format can't change. Migration:toRpcStream(value, peer)at the call site; it returnsnullwhen the value isn't a stream reference, and unlike the old inference it also handles the object shape the binary formats use.The plugin cache moved and changed format.
FileSystemPluginFindercached discovered plugins underPath.GetTempPath()— a path any local user could pre-create — and deserialized it withTypeNameHandling.Autoand no binder, which is local code execution out of the box. The cache now lives in a per-user location (Environment.SpecialFolder.LocalApplicationData→ActualLab/…; a group- or other-writable directory is refused on Unix), round-trips through System.Text.Json with no$type, and re-validates every cached type against the exact predicate scanning applies. Migration: none — the cache key version was bumped, so old entries are simply ignored. Override the location viaFileSystemPluginFinder.Options.CacheDirif you relied on the old one.INotLoggedis gone. It suppressed a command's contents everywhere it could be logged, which cost four near-identical branches at the log sites and threw away the whole command even when one member was sensitive. A command that carries a credential now redacts itself instead, so the log still says which command failed and what else was in it.CompletionProducer.Options.IgnoreNotLoggedgoes with it — it existed only to opt back out of the suppression. Migration: implementISanitizedand overrideToString()/PrintMemberson the command, masking the members that need it — seeAuthBackend_SetupSession. The interface is gone, so every declaration that named it stops compiling — but the obvious fix is to delete the marker, and that alone turns full logging back on for a command that was marked precisely because it carries something sensitive. Redact the command before you drop the marker, not after.ApiMap<TKey, TValue>.EmptyandApiSet<T>.Emptyare gone. Both werestatic readonlyinstances of types that derive fromDictionary<,>andHashSet<>— mutable, shared process-wide.User.ToClientSideUser()masked a user's identities by starting fromApiMap<UserIdentity, string>.Emptyand callingTryAddon it, so the one shared map accumulated every masked user's identity schemas and every maskedUserpointed at it: user A's client-side user listed user B's identity schemas, and those are serialized to the client. Concurrent request threads mutating thatDictionaryunsynchronized could also corrupt its buckets, and it is enumerated on everyUserserialization. Returning a fresh instance from a property would have fixed the sharing while leaving the shape that invites it — andDictionary/HashSetmutators are non-virtual, so a genuinely frozen shared instance can't be enforced through the base-class entry points on every target framework. The members were removed outright instead.Emptywas a static member, not a wire member, so the serialized shape of both types is unchanged. Migration:ApiMap<TKey, TValue>.Empty→new ApiMap<TKey, TValue>(),ApiSet<T>.Empty→new ApiSet<T>(). Every use site is a compile error, so nothing changes silently.ApiArray,ApiOption,ApiNullableandPropertyBagare immutable structs andApiListhas noEmpty, so these two types were the only ones affected.RedirectUrlCheckeris nowRedirectUrlHelper. The delegate could only accept or reject, so a rejectedreturnUrlwas silently replaced with"/"— and sinceIUrlHelper.IsLocalUrlrejects every absolute URL, including a same-origin one addressing a page of this very app, that quietly broke the sign-in and sign-out popups and the render-mode switcher. The replacement is a DI-registered class whoseNormalizereduces an accepted URL to a relative one rather than discarding it, withAllowedHostsandMustStripHostas init-only options. Migration: replace a customRedirectUrlCheckerregistration with aRedirectUrlHelpersubclass overridingCheck;FusionWebServerBuilder.DefaultRedirectUrlCheckerFactoryis nowDefaultRedirectUrlHelperFactory. An absolutereturnUrlthat used to end up at"/"now lands on its own path.Session.ToString()andUser.ToString()no longer print their secrets.Sessionrenders a 4-char prefix plus a hash;Userprints its claim names and identity schemas instead of their values —User.Identitiesmaps an identity to that provider's secret, and a wholeUsertravels insideAuthBackend_SignIn. Serialization is untouched, so nothing on the wire or in the database changes. Migration: none, unless you parsed eitherToString(). Wrap a diagnostic inSanitization.Suspend()to get the full contents back.
Added
- A sanitization framework (
ActualLab.Compliance), for keeping secrets out of logs without keeping them off the wire.SanitizedString<TSanitizer>is a drop-in for astringmember that masks itself when rendered while serializing byte-identically,ISanitizedmarks a type whoseToString()does the same, andSanitizerssupplies the policies (Hidden,LengthHint,PrefixAndLengthHint,Fingerprint,SessionString,UriQuery,RpcRequestQuery). Masking is off until a scope turns it on, andAddSanitizingLoggerFactory()is what opens one around each log call — so secrets are masked in the log and nowhere else. See Sanitization. - RPC request queries are sanitized by an allow-list.
RpcQuerySanitizeris replaced bySanitizers.RpcRequestQuery, which is deny-by-default: a parameter is readable only if listed, so one added later can't start leaking a credential because nobody remembered to mask it. Session parameters render asSession.ToString()does, everything else as a prefix and length bucket. - Reconnect proof of possession (.NET and TypeScript). The connect URL's
clientIdused to be the whole peer-selection key, and the incumbent connection was disconnected before anything was verified — so anyone who read aclientIdout of a proxy log, browser history or aRefererchain could keep the victim permanently offline, or inherit its server-side peer state. The server now mints a per-peer CSPRNG secret and ships it in a newRpcHandshake.Secretmember, over the established connection; subsequent connects carryc(a monotonic counter) andp(Base64Url_NoPad(HMAC_SHA256(secret, clientId + "\n" + c))). Verification runs before the peer lookup, before the socket is accepted and before any disconnect, so a failed proof is a bare 403 with the incumbent untouched, and all three server endpoints share one policy function.RpcWebSocketServerOptions.RequireReconnectProofdefaults tofalse, yet the gate still protects by default: a peer that has proven once may not downgrade to a no-proof reconnect. SeeRequireReconnectProof. RpcWebSocketServerOptions.OriginValidatorplusRpcWebSocketServerOriginValidators.AllowAll/SameOrigin/Allow(origins). The WebSocket upgrade is exempt from CORS and from preflight, so a CORS policy never protected the RPC endpoint against cross-site WebSocket hijacking of a cookie-bound session. The default staysAllowAllso nothing breaks on upgrade;WarnOnUnvalidatedOrigin(defaulttrue) logs one startup warning when nothing validates theOrigin, from both the ASP.NET Core and the OWIN server.RpcLimits.CallCountLimitandRpcLimits.ObjectCountLimit— per-peer backstops on inbound+outbound calls and on shared+remote objects, checked once perObjectReleasePeriodand resetting the peer when exceeded. The call cap defaults toint.MaxValueon purpose (a Fusion server legitimately holds one inbound call per live subscription); the object cap defaults to 64K.NoWaitcalls are invisible to the cap.Session.Sha256Hash— a strong, collision-resistant digest of the session id, cached as 32 bytes.Session.Hashkeeps its legacy XxHash3 value because it's on the wire viaSessionAuthInfo.SessionHashandAuth_SignOut.KickUserSessionHash.PruningCache<TKey, TValue>— a capacity-bounded cache with lock-free reads that amortizes its size check through aStochasticCounter, so the hot path stays a plainConcurrentDictionaryread. It now bounds bothTypeRefcaches and the legacy method-resolver cache.Symbol,Moment,ByteString,FilePathandTypeRefcan be used as System.Text.Json dictionary keys — STJ routes non-string keys throughReadAsPropertyName/WriteAsPropertyName, whose defaults throw.VersionSetfiltering extensions, and clamped-capacityGetMemory/GetSpan/EnsureCapacityoverloads onRefArrayPoolBuffer<T>, mirroringArrayPoolBuffer<T>.- TypeScript:
hub.systemCallSender.errorFilterwith a ready-madegenericErrorFilter. Handler exception messages were forwarded to the remote peer verbatim, and Node error text routinely embeds absolute paths and connection strings. The default stays pass-through, so nothing changes unless you opt in.
Fixed
- Sign-in, sign-out and the render-mode switch all redirected to the app's home page.
fusionAuth.jsbuilt itsreturnUrlwithnew URL(..., document.baseURI).hrefandRenderModeHelperpassedNavigationManager.Uri, both absolute — and the server's redirect check rejects absolute URLs, so all three silently fell back to"/". The visible symptom differed per flow becauseServerAuthHelpergates them differently:AllowSignInisAllowAnywhere, so sign-in worked and only its popup failed to close, whileAllowChangeandAllowSignOutrequire the/fusion/closerequest that never arrived — so sign-out cleared the ASP.NET cookie while leaving the Fusion session authenticated, and "sign out everywhere" then churned sessions asIsSignOutForcedinvalidated each oneSessionMiddlewareminted. Both callers now send relative URLs, andRedirectUrlHelperrecovers an absolute one anyway. - Publication ordering on lock-free read paths.
Computed.TrySetOutputstored theConsistentflag before_output, so a reader that sawConsistentcould read a default output and returnnullfrom a compute method that had succeeded — a store-store ordering error x64 doesn't mask.LazySlim(all three arities),DbHub,RedisTaskSubandRpcConfiguration.Freezehad broken double-checked locking: a lock orders nothing on its own, becauseMonitor.Exitfences before it exits, leaving the constructor's writes and the reference store mutually unordered.RandomInt32/64Generatorread their shared buffer after releasing the lock, so two concurrent callers could get the same value from a generator whose point is cryptographic randomness. RpcPeer.SetConnectionStatereleased its lock twice. The "state is already final" early return sat inside thetry, so it exited the lock and then ran thefinally, which performed the whole transition — publishing the transport, marking states, cancelling the old reader token source — with no lock held on a path where nothing had transitioned, then exited the lock again.- Wire-driven type resolution and its caches. Type markers were cached under a
ByteStringkey that aliased the pooled transport buffer, so the next frame overwrote it — the entry became permanently unreachable and the correct marker permanently missed, growing under ordinary polymorphic traffic rather than only under attack.TypeRef.ResolveCachewas unbounded and keyed by the raw wire spelling, whichType.GetTypeaccepts in many variants, so one type could occupy many permanent entries; only the canonical spelling is cached now and failures never are.ExceptionInforesolved the remote type before correlating$sys.Errorwith an outbound call, so an uncorrelated error still reachedType.GetType; the error is correlated first, and type names get structural pre-checks before any assembly probing. - Polymorphic reading is no longer reachable through a widened slot type.
$sys.Breplaced the declared argument typeT[]withobjectwheneverTItemwas polymorphic, purely to reach the polymorphic serializer — but the declared type is also the bound the wire-supplied type name is checked against, so widening it removed the check.IsPolymorphicnow recurses into array element types, so the slot stays exactlyT[]. - The legacy method-resolver cache is bounded. It was a process-wide, unbounded
ConcurrentDictionarykeyed byRpcHandshake.RemoteApiVersionSet— taken verbatim from the first message a remote peer sends, pre-authentication. The key is normalized to the scopes the registry knows (so all garbage collapses to one entry), the handshake caps the set at 16 scopes and 512 chars, and the cache is bounded byPruningCache.VersionSet.HashCodealso swaps its XOR fold for an additive one, since XOR is linear over GF(2)^32 and any 33+ hashes contain a subset that XORs to zero. - Session ids and command payloads are no longer logged unredacted on every connection and every command failure, and
INotLoggedis honored. - A duplicate inbound call id no longer leaks a linked
CancellationTokenSource(plus its registration on the long-lived peer-change token). A duplicate id carrying a different method reference or call type is now rejected as a protocol violation instead of silently resolving to the registered call; the same leak on the sibling path, whereDeserializeArgumentsreturns null after registration, is fixed with it. ApiArrayNerdbankConverterandPropertyBagNerdbankConverterno longer preallocate from the wire-declared count. A 4 MB payload declaring 4M items went from 32 MB to ~40 KB (ApiArray) and 64 MB to ~73 KB (PropertyBag).RetryPolicy.Applyobserves itsCancellationTokenand backs off onSuperTransienterrors instead of retrying with no delay — together an uncancellable 100% CPU spin, reproduced at 1.9M iterations in 5 s.Auth_SignOuton an unknown session id no longer inserts a session row plus an operation-log row, and unregistered shard tags no longer become permanent per-shard cache keys — both let unauthenticated input create persistent state.User.ToClientSideUser()no longer mutates a process-globalApiMap, andInMemoryAuthService's intermediateGetUserSessionsoverload is now a[ComputeMethod]— it is on no interface, so it was never intercepted and every invalidation of it did nothing, leavingIAuth.GetUserSessionssubscribers with a stale session list indefinitely.- The default invalid-session handler no longer loops. It signed out and redirected to the same URL without clearing the rejected Fusion cookie, so every redirected request presented the same cookie and got another redirect; the unconditional
SignOutAsync()also threw when no default sign-out scheme was configured, turning the same cookie into a persistent 500. The cookie is now rewritten before the handler can short-circuit, and aReloadGuardCookieNamecookie breaks the loop after one attempt. - RestEase no longer buffers an unbounded 500-response body before parsing it.
ActivatorExt.CreateInstanceworks for value types — it threw for every struct, because the constructor delegate's return type was cast toFunc<..., object>, a conversion delegate return types only allow for reference types.ByteSpanExt.ToHexStringhad a dead#if NET5_0_OR_GREATER1, which silently pinned every build to the fallback branch — which also emitted a different casing.
Fixed (TypeScript)
Computedlifetime, in both directions. An outbound compute call stayed inpeer.outboundCallsafter$sys.Okand its reaction pinned theComputedand its result from a GC root, so a long-lived SPA retained one live call, computed and full result per distinct(method, args)tuple ever queried; 300 discarded calls went fromoutboundCalls=300, collected=0tooutboundCalls=1, collected=299, and the server dropped its 300 retained computeds with them. Conversely, an inbound call now owns itsComputedstrongly for as long as it owes the caller an invalidation. A lateFinalizationRegistrycallback no longer deletes a live replacement computed, and dependency edges are pruned on dispose — 200 mount/dispose cycles left 200 dependants, 199 of them dead; now 0.$sys.Disconnectlooked ids up insharedObjectsas well asremoteObjects, which is the wrong namespace — and both counters start at 1, so collisions were the norm: a client that both consumed a server stream and uploaded one of its own had its outgoing stream aborted the moment the server tore down an unrelated incoming one.- The remote stream receiver enforces the acknowledgement window it advertises (overrun fails the stream with
$sys.AckEnd),$sys.Ackis coalesced at receipt instead of accumulating in an array drained withshift(), and a hugenextIndexcan no longer stall a stream permanently. The$sys.Reconnectstale-generation check no longer skips a non-numeric handshake index. - The binary receive path is hardened.
argDataLenwas read signed and used directly aspos + argDataLen, so a negative value moved the cursor backwards and a 7-byte envelope could reportbytesRead = 1— the frame was then re-parsed at overlapping offsets, yielding more messages than it contained, each allocating an inbound call and emitting a$sys.Errorreply. Lengths are read unsigned and validated as in-bounds safe integers before any slice. - One bad message no longer discards every message already decoded from the same frame.
.NETbatches many RPC messages per frame, and the peer'strycovered deserialization but not dispatch, the text branch had notry/catchat all, and the binary branch'scatchsat outside the per-message loop — so$sys.Okreplies were dropped and, since per-call timeouts default to unbounded, those calls hung forever. Errors are now contained per message. - Inbound compute arguments are truncated to the declared arity, which closes compute-cache poisoning.
14.1.78+f65f331ed | npm: 14.1.5
Release date: 2026-07-27
A NuGet-only release (npm stays at 14.1.5) that repairs MessagePack code generation for everyone who puts a PropertyBag in a MessagePack contract. If you're on 14.1.71 or 14.1.73, upgrade.
Fixed
- MessagePack's source generator no longer dies on the
PropertyBagfamily. MessagePack 3.1.8 crashes its own generator and analyzer with anIndexOutOfRangeExceptionwhenever a[MessagePackObject]type has a member whose type is a closed generic struct defined in a referenced assembly — generic classes and non-generic structs are fine.TypeSchemagavePropertyBag,PropertyBagItemandTypeDecoratingUniSerializedexactly that shape in 14.1.71, so since then any assembly with aPropertyBagin a MessagePack contract silently lost its entire generated resolver — the generator "will not contribute to the output" — which breaks trimming and NativeAOT. The three types now carry[MessagePackFormatter]with hand-written formatters that reproduce the previously generated wire format byte for byte, the same thingOption<T>,Result<T>,ApiArray<T>andBox<T>already do.MutablePropertyBagneeded nothing — it's a class, and only broke through itsPropertyBagItem[]member. - The NativeAOT roots added in 14.1.73 are now complete.
KeepSerializable<T>keeps the type and its serializers but not its MessagePack formatter, soTypeDecoratingUniSerialized's formatter was still missing, as was theArrayFormatterboth bags need for theirPropertyBagItem[]contents.
14.1.73+02df3329a | npm: 14.1.5
Release date: 2026-07-27
A small NuGet-only release (npm stays at 14.1.5) that fixes trimming / NativeAOT retention for the types Fusion always serializes. Worth taking if you publish trimmed or AOT-compiled apps.
Fixed
Unit,PropertyBag,MutablePropertyBagand their serializers are now retained byActualLab.Coreitself.DefaultMessagePackResolverbuilds its formatters viaCreateInstance/MakeGenericType, so nothing referenced their constructors and full trimming dropped them — every app had to keepUnitMessagePackFormatterfrom its own startup code. Core has a module initializer of its own now, matching the RPC / CommandR / Fusion ones.- The
PropertyBagfamily's generated MessagePack formatters are rooted explicitly. They were non-generic beforeTypeSchema, so the generated resolver looked them up statically; since 14.1.71 they are closed viaMakeGenericType, which ILC can't follow.
14.1.71+a733921f | npm: 14.1.5
Release date: 2026-07-27
A NuGet-only release (npm stays at 14.1.5). It adds TypeSchema — a way to constrain which types a PropertyBag is allowed to materialize, which matters for bags filled from remote input — plus a per-method consolidation comparer, and it turns a silently ignored ConsolidationDelay into a startup error.
Breaking Changes
TypeDecoratingUniSerialized<T>and the four single-formatTypeDecorating*Serialized<T>wrappers now take<TSchema, T>. Open generics can't be aliased, so these call sites need updating —TypeDecoratingUniSerialized<TypeSchema.Any, T>preserves today's behavior.PropertyBag,MutablePropertyBagandPropertyBagItemare spelled as before via global using aliases.ConsolidationDelayon an RPC-exposed compute method of aRpcServiceMode.Distributedservice now throws at registration time instead of being silently ignored. Such calls are served byRemoteComputeMethodFunction, which always produces a plainComputeMethodComputed— so no consolidation ever happened, even when routing resolved to the local peer. Move the consolidation to a non-RPC-visible (e.g.protected virtual) compute method and have the public one derive from it.Local,Server,ClientandServerAndClientservices are unaffected.
Added
TypeSchema(ActualLab.Serialization) — a phantom type parameter threaded throughPropertyBag,MutablePropertyBag,PropertyBagItemand the type-decorating serialized wrappers. It supplies the type filter the type-decorating serializers already accept, so a disallowed type is rejected right afterTypeRef.Resolve()and never materializes;PropertyBagItemalso checks on write, so a bad value fails where it's added.TypeSchema.Anykeeps today's behavior,TypeSchema.PrimitiveOnlyallows the primitives plusstring,Guid, the date/time types,MomentandSymbol. The wire format is unchanged —PropertyBag<TypeSchema.Any>serializes byte-identically to the previousPropertyBag.ComputeMethodAttribute.ConsolidationComparer— theIEqualityComparer<T>type used to compare two consecutive outputs while consolidating, whereTis the method's unwrapped return type. Result types with referential equality (or collections whoseEqualsis referential) could never consolidate before, since the default comparer bottoms out inEquals(x.Value, y.Value). The comparer is validated and instantiated once per type at startup, and errors keep the existing error-aware comparison.
Fixed
- Consolidation sources no longer share a
FusionMonitorcategory with the targets that absorb them.ComputeMethodDefbuilds both from the same type andMethodInfo, so a churning source was conflated with its quiet target; the source now reports~<FullName>, mirroring the*prefixRemoteComputeMethodFunctionuses for remote replicas.
14.1.62+ab9673b6 | npm: 14.1.5
Release date: 2026-07-26
Another NuGet-only release (npm stays at 14.1.5). Its centerpiece is a fix for an HTTP RPC deadlock that could stall a client indefinitely: every connection now gets its own HttpClient. It also folds in the unreleased 14.1.53 packaging work, which makes each package render a README on nuget.org.
Breaking Changes
RpcHttpClient'sprotected HttpClient HttpClientproperty is replaced byprotected virtual HttpClient CreateHttpClient(), called once per connection. Subclasses that read or override the property should override the method instead.Options.HttpClientFactoryis now invoked per connection rather than once per client. A custom factory that returns a shared or cachedHttpClientwill reintroduce the deadlock described below — return a fresh instance.
Added
IHasRetryDelay(ActualLab.Resilience) — an error implementing it extends its cachedComputed's auto-invalidation delay to at leastRetryDelay, so a retry can't be issued sooner than the error asks for. Useful for errors caused by request volume, where retrying early makes the underlying condition worse.
Fixed
- HTTP RPC connections no longer deadlock when two of them are open to the same host. An RPC connection holds its request open for its whole lifetime, and
SocketsHttpHandlercan't carry two such requests over one HTTP/2 connection — the second never receives its response headers. SinceRpcHttpClientcached a singleHttpClient, this hit every reconnect (the new connection overlaps the old one's teardown) and every client with both a client and a backend peer, leaving the peer retrying forever. Each connection now creates and disposes its ownHttpClient, and closing a connection resets its HTTP/2 stream instead of leaving it half-open.
Infrastructure
- Every NuGet package now ships a README rendered on nuget.org, along with a corrected project URL, an embedded icon, and cleaned-up descriptions.
- The test runner aborts a hung test group instead of letting it wedge the whole run.
Documentation
- Reworked the docs-site home hero cards and the MCP page intro.
- Added
bool/Tasknaming and member-ordering rules toCODING_STYLE.md.
14.1.47+493d3cc7 | npm: 14.1.5
Release date: 2026-07-21
This is a NuGet-only release (npm stays at 14.1.5) that overhauls Fusion's OpenTelemetry story: RPC call and connection metrics, database log pipeline metrics, invalidation pass and persistent remote cache monitoring, plus a set of distributed-tracing fixes — see the updated Part "OpenTelemetry" for the full instrument reference.
Breaking Changes
- Per-method RPC instruments (
rpc.server.{Service}/{Method}.*) are gone; RPC call metrics are now a fixed set of instruments carrying the boundedrpc.methodattribute, andrpc.server.durationis renamed torpc.server.call.duration. Use metric views to droprpc.methodwhere per-method breakdown isn't needed. - Live-count instruments (e.g.
rpc.{transport}.transport.count) areObservableGauge-s now instead ofObservableCounter-s — update dashboards that treated them as monotonic sums.
Added
- RPC call instruments:
rpc.client.call.duration(logical call duration including reroutes),rpc.client.reroute.count, open-call gaugesrpc.server.call.open/rpc.client.call.open(by call stage), andrpc.client.call.event.countfor delayed / resend / timeout observations. - RPC connection instruments:
rpc.client.connection.attempt.count,rpc.client.connection.attempt.duration, and client/serverrpc.*.connection.uptimehistograms, tagged withrpc.connection.kindandoutcome. - Database log pipeline instruments (
ActualLab.Fusion.EntityFramework):db.event_log.processing.delay,db.log.batch.size, anddb.log.batch.duration. - Command and operation retry instruments (
operation.retry.*) in CommandR / Fusion operations. - Safe invalidation pass monitoring:
invalidation.pass.durationandinvalidation.pass.command.count; partial invalidation failures mark the enclosing span as an error. - Persistent remote cache instruments:
remote_computed.cache.request.count,remote_computed.cache.lookup.duration, andremote_computed.cache.stale_value.count. RpcDiagnosticsOptions.MustPropagateAmbientActivityContext— lets perf-sensitive hosts skip theActivity.Currentlookup on the outbound call path.
Fixed
- Distributed trace context is preserved across RPC calls, and call traces are renewed after rerouting.
- RPC spans are aligned with OpenTelemetry semantic conventions, and response serialization is traced.
- Entity Framework spans use the proper EF activity source.
- Blazor circuits suppress only connection-scoped activities, so deliberate ambient activities (e.g. .NET 10 Blazor circuit spans) are preserved; the suppression predicate is virtual for custom policies.
- Database log batch outcome distinguishes cancellation from error, and retry instruments skip tag construction when disabled.
- Inbound call trace completion tolerates incomplete result tasks.
Performance
- Call-path diagnostics overhead is nearly eliminated when telemetry is off: no per-outbound-call trace allocation, and the inbound call lock is skipped when there is no trace to complete.
- Enabled-instrument checks and staged RPC call counts are cached.
Infrastructure
- The test suite runs in parallel groups with an exclusive phase for time-sensitive tests: the full default run went from ~38 to ~14 minutes.
14.1.3+79939c2a | npm: 14.1.5
Release date: 2026-07-21
This release makes RPC peer refs stable: there is now one RpcRef per logical target (shard, host, "default"), cached forever and safe to return from routers with no factory ceremony. A topology change no longer mints a new ref — the ref's route (a new per-generation RpcRoute) is reset instead, while peers stay 1:1 with route generations, so the entire battle-tested reroute pipeline is preserved.
npm 14.1.5 is the TS side of the same release (published from 0abe15b7).
Breaking Changes
- TS (
@actuallab/rpc):RpcPeerRefBuilderis renamed toRpcRefBuilder. TS refs stay plain strings — there's noRpcRoutecounterpart on the TS side. RpcPeerRefis renamed toRpcRef, andRpcRouteStateis replaced byRpcRoute— see the "RpcPeerRef: renamed to RpcRef in v14.1" migration note. Key points:- Custom refs override
CreateRoute()to mint a route per generation instead of being re-created on topology changes; ref caches collapse to a plainConcurrentDictionary.GetOrAdd.RpcRef.Routere-mints lazily when the current route is marked as changed;RpcRoute.NewStatic()denotes refs that never reroute. RpcRouteStateExtis merged intoRpcRoute;IsChanged/WhenChangedare properties now, and per-generation target data (host id, endpoint) belongs onRpcRoutesubclasses.RpcPeeris constructed fromRpcRoute(peer.Ref==peer.Route.Ref); the pipeline readspeer.Route— the generation the peer is bound to.RpcHub.GetPeer(RpcRoute)is the primary overload (uses the exact generation);GetPeer(RpcRef)resolves the current one;RpcHub.Peersis keyed by route.- Delegate signature changes:
RpcPeerOptions.PeerFactoryisFunc<RpcHub, RpcRoute, RpcPeer>,ConnectionKindDetectorisFunc<RpcRoute, RpcPeerConnectionKind>; server-side delegate renames:RpcWebSocketServerPeerRefFactory→RpcWebSocketServerRefFactory,RpcHttpServerPeerRefFactory→RpcHttpServerRefFactory,PeerRefFactoryproperties →RefFactory. RpcPeerStateMonitoris constructed from(RpcHub, RpcRef?)and transparently restarts across reroutes.
- Custom refs override
- The operation log processing delay metric is renamed from
db.operation.log.processing.delaytodb.operation_log.processing.delay(OTel naming conventions: "operation log" is a single snake_case component). The Prometheus-flattened name is unchanged.
Changed
- Rerouted peers are now removed from
RpcHub.Peerswith zero delay once drained — the 5-minute removal delay applies only to terminally-failed client peers. - A burst of topology churn with no interleaved calls coalesces into a single route re-resolution — something the previous ref-per-version model couldn't do.
- Peer-bound logging now renders the route generation (
<address> [vN->target], cached), so overlapping generations are distinguishable in logs;RpcRoute.GetTargetString()supplies the target. FusionEntityFrameworkInstrumentsnow follows the shared instruments pattern (addsActivitySource); the TodoApp Aspire sample registers theActualLab.Fusion.EntityFrameworkmeter, so the operation log delay histogram reaches the dashboard.
Tests
- New
RpcRefRouteTestandMeshStableRefTestsuites: route re-mint and churn coalescing,Reset(),GetPeerreplacement and races, and local↔remote shard flips with computed invalidation on a stable ref.
14.0.37+1d67c8de | npm: 14.0.17
Release date: 2026-07-20
This release focuses on cross-host invalidation latency: the operation log reader now reports its processing delays (metric + rate-limited warnings), the out-of-order-commit path reacts to notifications instead of polling them out, and both the Npgsql and Redis log watchers coalesce their change notifications.
Breaking Changes
RpcWebSocketServerOptions.ConfigureWebSocketis now anRpcWebSocketServerAcceptContextFactoryreceiving(server, context, peerRef)instead of a plainFunc<WebSocketAcceptContext>, so the accept context can vary per connection (e.g. to enable WebSocket compression selectively based on the request or peer ref). The OWIN/.NET Framework server gains the same hook.Migration: if you assign a custom
ConfigureWebSocketdelegate, update it to the new signature; the default behavior is unchanged.
Added
- Operation log processing delay reporting: every remote operation applied by the log reader records a
db.operation.log.processing.delayhistogram (tagged with shard and the processing path: batch / gap / reprocess), and delays aboveProcessingDelayWarningThreshold(1 s by default) produce rate-limited warnings naming that path — enough to tell out-of-order-commit gap polling from lost-notification check-period fallbacks. TaskCoalescerinActualLab.Core: coalesces concurrent runs of a task factory — at most one run in flight plus one queued behind it, so any burst of requests is served by at most two runs.
Performance
- Out-of-order commits now invalidate near-instantly: a notification-triggered wake-up of the operation log reader forces young pending gaps to be re-checked immediately, collapsing the former up-to-~1 s gap-poll delay to one notification round-trip plus one query.
- Npgsql and Redis log watchers coalesce change notifications via
TaskCoalescer: a burst of N commits costs at most two NOTIFY/PUBLISH round-trips instead of N, and the Npgsql watcher no longer serializes sends behind a per-shard lock.
Tests
- PostgreSql-backed tests randomly split between the Redis and Npgsql operation log watchers, so both notification transports get coverage.
Documentation
- Performance doc refresh: benchmark numbers updated to 14.0.17, external grpc_bench cross-check, layout cleanups.
14.0.17+ddd1df1b | npm: 14.0.17
Release date: 2026-07-16
Major release. Three things land together: a proxy/interception overhaul that replaces the per-interceptor dispatch path with compile-time method slots (breaking — see below), a hot-path performance campaign across the proxy, compute, locking, and RPC layers, and a systematic correctness audit of the entire .NET codebase (Core, Fusion, RPC, Interception + generators, CommandR, Blazor, and the persistence/EF/Redis/Npgsql supporting libraries) that fixed a large batch of edge-case and boundary defects.
Breaking Changes
Proxy method slots + array-based interceptor dispatch. Generated proxies now assign a compile-time integer slot to every intercepted method and cache the resolved handler per proxy instance, per slot — a warm call is a field load + delegate invoke, with no dictionary probe and no virtual
SelectHandlerdispatch. This changes severalActualLab.Interceptioncontracts:IProxy.Interceptoris replaced byIProxy.MethodTable(staticProxyMethodTable) +IProxy.Binding(InterceptorBinding).Invocationnow carries(MethodTable, MethodIndex);Methodresolves via the table. The legacyMethodInfo-basedInvocationconstructor is removed.- New public types
ProxyMethodTable,ProxyMethodRef, andInterceptorBinding;InterceptorExt/InvocationExtsurfaces changed.
Migration: rebuild — the source generator emits the new proxy shape, so a clean rebuild regenerates all proxies. Only code that constructs
Invocationby hand or readsIProxy.Interceptordirectly needs source changes.
Performance
- Compute method leaf invalidation, empty-computed invalidation, and registry-slot carry-through
- Cached compute-input lookup, initial computed registration, and completed-task result adaptation
AsyncLockSet: atomic entry lifecycle, uncontended-release fast path, and fewer redundant locks during invalidation cascades- Inlining hints on proxy dispatch, compute lookup, and the RPC/Fusion hot paths
VarUIntencode/decode fast paths (VarUInt32/VarUInt64)- WebSocket receive transfers use tuple-assigned metadata
- Skip inactive RPC inbound/outbound traces, redundant simple-channel disposal, and redundant envelope-encoding scans
Added
- Invalidation handler logging helper exposed, kept in its handler set
- Typed delegate invocation-list helper added to Core
- Fusion redirect-checker defaults are now factory-based
Changed
- Use
127.0.0.1instead oflocalhostin service connection strings - Complete builder wiring for pre-registered implementations
- Read JSON property names case-insensitively in
SystemJsonSerializer
Fixed
RPC
- Frame buffer corruption on transport send failures
- Always notify the client when a response send fails
- Enqueue completion and stream flow-control correctness; handshake/WebSocket boundary hardening and clearer handshake-mismatch errors
- Backend validation and NetFx WebSocket ownership
- Keep the TS stream reset flag across drained ack batches
Interception + generators
- Proxy generator identity, nesting, and incremental retention
- Reject unsupported proxy signatures and deduplicate diamond methods
- Prevent
ArgumentListstruct invokers from crashing the process
Fusion
- State, synchronization, and invalidation contracts; state notifications, dependency snapshots, and Blazor metadata; Blazor lifecycle and render-point contracts
- Secure redirects and session-binding boundaries; web and key-value service boundary defects
- Lifecycle, monitoring, and session edge cases; cache, pruner, comparer, and session-tag defects
Core
- Encoding, reflection, framing, and sampling bugs; sharding, numeric, random, and conversion boundaries
- Collection, timer, and convenience API defects; file, activation, collection, and hash-ring edge cases; disposal, channel, and pooled-buffer boundaries
CommandR
Persistence + supporting libraries
- Persistence boundaries, watcher, and shard-factory lifecycles; Nerdbank alignment and plugin lifecycle correctness
- EF save guards and Npgsql SQL generation; Redis isolation and atomic sequence resets
- Honor REST query formats and preserve plugin cleanup failures
Infrastructure
- BenchmarkDotNet performance runner with RPC argument-codec, WebSocket-transfer, and VarUInt benchmarks
- Test deflaking and faster test hosts: cut web-host shutdown grace from 3s to 50ms, deflake Redis streamer and just-disconnected peer state tests
13.0.163+7e1e746a | npm: 13.0.167
Release date: 2026-07-15
TypeScript-only follow-up to the 13.0.163 hardening release. Promotes two primitives that until now lived only in ActualChat's synced copy into the shared Fusion TS packages so both repos stay in lockstep, and fixes the TS typecheck script to read current source instead of a stale build.
npm-only release. There are no .NET framework source changes, so the NuGet packages were not republished — the latest on nuget.org remains 13.0.163. The 13.0.167 version reflects the npm package and the Nerdbank git-height version of this commit.
Added (TypeScript)
AsyncSignal(actuallab-core) — auto-reset, edge-triggered async wakeupRpcStreamSender.minRttMs(actuallab-rpc) — windowed minimum of send→ack round-trip times
Fixed (Tooling)
- Typecheck against source, not stale
dist— a dedicated src-onlytsconfig.typecheck.jsonclears 14 phantomtsc -berrors - Make
decorators.tstype-safe under strict no-unsafe-* lint (fn.toString())
13.0.163+ab89147d | npm: 13.0.163
Release date: 2026-07-15
TypeScript-port hardening release. This is the payload of a full audit of the TS port against C# Fusion (contracts + robustness): 70+ findings across Core, RPC, the compute/state kernel, and the React bindings were triaged and fixed, bringing the TS runtime to behavioral parity with .NET on invalidation, cancellation, reconnect, and error propagation. The npm package advances to 13.0.163 to match NuGet. No .NET framework source changes.
Fixed (TypeScript)
Core (actuallab-core)
- Abortable
delayAsync,retry,RetryDelayer.getDelay, andAsyncLockwaiters - Robustness fixes for TS-port audit items C1, C2, C4, C7, C9
RPC (actuallab-rpc)
addClientextends the shared proxy instead of dropping methods (F8)- RPC lifecycle hardening — R10, R11, R12, R17, D4
- Harden
RpcPeerhandshake/reconnect/dedup (R5, R8, R9, R13, R16) - Align the RPC wire layer with .NET V5/
ExceptionInfocontracts - Stream lifecycle + tracker identity invariants (R3, R4, R21, R22)
- Correct dispatch receiver, decorator metadata isolation, and wire arity
Fusion compute/state kernel (actuallab-fusion)
ComputedState.valuerethrows the stored error instead of masking it (S2)- Reconnect reporting, client-proxy dedup, server-peer auto-dispose (F6, F8, F11)
- Settle/retry pre-result invalidations, propagate cancellation (F2, F3, F4, F5)
- State + UI robustness parity (S8, S9, S10, S17, S18)
- Thread compute
AsyncContext+ ALS backing, capture rework, reentrancy guard (K3, K11, K14) - Server-side invalidation parity (F1, F9, F7, F10)
- Versioned
whenUpdated+ prompt dispose + abort-safe delayers (S11, S5, S14, S4) ComputedOptions+ cancellation caching, keying, renewer (K5, K6, K13, K15)- State-layer C# parity for
MutableState/ComputedState(S1, S13, S3, S16, S15) - Kernel invalidation-path robustness (K4, K17, K12, K10, K9)
- Kernel graph/registration parity (K1, K2, K7, K8, K16)
React bindings (actuallab-fusion-react)
Fixed (Samples & tooling)
- Update the TodoApp TS UI for the current Fusion TS-port APIs
- Make ESLint deterministic from a clean checkout; fix tooling globs
Documentation
- Updated TS-port docs for the fix-campaign behavior/API changes
- Fixed the Server-Side Usage sample — compose compute methods via a
@computeMethodclass - Added the TypeScript port gap audit (contracts & robustness vs C# Fusion) and its per-item decisions under
docs/plans.
13.0.126+c30df2eb | npm: 13.0.25
Release date: 2026-07-14
Maintenance release: dependency floor bumps and build fixes; no framework source changes. The npm package is unchanged at v13.0.25.
Changed
- Bumped crucial dependency floors: MessagePack
[3.1.8,)(was 3.1.6) and StackExchange.Redis[3.0.17,)(was 2.9.32 — a major-version bump that affectsActualLab.Redisconsumers). - Pinned Roslyn (
Microsoft.CodeAnalysis.CSharp) to the[4.3.0,)lower boundary and decoupled it from the C# runtime-binder version. A source generator only loads in a Roslyn host at least as new as the one it was compiled against, so this keepsActualLab.Generatorsloadable under Unity's older build stack.Microsoft.CodeAnalysis.Analyzersheld at 3.3.4 to match. - Pinned the test SDK/runner (
Microsoft.NET.Test.Sdk,xunit.runner.visualstudio) to their low floors — used only by Fusion's own tests; newer majors caused issues.
Fixed
- Repaired the
-p:UseMultitargeting=truebuild.System.Memorywas pinned to 4.6.0, but theMicrosoft.Extensions.Logging.Abstractionsfloor now resolves to 10.0.5 (viaILogger.Moq), which requiresSystem.Memory >= 4.6.3→ NU1605 downgrade error on the netstandard2.0/net472/net48 target frameworks. Bumped to 4.6.3. Also guarded a test's fakeWebSocketMemory<byte>overrides for those older TFMs.
13.0.101+4292afe9 | npm: 13.0.25
Release date: 2026-07-14
.NET-focused release; the npm package is unchanged at v13.0.25. The bulk of this release is a distributed-invalidation audit and the correctness wave it produced: Fusion's compute-call sharing, IState.Invalidated semantics, the operation-log reader, and operation-completion listeners were all hardened against races, duplicate delivery, and silent loss under load and reconnects.
Breaking Changes
- Compute services that also expose command handlers must now be registered as singletons.
AddComputeServicerejects a scoped/transient registration of such a service at DI-build time instead of silently breaking invalidation replay (the handler was resolved from the root provider, faulting every completion). Migration: register these services as singletons. Scoped/transient compute services without command handlers (e.g. UI-scoped services) are unaffected. DatabaseFacadeExt.DisableAutoTransactionsAndSavepoints()(in the...Internalnamespace) was renamed toDisableAutoTransactions(bool allowSavepoints = true). CallDisableAutoTransactions(allowSavepoints: false)for the old behavior.
Added
NonTransientErrorInvalidationDelayis now a settable[ComputeMethod]option, letting you tune how long a non-transient error result is cached before auto-invalidation on a per-method basis (documented alongside the other compute method options).
Changed
- Error auto-invalidation is now routed by error transiency: Terminal errors use
AutoInvalidationDelay, while NonTransient errors use the minimum of the relevant delays. A throwingTransiencyResolveris treated as transient. - Reworked the operation-log gap-set cadence: due-gated queries, no reader starvation, and horizon expiry applied to all pending entries.
MutableStateis documented as an exception toNonTransientErrorInvalidationDelay.
Fixed
IState.Invalidatednow fires exactly once per generation (no duplicate or skipped invalidation events across generations).- Serve-stale no longer leaves a predecessor's
SynchronizedSourceuncompleted, and the cache-update path no longer double-binds the shared RPC call; the hand-off marker was fixed so successor invalidation still cleans up the shared RPC call. - Fixed a
KeyConflictStrategyrace on_eventsinserts (actual-chat#4049), and restored flush-and-retry recovery for version-checked event updates while avoiding unnecessary event-conflict flushes. - Hardened the operation-log reader: gap pending-set tracking, bounded retry with a corrected failed-entry retry cadence, and a coverage-loss sweep so entries can't be silently dropped.
- Operation-completion listeners are now reliable under failure: synchronous listener throws are routed through the external-terminal path, an external completion-command failure is terminal (propagated and unmarked), and at-least-once delivery is documented and asserted.
- Fixed a misleading discard log on the operations-log reprocess path and enriched
OperationCompletionNotifierassertion-failure logs with context. - Fixed the multitargeted build by disambiguating
FirstOrDefaultAsyncinDbOperationScope.
Documentation
- Added a distributed-invalidation audit report and course-of-action plan.
- Docs site moved to Cloudflare Pages with extensionless canonical URLs and IndexNow submission on deploy; improved documentation search indexing; fixed homepage hydration mismatches and compressed GIF assets.
- Linked the live sample demos (incl. Board Games, TownHall) from the docs and README.
Tests
- Added a must-not-throw test harness for operation-completion listeners, plus operation-log reader gap/budget tests and reprocessor tests (Uuid preservation across retries, no-retry-after-commit).
Infrastructure
- Dependency bumps: ActualLab.Core → 13.0.12, MessagePack → 3.1.6, AwesomeAssertions → 9.4.0, and several dev/CI tool updates.
13.0.28+5c8f5ae1 | npm: 13.0.25
Release date: 2026-07-06
.NET-only release; the npm package is unchanged at v13.0.25. Fixes MessagePack serialization code being trimmed away in Native AOT / fully trimmed apps, and introduces feature switches to control which serializers CodeKeeper preserves.
Added
Serialization feature switches (both on by default), mirroring
ArgumentList.AllowGenerics:MemoryPackByteSerializer.IsEnabledMessagePackByteSerializer.IsEnabled
An app that uses only one serializer can disable the other via a trimmed
RuntimeHostConfigurationOptionto drop its keep-code from the published binary. See the new "Feature Switches" section in the Native AOT and Trimming doc.
Fixed
CodeKeeper.KeepSerializable<T>()now preserves MessagePack serialization code in addition to MemoryPack. Previously, apps whose RPC uses MessagePack formats (e.g.msgpack6c) had that code trimmed under full trimming, so types with runtime-resolved formatters (ApiMap<,>,ImmutableArray<T>, ...) failed to deserialize with"Cannot deserialize inbound call arguments".
Tests
- New RPC keep-alive test suites in both .NET (
RpcKeepAliveTest) and TS (rpc-keep-alive.test.ts): connections stay up while keep-alives flow and are dropped when they stop, including half-open link and reconnect scenarios.
13.0.12+ed8b32fc | npm: 13.0.25
Release date: 2026-05-27
TypeScript-only release. .NET package unchanged from v13.0.12. Fixes the TS RPC client's handshake parsing on the text (json5np) transport when talking to a .NET server, and adds cross-casing E2E coverage for $sys.* payloads.
Fixed
- TS RPC: the client now reads camelCase
$sys.Handshakefields, not just PascalCase. A .NET server serializesRpcHandshakethroughSystem.Text.JsonwithJsonSerializerDefaults.Web, so over the text transport the wire keys areindex/remoteHubId— which previously parsed asundefined. The fallout (text transport only):_remoteHandshakeIndexdefaulted to0, so every$sys.Reconnectwas rejected by the server ("own handshake index N != 0") and forced a resend-all; and peer-change detection (keyed offRemoteHubId) never fired across server restarts. The MessagePack transport was unaffected (positional array). The parse now accepts the array, PascalCase, and camelCase shapes.
Tests
- New JSON casing E2E suite (
rpc-handshake-casing.test.ts): drives the real clientrun()loop against a mock .NET-style server that emits$sys.*payloads in camelCase or PascalCase, asserting the client parses the handshakeIndex(echoed back in$sys.Reconnect),RemoteHubId(peer-change detection), and$sys.Errorinfo identically in both casings. Confirms the audit finding that$sys.Handshakewas the only affected type —$sys.Error/$sys.Endalready read both casings, and all other$sys.*arguments are positional or stage-keyed.
13.0.12+63e3d65b | npm: 13.0.20
Release date: 2026-05-27
TypeScript-only follow-up. .NET package unchanged from v13.0.12 — fixes a Fusion-on-TS race exposed by the new reconnection-matrix tests, and ports those tests across all three layers (.NET unit, TS unit, TS↔.NET E2E).
Fixed
- TS Fusion: invalidating a still-computing
Computedno longer throws"Cannot set output on a non-computing Computed."It now mirrors .NET semantics —invalidate()called inComputingstate sets a_invalidatePendingflag, andsetOutputapplies the deferred invalidation immediately after transitioning toConsistent. This surfaced on reconnect when the server delivered result + invalidation back-to-back: thefusion-rpcclient'soutboundCall.whenInvalidated.then(...)microtask fired beforecompute-function.ts'ssetOutput. Covered by the new invalidate-during-Computing unit test incomputed.test.ts.
Tests
- New reconnect lifecycle matrix covering every (disconnect-stage × reconnect-stage) cell for both regular RPC and Fusion compute calls, in three layers:
- .NET pure:
RpcReconnectionMatrixTest,FusionRpcReconnectionMatrixTest. - TS unit:
computed.test.tsinvalidate-during-Computing cases. - TS-client ↔ .NET-server E2E:
TypeScriptRpcE2ETest.ReconnectMatrix(driven byts/e2e/ts-dotnet-e2e.ts). The F4 cell (disconnect mid-exec, server invalidates during outage, reconnect) is exactly the scenario that surfaced the bug above.
- .NET pure:
13.0.12+f471d693 | npm: 13.0.15
Release date: 2026-05-26
Follow-up release to v13.0 focused on connection-loss recovery and TypeScript catching up to .NET. New HTTP/2 transport guide, shorter keep-alive defaults, WebSocket leak / hang fixes on both .NET and TS sides, and RpcLimits ported to TypeScript.
Changed
- RPC: keep-alive defaults tightened from 15 s / 55 s to 10 s / 25 s (
RpcLimits.KeepAlivePeriod/KeepAliveTimeout). The new timeout still tolerates a full ~15 s server stall on top of one keep-alive cycle while cutting dead-connection detection from ~55 s to ~25 s. Reconnects are cheap; the old budget paid for nothing. Override per process viaRpcLimits.Defaultor per peer if your environment needs the old values. - RPC:
RpcPeer.SetConnectionStateis nowprivateand must be called only fromOnRun. External callers (rare) should drive state through the transport / connection layer instead.
Added
- TS RPC:
RpcLimitsclass mirroring the .NET shape, plumbed throughRpcHub.limits(defaults to a process-wideRpcLimits.Default). Replaces the looseCONNECT_TIMEOUT_MS/HANDSHAKE_TIMEOUT_MS/KEEP_ALIVE_*_MSconsts. Three override paths now match .NET: mutateRpcLimits.Default, assignhub.limits = new RpcLimits({ ... }), or set the matching*Msfield on a peer. Peers snapshot values fromhub.limitsat construction. - TS core: ported the remaining
promises.tsutilities from ActualChat so@actuallab/coreis self-sufficient —delayAsync/delayAsyncWith,PromiseSourceWithTimeout,throttle/debounce/ResettableFunc,serialize(fixes a latent queue-poisoning bug from the original),retry+catchErrors,abortPromise,ResolvedPromise.Void/True/False,TimedOutsentinel, and a per-packagegetLogsfactory. 48 new tests. - TS core:
PromiseSource<T>againimplements Promise<T>— exposesthen/catch/finallydelegators and[Symbol.toStringTag] = 'Promise', so it can be awaited or passed anywhere aPromise<T>is expected. The initial port had dropped this surface and forced callers through.promise; that's reverted..promise, boolean-returningresolve()/reject(), andisCompletedsemantics are unchanged;PromiseSourceWithTimeout<T>inherits the restored surface..promiseis retained at the RPC client proxy (rpc-client.ts), which hands the value to user code where exposing thePromiseSourcewould risk accidental.resolve()/.reject()calls. - TS core:
TimeoutError+withTimeout(promise, ms, message)helpers, used by the newconnectTimeoutMspath and the existing handshake-timeout block (replaces a string compare one.message).
Fixed
- RPC:
RpcWebSocketClientno longer leaksClientWebSocketinstances whenConnectAsynchangs or doesn't honor cancellation — the socket is now disposed on the cancellation path. Covered byRpcWebSocketClientConnectLeakTest. - RPC:
RpcWebSocketTransportaborts the underlying WebSocket onReadAllcancellation instead of relying on transport disposal, preventing hangs inReceiveAsync. NewAbortWebSockethelper; covered byRpcWebSocketTransportCancellationTest. - TS RPC: hung WebSocket connect (mobile after network change / device sleep, half-open TCP) no longer blocks the reconnect loop for the browser's ~2 min internal timeout. New
connectTimeoutMs(default 10 s) force-closes the socket so the loop iterates to the retry-delay branch. Mirrors .NET'sRpcLimits.ConnectTimeout. Covered byrpc-connect-timeout.test.ts.
Documentation
- New HTTP/2 transport guide — setup, use cases, performance comparison, and configuration options. Linked from the RPC sidebar and homepage.
- Videos and Slides page: improved layout, split-button quick-access to local decks.
13.0.3+26a8a3bd | npm: 13.0.4
Release date: 2026-05-23
This is a major release introducing new HTTP/2 transport for ActualLab.Rpc, Stream-based and PipeReader + PipeWriter-based transports, and RpcAlternatingClient capable of switching between transports on failures.
Breaking Changes
- RPC:
RpcPeer.IsConnected(),IsConnected(out handshake, out transport), andIsConnectedOrHandshaking()are gone fromRpcPeer. Equivalent checks now live onRpcPeer.ConnectionState:peer.IsConnected()→peer.ConnectionState.Value.IsConnected()peer.IsConnected(out h, out t)→peer.ConnectionState.Value.IsConnected(out h, out t)peer.IsConnectedOrHandshaking()→peer.ConnectionState.Value.IsConnectingOrConnected()
- RPC:
RpcPeerConnectionState.IsHandshaking()renamed toIsConnecting();IsConnectedOrHandshaking()renamed toIsConnectingOrConnected(). Two new helpers:IsDisconnected()andIsTerminal(). State is now also exposed via the newKindproperty (RpcPeerConnectionStateKind:Disconnected,Connecting,Connected,Terminal).
Added
- RPC: full-duplex HTTP/2 transport similar to gRPC's —
RpcHttpClient/RpcHttpClientOptionson the client andRpcHttpServer/RpcHttpServerBuilder/RpcHttpServerOptionson the server. The client streams requests viaDuplexHttpContent; the server is wired throughEndpointRouteBuilderExtwith default delegates inRpcHttpServerDefaultDelegates. Protocol negotiation inRpcWebHostdefaults to HTTP/2 whenUseHttpClientis used. - RPC:
RpcAlternatingClient— a composite client that alternates connection attempts across multiple innerRpcClients, tracking failed endpoints and rotating through them on reconnect. Useful for primary/secondary deployments and multi-region failover. - RPC:
RpcFrameBasedTransport— new base class for batched, frame-oriented transports.RpcPipeTransport(PipeReader/ PipeWriter),RpcStreamTransport(Stream-based framing), and the existingRpcWebSocketTransportall derive from it, sharing frame composition, metrics, and buffer renewal logic. - RPC:
RpcPeer.Extensions(MutablePropertyBag) — an extension-point property bag for attaching ad-hoc state to a peer.
Changed
- RPC: WebSocket transport updated to share frame composition with the new HTTP/2 / pipe / stream transports via new
RpcFrameBasedTransportandRpcFrameCodec. The publicRpcWebSocketTransportsurface is preserved, but the internals are now shared. - RPC: connection-state machine reshaped around a single
Kinddiscriminator (Disconnected/Connecting/Connected/Terminal) instead of inferring state fromHandshake/Connectionfields. TheRpcClientPeerpath now firesRpcClient.OnConnectionStateChangeon every transition to enable error history-based transport changes.
Documentation
- Homepage / SEO refresh: per-page meta descriptions, OG/Twitter cards, JSON-LD, self-referencing canonical URLs, refreshed copy, social card, and a sun hero background image.
- Added Fusion intro slide deck
- Linked "Videos and Slides" to the local slide decks.
- Clarified the comment and documentation style guidelines in
CODING_STYLE.md.
Tests
RpcAlternatingClientTestcovering the alternating logic and reconnection behaviour.RpcHttpBasicTestandRpcHttpPerformanceTestfor the new HTTP/2 transport; obsolete HTTP/2 window-size configuration was removed from the test harness.
12.5.2+080e9963 | npm: 12.5.2
Release date: 2026-05-09
Breaking Changes
- RPC:
RpcStream.BufferSizeis split intoRpcStream.AckAdvance(the wire-level flow-control window, formerly namedBufferSize) and a new local-onlyRpcStream.BufferSize(sender ring buffer capacity hint, default0= inheritAckAdvance). The MessagePack wire key changes fromBufferSizetoAckAdvance; the comma-separated text format keeps the same field position. Update any code that setsRpcStream.BufferSize— rename toAckAdvanceif you meant the in-flight window, or leave it for the new local-buffering hint (and addAckAdvance = ...if you want a non-default window). - TypeScript RPC:
RpcStreamRef.bufferSize,RpcStream.bufferSize, and thebufferSizeoption inRpcStreamOptions<T>→ackAdvance. New optionalbufferSizecontrols the local sender ring buffer (resolved atRpcStreamSenderconstruction time and exposed assender.bufferSize).
Added
- RPC:
RpcStream.BufferSize(local-only, .NET) /RpcStream.bufferSize(TypeScript, optional) lets real-time senders pre-buffer items past the in-flight ACK window so a freshly arrived ACK is served from RAM rather than waiting on the source. Values smaller thanAckAdvanceare clamped up toAckAdvanceand log a warning. - TypeScript RPC:
RpcStreamSender.onBuffered(count)callback fires after every push onto the local ring buffer. Combined with the existingonAckProcessed(drain side), this gives controllers a complete picture of buffer utilisation — the source pull is paused whenbufferedCount === sender.bufferSize. - TypeScript RPC: new
RpcConnectionState.Handshakingstate betweenConnectingandConnected, mirroring .NET'sRpcPeerConnectionStatephases. The run loop transitions to it on WS open;RpcServerPeer.accept()transitions to it immediately.
Changed
- TypeScript RPC: outbound calls now self-manage their connection wait, mirroring .NET
RpcOutboundCall.SendAsync. Each call registers up front and either sends immediately (if_isConnected) or attaches a one-shotconnectionStateChangedlistener that fires on the nextConnectedtransition. Removes the peer-level_pendingSendsqueue, the_flushPendingSendspump, and the_reconnectFlushInProgressflag. As a side effect, the$sys.Reconnectmid-reconcile deadlock is gone — the inner reconcile call no longer competes with a peer-level flush gate.
Documentation
- Updated
PartR-RpcStream.md,PartR-D.md, andPartTS-Rpc.mdto describeAckAdvancevsBufferSize(and when each applies).
Tests
.NET: 3 new end-to-end cases inRpcStreamRealTimeTest(BufferSizeAboveAckAdvance_PreBuffersBeyondAckWindow,BufferSizeUnset_FallsBackToAckAdvance,BufferSizeBelowAckAdvance_ClampsUpToAckAdvance) backed by a burst-tracked source.RpcStreamBasicTest: defaults, independent property setters,BufferSizenot in wire format.- TypeScript: 3 new
onBufferedcases plus refreshed wire-format tests assertingAckAdvanceis now the binary key.
12.4.8+77552387 | npm: 12.4.10
Release date: 2026-05-04
Added
- TypeScript RPC:
RpcStreamSenderobservability surface —nextIndex,lastAckIndex, andskipCountgetters plus anonAckProcessedcallback that fires once per ACK drain (coalesced when multiple ACKs are processed together; listener errors are swallowed so they can't break the pump).RpcStream.senderaccessor exposes the localRpcStreamSenderfor callers that need to read these metrics — intended for quality controllers that watch a real-time stream's backpressure / skip behaviour without reaching into private state.
Tests
rpc-stream-realtime.test.ts: 7 new cases covering the new metrics —nextIndex/lastAckIndexaccounting, drain coalescing, throwing-listener resilience, and a deterministicskipCountduring real-time compaction.
12.3.79+2fc189f6 | npm: 12.4.6
Release date: 2026-05-01
Breaking Changes
- RPC:
RpcStream.AckAdvanceis renamed toRpcStream.BufferSize(both .NET and TypeScript). Wire format is unchanged — only the property name and the corresponding TypeScript option (ackAdvance→bufferSize) differ. Update any code that explicitly sets the buffer-ahead limit onRpcStream<T>.
Changed
- RPC:
RpcStream.IsRealTimeskipping is now reactive instead of speculative. Previously, on hitting the buffer-ahead ceiling the sender drained the source itself looking for the nextCanSkipToitem; now the sender waits for an ACK and, when one arrives, compacts the already-buffered unsent suffix down to the latest skippable item. The sender no longer pulls ahead from the source just to hunt for a skip target. Reconnect-time skip-ahead also runs only whenIsRealTimeis set; non-realtime streams keep back-pressure semantics unchanged.RpcSharedStreamis consolidated to a singleOnRunpath covering both modes.
Documentation
- Expanded
CODING_STYLE.mdwith rules for flow-control spacing, class member ordering, primary constructors, sealed classes, preferred types likeFilePath, and TypeScript-side conventions mirroring the .NET ones.
Infrastructure
- Updated Fusion / OAuth / CliWrap package pins in
Directory.Packages.props.CommunityToolkit.HighPerformancebriefly moved to 8.4.2 and was reverted to 8.4.0 (a regression in 8.4.x is in flight upstream).
Tests
NerdbankCrossCompatTestis now guarded byNET8_0_OR_GREATER, andShardMapTestconstructs itsHashSetin a way that compiles on .NET Framework targets.
12.3.79+d2bf83a0 | npm: 12.3.85
Release date: 2026-04-30
Added
- TypeScript RPC:
RpcConnectionUrlResolvermay now returnstring | Promise<string>, and the connect pathawaits it. This unblocks resolvers that need to fetch a per-connection token (e.g. a session token) before forming the WebSocket URL. - TypeScript RPC:
sanitizeUrl(url)utility exported from@actuallab/rpc— redacts?session=...(URL-parsed when possible, regex fallback otherwise) so the connect-attempt log line no longer leaks bearer-style query parameters. Declared asexport letso library users can swap in a different sanitizer (e.g. one that redacts additional query keys).
12.3.79+a7608a16 | npm: 12.3.83
Release date: 2026-04-30
Fixed
- TypeScript RPC: proper backpressure in
RpcStream— acknowledgements are now consumer-driven instead of producer-driven. Previously the receiver sent an ACK as soon as an item landed in the buffer, which signalled false capacity to the producer and effectively disabled flow control. The stream now tracks_nextConsumedIndexand only acks up to what the iterator has actually yielded; duplicate-frame fast paths cap their ack at the consumed index as well, and the iterator loop emits a fresh_maybeSendAckafter each batch is drained. Addsrpc-stream.test.tscases covering the consumer-driven ACK behavior.
12.3.79+ef249695 | npm: 12.3.81
Release date: 2026-04-29
Added
- TypeScript RPC:
RpcErrorclass — failed remote calls now reject with anRpcError(instead of a plainError) that carries the remote exception'stypeNamewhen available. The handler parses.NET's assembly-qualifiedTypeRefstring (e.g."System.InvalidOperationException, System.Private.CoreLib") and exposes the type name only, so TypeScript callers can branch on remote exception types. The internalRpcRerouteExceptionlog path now matches against the fully-qualifiedActualLab.Rpc.RpcRerouteException. Exported from@actuallab/rpc. error-propagationE2E scenario inTypeScriptRpcE2ETest/ts-dotnet-e2e.tsvalidating .NET → TS exception propagation.
12.3.79+a4fdbdd9 | npm: 12.3.79
Release date: 2026-04-28
Fixed
- RPC: prevent connection stacking during a mid-handshake state. Connection state checks now use
IsConnectedOrHandshakinginstead ofIsConnected, so new connections no longer pile up against peers stuck in transient handshake states. Adds teardown safeguards and tightens disconnect resolution in edge cases. AffectsRpcPeer,RpcServerPeer,RpcPeerConnectionState,RpcWebSocketServer, and the TypeScriptRpcPeer.
12.3.76+d67c674e | npm: 12.3.76
Release date: 2026-04-25
Added
- RPC: compute methods can now serve a Regular call type (compute-to-regular downgrade). When an inbound message targets a
[ComputeMethod]but itsCallTypeIdisRegular, the server returns the result immediately and skips invalidation tracking — no entry is retained in the inbound call registry past completion. Useful for callers that want a one-shot value from a compute method without subscribing to invalidations. Implemented inRpcInboundContext(accepts the alternate call type) andRpcInboundComputeCall(newIsRegularCallpath that unregisters afterSendResult).
12.3.74+279ac90c | npm: 12.3.70
Release date: 2026-04-23
Added
- Nerdbank MessagePack converters for a core set of RPC and serialization types:
Result<T>,ExceptionInfo,VersionSet,RpcCacheKey,RpcCacheValue,RpcHandshake,RpcHeader,RpcHeaderKey,RpcMethodRef, andRpcObjectId. All converters emit the same array-based wire shape used by MessagePack-CSharp, somsgpackXandnmsgpackXRPC formats are now fully byte-compatible across runtimes — clients and servers can mix Nerdbank.MessagePack and MessagePack-CSharp without re-serialization. Registered in the defaultNerdbankMessagePackByteSerializerconfiguration. NerdbankCrossCompatTestcases covering the new RPC converters.
12.3.72+94144fd7 | npm: 12.3.70
Release date: 2026-04-23
Fixed
- Nerdbank
ApiMapNerdbankConverter<TKey, TValue>andImmutableOptionSetNerdbankConverternow also accept the legacy array-of-kv-pairs wire shape ([[k, v], [k, v], ...]) in addition to the standard map shape ({k: v, k: v, ...}). Keeps DB blobs written by the MessagePack-CSharp source-generated collection formatter readable after the v12.3.70 migration to Nerdbank, so no migration step is required for existing stored payloads.
12.3.70+04d6f22d | npm: 12.3.70
Release date: 2026-04-22
Added
- Nerdbank MessagePack converters for
PropertyBag,ImmutableOptionSet,ApiMap<TKey, TValue>, andTypeDecoratingUniSerialized<T>— closes the wire-compat gap where Nerdbank's default reflection shape couldn't express the legacy[Key(N)]index-based layouts used by MessagePack-CSharp. Stored blobs written by the legacy serializer remain readable. Registered in the defaultNerdbankMessagePackByteSerializerconfiguration. TextSerializedNerdbankConverter<T, TSerialized>(and closed-overNewtonsoftJsonSerializedNerdbankConverter<T>) — fixes a cross- serializer wire gap where MessagePack-CSharp wroteNewtonsoftJsonSerialized<T>as[Data]while Nerdbank emitted{Data: ...}, breaking every composite embedding such a value (notably a populatedImmutableOptionSet).NerdbankCrossCompatTestsuite — drives bytes directly between MessagePack-CSharp and Nerdbank readers/writers to catch wire-format divergence that self-round-trip tests miss.- TypeScript:
useBigInt64: truein the defaultmsgpackencoder —bigintvalues now serialize as msgpack int64/uint64, required for .NETlongfield compatibility when the value exceedsNumber.MAX_SAFE_INTEGER.
Changed
TypeDecoratingUniSerialized<T>wire format aligned with MessagePack-CSharp's[Key(0)] MessagePackDatalayout — a 1-element array with type-decorated inner bytes — so the same payload now cross-reads between Nerdbank and MessagePack-CSharp.- TypeScript: upgraded
@msgpack/msgpackto v3.1.3;Encoderinitialization refactored to the new object-based configuration API.
Fixed
- TypeScript RPC:
Dictionary<int, byte[]>arguments now serialize correctly over msgpack (unblocking the$sys.Reconnectmethod argument) via a newmsgpack-map-patch.tsalongside the v3.1.3 upgrade.
12.3.63+373bb905 | npm: 12.3.64
Release date: 2026-04-21
Added
- TypeScript:
RpcPeer.disconnect()— closes the current WebSocket connection without disposing the peer. ForRpcClientPeer, the run loop detects the disconnect and reopens the connection; the peer stays in the hub and all client proxies bound to it remain valid.
Changed
RpcStream.Disconnect()is now public (previously an explicitIRpcObject.Disconnect()implementation forwarding to a protected abstract). Callers holding anRpcStreamreference can now end it directly without casting toIRpcObject.
Fixed
RpcStream: reordered the disconnect path so the$sys.AckEndclose message is always sent before_isDisconnectedflips. This closes a race where a stream disconnecting during the "cannot reconnect" branch ofReconnectwould skip its close notification, leaving the remote side waiting.
12.3.60+21bf6f76 | npm: 12.3.60
Release date: 2026-04-20
Added
- Custom MessagePack formatters for core serialization types —
ImmutableBimapMessagePackFormatter,ResultMessagePackFormatter,BoxMessagePackFormatter, andMutableBoxMessagePackFormatter, registered inDefaultMessagePackResolverand wired up via[MessagePackFormatter]attributes onImmutableBimap,Result,Box,MutableBox, andApiArray. Works around a MessagePack source-generator bug that emits incorrect code for struct fields relying on default formatters.
Fixed
- TypeScript:
RpcPeercould enter a zombie state on reconnect when a disconnect happened mid-handshake. Connection tracking now uses a dedicated_isConnectedflag (independent of WebSocket state), which tightens state transitions, outbound call gating, andclose()behavior so silent no-ops and lingering zombie peers no longer occur.
12.3.56+9d28308c | npm: 12.3.50
Release date: 2026-04-19
Added
- Fusion:
RemoteComputeMethodFunctionracesSendRpcCallagainst peer disconnect events — remote compute calls no longer hang indefinitely when the peer drops mid-call; a stale cached value is served instead when one is available. RpcPeer.WhenDisconnectedandMarkDisconnected— disconnection is now a first-class, awaitable state.RpcPeer.WhenConnectedOrReroute— waits for a live connection and surfaces reroute exceptions so callers can re-resolve the peer instead of blocking on a dead one.RpcRouteStategained a reroute-aware hook used by the above.
Changed
RpcPeerConnectionStateExtremoved; theWhenConnectedextension methods are gone - use the identical regular method directly, or the newRpcPeer.WhenConnectedOrReroutehelper. State transitions (MarkConnected/MarkDisconnected/MarkTerminated) now live onRpcPeerConnectionStateitself.- RPC peer connection-state handling simplified: state transitions consolidated into
RpcPeerConnectionState, and the disconnection / connection-timeout flow inRpcPeertightened.RpcTestConnectionupdated to match.
Fixed
- Additional safeguards around serving stale cache on disconnect so reroute exceptions and terminal errors propagate correctly, closing race windows that could produce spurious failures right after a peer dropped.
12.3.50+c3a95b95 | npm: 12.3.50
Release date: 2026-04-18
Breaking Changes
- TypeScript:
RpcClientPeerAPI reshaped —peer.run(factory)is replaced by thewebSocketFactoryfield pluspeer.start()(ctor also gained amustStart = trueparameter);peer.connected/peer.disconnectedevents are gone — subscribe topeer.connectionStateChanged(emitsRpcConnectionState) or awaitpeer.whenConnected();peer.connectionKindrenamed topeer.connectionState; andpeer.reconnectDelayermoved up toRpcHub.reconnectDelayerso every client peer on a hub shares the delayer. Migration: dropvoid peer.run()(auto-starts by default) or setwebSocketFactory+ callpeer.start()when you needmustStart = false; replace event-based checks withconnectionStateChangedorwhenConnected(); routecancelDelays()/delays = ...throughhub.reconnectDelayer.
Added
- Fusion: remote compute methods now serve the last cached value when the peer is disconnected (instead of failing), and auto-invalidate via the new
InvalidateWhenReconnectedpath once the peer reconnects — improves UI resiliency across brief disconnects. - TypeScript:
RpcPeerRefBuilderhelper for composing peer refs.RpcPeerRefBuilder.forClient(url, format)bakes the serialization format into the URL via?f=...;RpcPeerRefBuilder.forServer(id)returnsserver://{id}. - TypeScript:
RpcClientPeer.webSocketFactoryfield for injecting a custom WebSocket constructor (Node.js / tests).
Changed
- TypeScript:
RpcClientPeerReconnectDelayeris now a single instance shared viaRpcHub.reconnectDelayerand centralized there — swap in an app-level subclass (e.g. signal-gated) on the hub before peers start.
Fixed
- TypeScript: reconnection edge cases in
RpcClientPeer— handshake timeouts now close the connection and retry after the configured delay; resolved deadlock scenarios during$sys.Reconnectcalls; addressed lost-close race conditions duringrun()iterations.
Documentation
- Rewrote
PartTS-Rpc.md,PartTS.md,PartTS-FusionRpc.md, andPartTS-React.mdexamples against the newRpcClientPeerAPI (start()/whenConnected()/connectionStateChanged/hub.reconnectDelayer).
Tests
- Added regression coverage for the reconnect edge cases above (
rpc-reconnect-edge-cases.test.ts) and migratedfusion-rpc-run-reconnection.test.tsto the new API. - Added
ComputeMethodResultStashTest(11 cases covering roundtrip, disposal cleanup, stash-twice / after-dispose errors, per-key serialization, and shared-lock-set ctor) plus aStashComputeServiceintegration test.
12.3.42+3661472f | npm: 12.3.33
Release date: 2026-04-18
Performance
- Replaced unnecessary
Interlocked.Exchangecalls withVolatile.Write/Volatile.Readacross Core, Fusion, and RPC hot paths (BatchProcessor,SafeAsyncDisposableBase,ArrayPoolBuffer,Connector,Computed,ComputedRegistry,ComputedSynchronizer,ComputedGraphPruner,RpcObjectTrackers,RpcSharedStream,RpcWebSocketTransport). AddedInterlockedExt.VolatileRead/InterlockedExt.VolatileWritehelpers for cases where a full interlocked operation isn't needed.
12.3.39+4e55ed7e | npm: 12.3.33
Release date: 2026-04-17
Breaking Changes
RpcRouteState.LocalExecutionAwaitersignature changed fromFunc<CancellationToken, ValueTask>toFunc<bool, CancellationToken, ValueTask>, andRpcRouteStateExt.PrepareLocalExecutiongained a requiredaddDependencyparameter beforecancellationToken. Custom awaiters and any directPrepareLocalExecutioncallers must accept and forward the newbool addDependencyargument.addDependencyistruefor compute method calls - you can use it to actually add a dependency for such call on, e.g., the current routing state.
Documentation
- Reworked all code snippets so every block in the docs is consumed directly from a compiled snippet source (no drifted hand-written duplicates).
- Fixed placement of
MustStore(false)in the operation-context setup example and clarified its usage. - Miscellaneous prose and formatting cleanup across the docs set.
npm: 12.3.33+59552c71
Release date: 2026-04-16
Added
- TypeScript: scoped logging API in
@actuallab/core—Log,LogLevel, andcreateLogProvider(prefix, defaults)factory for per-package, typedgetLogs(scope)helpers. EachLog.get(scope)returns a bag of optional loggers (debugLog,infoLog,warnLog,errorLog) that arenullwhen below the scope's minimum level — call sites usedebugLog?.log(...)so disabled logs cost a single nullish check. - TypeScript:
initLogging()persists user-set minimum levels tosessionStorage(3-day TTL) and installs aglobalThis.logLevelscontroller exposingoverride(scope, level),overrideAll(prefix, level),dump()(prints every known scope as aconsole.table),reset(), andclear()for runtime tweaking from the browser dev console. - TypeScript: per-package
getLogshelpers in@actuallab/rpcand@actuallab/fusionwith package-prefixed scopes (e.g.'rpc.RpcPeer','fusion.ComputedState') and explicit per-scopeLogLeveldefaults. Global baseline isWarn;rpc.RpcPeerisInfoso connection-lifecycle events surface out of the box. All other scopes areWarn— quiet by default; users opt in vialogLevels.override(...).
Changed
- TypeScript: replaced ad-hoc
console.warncalls acrossrpc-peer,rpc-stream,rpc-stream-sender,rpc-system-call-handler,rpc-system-call-sender,rpc-hub,rpc-service-host,rpc-connection,rpc-peer-state-monitor,computed-state, andui-action-trackerwith the new scoped logger, mirroring the corresponding .NET log calls.
12.3.25+0a851ea7 | npm: 12.3.29
Release date: 2026-04-16
Breaking Changes
- Removed the
RpcWebSocketServerOptions.ChangeConnectionDelayoption (both ASP.NET Core and OWIN/NetFx variants). Stale-connection teardown now happens synchronously before the WebSocket upgrade, so the dedicated delay is no longer meaningful. Drop any code that sets this option.
Added
- TypeScript:
RpcStreamsource factories of the form(abortSignal: AbortSignal) => AsyncIterable<T>now get a grace period (RpcStream.disconnectGracePeriodMs, default 100ms) ondisconnect()to honor the AbortSignal and exit cooperatively before the sender force-closes viaiterator.return(). PlainAsyncIterable<T>sources (which can't observe the signal) are force-closed immediately as before. - TypeScript:
RpcCallStageconstants (ResultReady,Invalidated,Unregistered) andcompletedStagetracking onRpcOutboundCall, both ported from .NET. - TypeScript:
IncreasingSeqCompressorin@actuallab/rpc— LEB128-based sorted-integer-sequence compression, wire-compatible with .NET. Shared fixtures between the TS test suite and a new .NETTheoryconfirm byte-for-byte wire compatibility for the$sys.Reconnectprotocol. - TypeScript:
RingBuffer<T>in@actuallab/core— fixed-capacity circular buffer matching .NETActualLab.Collections.RingBuffer<T>. - TypeScript:
RpcPeer.formatis now a mutable property (getter/setter);RpcServerPeeraccepts an explicit format override, letting the test harness align both peers on any supported wire format.
Fixed
- RPC server: stale connections are now disconnected before the new WebSocket upgrade rather than after. Previously the old-connection teardown could consume the client's
HandshakeTimeoutbudget on a dead socket; performing it before the upgrade consumesConnectTimeoutinstead, which is the correct budget for "waiting for server to be ready to talk". - RPC WebSockets: reduced the default
RpcWebSocketTransport.CloseTimeoutfrom its previous value to 1 second to limit effectiveConnectTimeoutshrinkage and lower the abrupt/graceful-close ratio impact on connection handling. - TypeScript:
RpcClientPeer._reconnectnow runs the$sys.Reconnect:3protocol on same-peer reconnects to ask the server which call IDs it no longer recognizes, and only resends those. Previously the client blindly re-sent every in-flight outbound call on every reconnect, causing the server to spawn a second handler for streaming calls (e.g. ActualChat'sPushAudio) and double-process the stream. Matches .NETRpcOutboundCallTracker.Reconnect. TS also now handles incoming$sys.Reconnectcalls: when a peer acts as server, the inbound-call tracker is consulted to produce the set of unknown call IDs, wrapped in$sys.Okexactly as .NET does. - TypeScript:
RpcClientPeer._reconnectnow disposes client-owned shared objects (e.g.RpcStreamSenderinstances) on peer change, matching .NETRpcPeer.Reset(). Previously these senders lingered indefinitely after a reconnect to a server with a differenthubId, causing an unbounded leak of stream-sender state and source iterators. - TypeScript:
RpcStreamSender.writeFromis now ACK-driven (mirroring .NETRpcSharedStream<T>): the main loop blocks waiting for a client ACK, so while the peer is disconnected no source items are pulled. PreviouslysendItemwas a no-op when disconnected but the pump kept pulling from the source, silently discarding up to thousands of items per disconnect window. A bounded replay buffer holds unacknowledged items so they can be resent on reconnect.
Documentation
- Documented
RpcRemoteExecutionModein the Call Routing reference page.
Tests
- New
.NETTypeScriptRpcE2ETest.ReconnectNoDuplicatecross-language E2E theory exercises$sys.Reconnect:3end-to-end (Node-hosted TS client ↔ ASP.NET Core .NET server) forjson5,msgpack6, andmsgpack6c. Verifies the server invokes a long-runningSlowEchohandler exactly once across a same-peer reconnect — the regression guard for the audio-double-processing bug. - New
rpc-reconnect-wire-format.test.tslocks in byte-level wire fixtures for both the JSON and MessagePack shapes of thecompletedStagesargument. - New
.NETIncreasingSeqCompressorTest.CrossPlatformWireFormatFixturestheory locks in the exact byte output of 10 representative inputs. The same fixtures are asserted by the TypeScriptincreasing-seq-compressor.test.tssuite, giving us a bidirectional wire-compatibility contract for$sys.Reconnect.
Infrastructure
- TypeScript:
Run-Tests.cmdnow setsCI=1andNO_COLOR=1and uses Vitest'sbasicreporter for consistent, silent CI output.
12.3.16+47f5b5a0 | npm: 12.3.14
Release date: 2026-04-16
Added
- New
RpcRemoteExecutionMode[Flags]enum (AwaitForConnection,AllowReconnect,AllowResend) giving per-method control over outbound RPC connection waiting, reconnection, and resending behavior RpcMethodAttribute.RemoteExecutionModeproperty for overriding the default (AwaitForConnection | AllowResend) on a per-interface or per-method basis;NoWaitmethods use0, compute methods must useDefault- TypeScript: matching
RpcRemoteExecutionModesupport inrpc-service-def,rpc-client, and decorators ShardMapBuilder.Maglev— Google's Maglev consistent hashing algorithm as a new shard map builder with perfect balance (max-min ≤ 1) and lower disruption than Rendezvous at higher node counts- TypeScript:
AbortSignal-based cancellation for localRpcStreamsources —RpcStreamSource<T>now also accepts a factory(abortSignal: AbortSignal) => AsyncIterable<T>, letting sources release resources (camera, microphone, etc.) promptly on disconnect - TypeScript:
RpcServerPeer.accept()now disconnects shared objects on connection close, andonAckEnd()delegates todisconnect()for proper iterator cleanup
Tests
- Expanded
ShardMapTest.BuilderComparisonTestwith additionalInlineDatascenarios, winner/tie tracking, and detailed comparison metrics across builder strategies - New
RpcRemoteExecutionModeTest(.NET) andrpc-remote-execution-mode.test.tscovering connection waiting, reconnection, in-flight calls, and method-definition validation - New
rpc-stream-cancellation.test.tsandNoReconnectStreamSourceCancellationTestverifying source enumerator finalization on client disconnect withAllowReconnect=false
Infrastructure
- TypeScript: ESLint warning cleanup across all packages
npm: 12.3.6+872c4869
Release date: 2026-04-15
Changed
- TypeScript: tightened ESLint rules — removed overly permissive overrides for
@typescript-eslint/no-unsafe-*rules - TypeScript: cleaned up code, fix ESLint warnings, and removed redundant ESLint disable directives across all packages
- TypeScript: removed
noUncheckedIndexedAccessfrom tsconfig and cleaned up all non-null assertion operators (!) that were only needed for it
12.3.2+f23f4ac2 | npm: 12.3.2
Release date: 2026-04-15
Added
- .NET and TypeScript: Real-time stream mode with skip-to-keyframe support in
RpcStream:- New
IsRealTime/isRealTimeproperty - New
CanSendTo/canSkipToproperty - When
IsRealTimeandAllowReconnectare both true, reconnection clears the stale buffer and skips to the nextCanSkipToitem
- New
- TypeScript:
RpcStreamis now dual-mode (local + remote), matching the .NET design — service methods can returnnew RpcStream(source, { isRealTime: true, ... })with full configuration - TypeScript:
RpcStream.toRef(peer)method creates and registers anRpcStreamSender, starts pumping items, and returns the serialized stream reference (text or binary format) - TypeScript:
RpcStream.whenSentproperty — aPromisethat resolves when the sender finishes pumping all items - TypeScript:
RpcStreamOptions<T>interface for configuring local streams - TypeScript:
RpcSerializationFormatandRpcSerializationFormatResolvertypes similar to the .NET ones - TypeScript: MessagePack format support (
msgpack6andmsgpack6c) - TypeScript: "Compact" call format support (name hash-based method resolution)
- TypeScript: bundled XXH3-64 hash implementation for method name hashing
Changed
- TypeScript:
rpc-peer.tsstream dispatch now wraps rawAsyncIterableresults inRpcStreamand delegates totoRef()instead of creatingRpcStreamSenderdirectly - TypeScript: adopted Voxt.ai TypeScript coding style (4-space indent, single quotes, prettier)
- Consolidated C# E2E tests into single class with
[Theory]
Documentation
- Documented
IsRealTime,CanSkipTo, and real-time reconnection behavior inPartR-RpcStream.md - Added TypeScript dual-mode
RpcStreamAPI documentation withtoRef()andwhenSentexamples
Tests
- .NET: Comprehensive tests for
RpcStream.IsRealTimefeature - .NET: New tests verifying that after disconnect/reconnect, the first item is a keyframe (multiple of
keyFrameInterval) - TypeScript: E2E tests for all 3 serialization formats and
isRealTimestream feature - TypeScript: Added local-mode
RpcStreamtests (construction, config, iteration,toRef,whenSent, E2E config propagation) - TypeScript: Added real-time, reconnect tests, and tests verifying
canSkipTofiltering on reset ACK - TypeScript: XXH3-64 implementation tests
Infrastructure
- TypeScript: added
Run-Lint.cmdbuild script, renamedInstall-Packages.cmdtoNpm-Install.cmd
12.2.4+09f1dc55 | npm: 12.1.115
Release date: 2026-04-04
Fixed
- Fixed NativeAOT downcast bug in additional places (
MethodDef,RpcMiddlewareContext) with conditionalUnsafe.Asworkaround
12.2.1+41e24193 | npm: 12.1.115
Release date: 2026-04-03
Breaking Changes
CodeKeeperAPI overhauled:- New and simpler
XxxCodeKeeper.IExtensionextension points. - Removed instance-based
Get<T>()/Set<T, TImpl>(),AddAction(),RunActions(),KeepUnconstructable(),CallSilently(), andFakeCallSilently()methods CodeKeeperis now a static utility with simplifiedKeep<T>(),Keep(Type),KeepSerializable<T>()methods- Removed
TypeCodeKeeper,SerializableTypeCodeKeeper,RpcMethodDefCodeKeeper RpcProxyCodeKeeper,CommanderProxyCodeKeeper,FusionProxyCodeKeeperare replaced byProxyCodeKeeper.IExtensionimplementations (RpcProxyCodeKeeperExtension,CommanderProxyCodeKeeperExtension,FusionProxyCodeKeeperExtension)
- New and simpler
RpcCallTimeouts.LogTimeoutrenamed toDelayTimeout;RpcCallTimeouts.DefaultLogTimeoutrenamed toDefaultDelayTimeout— update any code referencing these propertiesRpcMethodAttribute.LogTimeoutrenamed toDelayTimeout— update attribute usages in service interfacesRpcOutboundCallOptions.ReroutingDelayersignature changed — now takes(RpcMethodDef, int, CancellationToken)instead of(int, CancellationToken)
Added
CodeKeeper.IExtensioninterface for pluggable trimming/NativeAOT code retentionRpcDelayedCallActionflags enum for configurable delayed call handling:None,Abort,Resend,Log,LogAndAbort,LogAndResendRpcMethodAttribute.DelayActionproperty to control per-method delayed call behaviorRpcOutboundCallOptions.DelayHandlerfor custom delayed call handling logic- Delayed compute calls are now automatically re-sent by default (
LogAndResend), improving reliability
Changed
- NativeAOT sample restructured: all
CodeKeeper.Set/RunActionscalls are removed (they're unnecessary now)
Fixed
- Fixed
RpcSharedStreamusingIsCompletedinstead ofIsCompletedSuccessfullyfor task state checks, preventing incorrect behavior when tasks are faulted or cancelled
Documentation
- Updated benchmark data to v12.1.130 (.NET 10.0.5) with new latency tables
12.1.130+4189e1bf | npm: 12.1.115
Release date: 2026-04-01
Added
- Added
AllowExecuteDeleteAsyncoption toDbLogTrimmerOptionsandDbSessionInfoTrimmer.Options, allowing control over whetherExecuteDeleteAsync(bulk SQL DELETE) is used in trimmer operations. Defaults totrueon .NET 7+ andfalseon older targets - When
AllowExecuteDeleteAsyncisfalse(or on pre-.NET 7), trimmers now fall through to the row-by-row deletion path even on .NET 7+, enabling compatibility with EF providers that don't supportExecuteDeleteAsync
Changed
DbAuthServiceBuildernow registersDbSessionInfoTrimmer.Options.Defaultvia factory instead of directTryAddSingleton<Options>(), allowing easier default customization
12.1.128+31a47345 | npm: 12.1.115
Release date: 2026-03-30
Fixed
- Fixed
NullReferenceExceptioninAsyncLockSet.Releaser.Dispose()when the releaser is default-valued (uninitialized)
12.1.125+ba936b35 | npm: 12.1.115
Release date: 2026-03-26
Fixed
- Fixed potential socket errors (SocketError 125 / ECANCELED) during WebSocket connection in
RpcWebSocketClient— the connect timeoutCancellationTokenSourcecould fire after a successful connect, aborting the already-established socket. Now disposed immediately after successful connection to prevent late cancellation from affecting the live socket
12.1.123+9dc3aaeb | npm: 12.1.115
Release date: 2026-03-24
Fixed
- Prevented race condition in
RpcCallTrackerson mobile app resume that caused misleading "delayed call" reports — added keep-alive timeout check to avoid premature call timeouts during app resume scenarios
Changed
RpcCallStagenow outputs "None" for stage value 0 instead of a numeric representation
12.1.119+8e01cd91 | npm: 12.1.115
Release date: 2026-03-20
Breaking Changes
- Removed
Arithmetics,ArithmeticsProvider,Range,Tile,TileLayer,TileStackand all associated types/extensions fromActualLab.CoreMathematics namespace. These abstractions are no longer part of the library - Removed
RangeModelBinderandRangeModelBinderProviderfromActualLab.Fusion.Server
Fixed
RpcSystemCallSenderandRpcSharedStream.Batcher._isPolymorphicnow useRpcArgumentSerializer.IsPolymorphicfor polymorphic type checks
12.1.114+a74e74b2 | npm: 12.1.115
Release date: 2026-03-18
Added
RpcSerializableAttribute([RpcSerializable]) — marks abstract types as non-polymorphic for RPC serialization, allowing the underlying serializer's union support ([JsonDerivedType],[MemoryPackUnion],[Union]) to handle type discrimination instead of RPC'sTypeRefwrappingRpcSerializationFormatExceptionandRpcWebSocketCloseCode.UnsupportedFormat— better error handling when client requests a serialization format unknown to the server
Documentation
- Added "Polymorphic Serialization" section to RPC Serialization docs covering
[RpcSerializable]usage
Tests
- Added tests for
[RpcSerializable]withNonPolymorphicBasehierarchy, includingRpcStreamscenarios - Added tests for unsupported serialization format handling in
RpcWebSocketTest
12.1.107+fe771590 | npm: 12.1.100
Release date: 2026-03-17
Added
SharedFloatPoolandSharedDoublePoolinArrayPools(ActualLab.Core) — shared array pools forfloatanddoubletypes
Changed
- Removed
unmanagedconstraint fromNonPoolingArrayPool<T>, allowing it to work with any type
Tests
- Added unit tests for
NonPoolingArrayPool<T>inActualLab.Tests
12.1.102+00807c35 | npm: 12.1.100
Release date: 2026-03-17
Fixed
AsyncTaskMethodBuilderExt.FromTaskdidn't work — introducedGenericAccessors<T>to workaround unsafe accessors for generics due to runtime limitations; refactored both generic and untypedFromTaskvariant
Tests
- Added unit tests for
FromTaskandGenericFromTaskto validate functionality and correctness
12.1.100+b7edfdd5 | npm: 12.1.100
Release date: 2026-03-16
Breaking Changes (.NET)
- Removed
IsReconnectableproperty fromIRpcSharedObject— replaced byAllowReconnectonIRpcObject RpcStream.New<T>()factory method parameter renamed fromisReconnectabletoallowReconnect
Added (.NET)
AllowReconnectproperty onIRpcObjectinterface — controls whether an RPC object (stream) should reconnect or immediately disconnect when a peer connection dropsRpcStream<T>now serializesAllowReconnectas a 5th field in the wire format (backward-compatible: old 4-field format defaults toAllowReconnect = true)- Server-side
RpcSharedStreamrejects reconnect attempts forAllowReconnect = falsestreams and auto-disposes them on disconnect RpcRemoteObjectTrackerdisconnects non-reconnectable remote objects on peer disconnect
Added (TypeScript)
- TypeScript RPC:
allowReconnectsupport inRpcStream,RpcStreamSender,parseStreamRef(), andRpcRemoteObjectTracker
Documentation
- Updated RpcStream docs to reflect
AllowReconnectreplacingIsReconnectable
Tests
- Added
NoReconnectStreamTestandNoReconnectStreamDisconnectTest(.NET) - Added TypeScript unit tests for
allowReconnectinRpcStream,RpcStreamSender, andparseStreamRef - Added TypeScript end-to-end tests for
allowReconnect = falsedisconnect behavior - Cross-language E2E test (
StreamNoReconnect) verifyingAllowReconnect = falsebehavior between TypeScript client and .NET server
12.1.98+62afac4f | npm: 12.1.69
Release date: 2026-03-10
Added
CpuTimestampBasedVersionGenerator— a newVersionGenerator<long>based onCpuTimestampticks, useful for in-process only high-resolution monotonic versioningIState.GetExistingComputed()method (it was available viaComputedInput, but wasn't a part ofIState)
Changed
- Renamed
Versioning/Providers/folder toVersioning/Generators/and movedClockBasedVersionGeneratortoActualLab.Versioningnamespace
Fixed
- Stale state bug in
ComputedStateduringRecompute: concurrent invalidation could target an already-replaced computed instance, causing the state to miss updates.StateExt.InvalidateandRecomputenow useGetExistingComputed()instead ofSnapshot.Computedto address that
Documentation
- Added Standalone Authentication guide — explains how to extract Fusion's auth system into your own project for full control and simpler code
12.1.89+ee8734c3 | npm: 12.1.69
Release date: 2026-03-04
Breaking Changes
ShardMap<TNode>constructor no longer acceptsFunc<TNode, IEnumerable<int>>for custom hashing — useShardMapBuilderparameter instead- Default shard mapping algorithm changed from greedy to rendezvous hashing — existing shard assignments will differ after upgrade
DbLogReader.ProcessBatchreturn type changed fromTask<int>toTask<Moment>— subclasses must update their override signatures
Added
ShardMapBuilderabstraction with two built-in strategies:GreedyShardMapBuilder(old behavior) andRendezvousShardMapBuilder(new default) for optimal minimal reallocation when nodes changeDbEventLogReader.GetMinDelayUntilfor precise delayed event scheduling — log reader now sleeps until the next delayed entry instead of polling on a fixed interval
Changed
- Improved log processing queries in
DbEventLogReader: it uses== LogEntryState.Newfilter andOrderBy(DelayUntil)for more reliable index utilization
Fixed
- Operation event processing now schedules precisely based on the earliest pending entry's
DelayUntil, avoiding unnecessary polling DbLogReader.ProcessNewEntriesreworked to support precise sleep-until scheduling based onProcessBatchreturn value
Documentation
- Replaced Mermaid diagrams with SVG images in architecture docs
- Added animated SVG diagrams for distributed scaling, dependency graphs, caching, and recomputation
- Added TypeScript port documentation section
Tests
- Added
ShardMapBuildercomparison tests (greedy vs rendezvous) - Added delayed event processing test with precise scheduling verification
12.1.69+10006328 | npm: 12.1.69
Release date: 2026-02-22
Added (TypeScript)
RpcStreamsupport in TypeScript RPC client — full streaming with batching, reconnection, and end-of-stream handling- Stream performance test (
StreamInt32) for benchmarking TypeScript RPC stream throughput RpcType.streamreturn type in TypeScript service definitions
Changed (TypeScript)
- TypeScript RPC method definitions now use
returns: RpcType.noWaitinstead of anoWaitboolean flag, improving API consistency - Simplified wire argument count calculations in TypeScript by assuming
CancellationTokenslot as default (removedctOffsetoption)
Tests (TypeScript)
- Added comprehensive
RpcStreamunit tests in TypeScript covering batching, reconnection, multiple enumeration, and disposal - New stream performance benchmarks
12.1.61+045f13f0
Release date: 2026-02-18
Added
- "No polymorphism" JSON serialization formats (
json5np,njson5np) for strict non-polymorphic RPC serialization RpcLimits.PrematureDisconnectTimeoutfor improved connection backoff logic
Changed
- Connection retry logic now uses
ConnectionAttemptIndexinstead ofTryIndexwith handling for premature connection closures - Default serialization format for
RpcClientPeer(TypeScript) changed tojson5np
Documentation
RpcLimitsclass documentation
Tests
- Added TypeScript RPC performance harness
12.1.51+8e1051d0
Release date: 2026-02-17
Added
IsSynchronizedandWhenSynchronizedmethods onState,ComputedState, andMutableStatefor streamlined synchronization checksIState.IsSynchronized(),IState.WhenSynchronized(), andIState.Synchronize()extension methods inStateExtKeepProcessedItemsandKeepDiscardedItemsretention settings inDbLogReaderOptionsfor fine-grained control over log item lifecycle
Tests
- Added TypeScript RPC reconnection scenario tests (both unit and E2E against .NET server)
12.1.41+718a0325
Release date: 2026-02-14
Added
- Work in progress: TypeScript Fusion client. Core abstractions, compute methods,
ComputedState,MutableState, invalidation, andfusion-reactpackage with React bindings. - React-based "Todo v3" page in the TodoApp sample showing how to use the client.
Fixed
- Missing
HasName(...)calls inFusionBuilderforAddClient,AddServer, andAddDistributedServicemethods - Proper
ArgumentDataformatting inRpcInboundCallfor improved readability
Infrastructure
- Added TypeScript monorepo under
ts/with@actuallab/core,@actuallab/rpc,@actuallab/fusion, and@actuallab/fusion-rpcpackages - Integrated TypeScript build pipeline into the Todo sample Host project via MSBuild targets
12.1.14+28a7e73e
Release date: 2026-02-11
Fixed
- RPC WebSocket disconnect detection was delayed by ~50 seconds instead of being instant. When the server shut down,
RpcPeer.OnRunawaitedmaintainTasksin afinallyblock before cancellingreaderTokenSource, soSharedObjects.Maintain()kept running its keep-alive check loop for up to 55s (KeepAliveTimeout) before detecting the timeout. The fix moves thereaderTokenSourcecancellation before themaintainTasksawait.
12.1.12+0475b1ca
Release date: 2026-02-11
Breaking Changes
mempack6(c)/msgpack6(c)binary protocols no longer persist message size, fixing the compatibility issue with pre-v12 protocols. This seems to be the very first release issue attributed to Claude Code: it somehow concluded the size has to be persisted while migratingWebSocketChanneltoRpcWebSocketTransportAPI, but in reality is wasn't (the code branch persisting the size was disabled via other logic). Sorry we caught this just now: the issue is there for two weeks already (from v12.0.9).
Fixed
Option<T>now always uses explicitMessagePackFormatterinstead of conditionalMessagePackObject(true)withSuppressSourceGenerationon .NET 8+, fixing serialization consistency across target frameworks
Tests
- Added conditional flags (
UseSystemJsonSerializer,UseNewtonsoftJsonSerializer,UseMessagePackSerializer,UseMemoryPackSerializer) inSerializationTestExtfor selectively enabling/disabling specific serializers in tests - Added serialization round-trip tests for
DbUser,DbChat,DbMessage
12.1.4+7b59e831
Release date: 2026-02-07
Added
- New
ActualLab.Serialization.NerdbankMessagePackpackage – optional Nerdbank.MessagePack serialization support. You can also register newnmsgpack6/nmsgpack6cRPC formats by callingRpcNerdbankSerializationFormat.Register()at startup
Fixed
ApiNullable<T>,ApiNullable8<T>,ApiOption<T>now always use explicitMessagePackFormatterinstead of conditionally usingMessagePackObject(true)on .NET 8+
Documentation
- Added api-index.md – condensed type reference (~300 lines) alongside api-index-full.md (full API index)
12.0.85+53469221
Release date: 2026-02-06
Breaking Changes
CpuTimestamp.PositiveInfinityandCpuTimestamp.NegativeInfinityrenamed toMaxValueandMinValueCoarseCpuClockremoved as mostly useless, useCpuClockinstead;MomentClockSetno longer has aCoarseCpuClockproperty and its constructor no longer accepts acoarseCpuClockparameter
Changed
- RPC keep-alive tracking now uses
Moment(wall-clock time) instead ofCpuTimestamp, fixing reliability issues on Unix systems where CPU sleep could stall timestamps
Fixed
IRpcSharedObject.LastKeepAliveAtchanged fromCpuTimestamptoMoment–CpuTimestampcould freeze during CPU sleep on Unix, causing incorrect keep-alive trackingRpcHub.Clockrenamed toRpcHub.SystemClock(now usesSystemClockinstead ofCpuClock) by the same reason
Documentation
- XML summary descriptions added (auto-generated with Claude Code) to all public types and members
- Added
Api-Index.md– a comprehensive type catalog listing all public types acrossActualLab.FusionNuGet packages
12.0.76+7e668fb2
Release date: 2026-02-04
Fixed
RpcRerouteExceptionhandling code no longer attempts to reroute during disposal ofIServiceProvider, preventing potential rerouting cycles during shutdown
12.0.70+1775d374
Release date: 2026-02-03
Fixed
RpcStreamwithIsReconnectable == falsefails right on the first enumeration rather than after the reconnection attempt
12.0.65+68251969
Release date: 2026-02-03
Added
RpcStream.IsReconnectableproperty – controls whether a stream can be reconnected after disconnection;trueby default, set tofalseto make reconnection attempts fail withRpcStreamNotFoundExceptionRpcStreamNotFoundException– new exception thrown when attempting to reconnect a non-reconnectable or expired stream
Tests
- Added
FlakyTest.XUnitdependency for marking time-dependent tests as flaky - Marked timing-sensitive tests (
ConcurrentTimerSetTest,ConcurrentFixedTimerSetTest) with[FlakyFact]attribute for improved test reliability
12.0.60+9bb19676
Release date: 2026-02-01
Breaking Changes
IOperationEventSource.ToOperationEvent()signature changed: now acceptsIServiceProviderparameter instead of no parameters (intermediate version acceptedIOperationScope)MemoryBuffer<T>replaced withRefArrayPoolBuffer<T>– migrate by updating type references and usingArrayPool<T>constructor parameterArrayPoolBuffer<T>,ArrayOwner<T>, andBufferWriterExtmoved fromActualLab.IOtoActualLab.Collectionsnamespace
Added
RefArrayPoolBuffer<T>– a ref struct buffer backed byArrayPool<T>with configurable pool and clearing behaviorTimeSpan?.ToShortString()extension method with customizable null fallback valueArrayPoolsstatic class providing common array pool instances
Changed
ArrayPoolBuffer<T>enhanced with additional constructor overloads andToArrayOwner()method
Fixed
AsyncTaskMethodBuilderextension methods now check task completion state before accessingTask.Exception, preventing potential inner exceptions
Tests
- Improved ActualLab.Rpc test stability with positive
maxWaitTimevalidation - Better test database isolation in
FusionTestBase
Documentation
- Added
BatchSizeproperty documentation to RpcStream flow control section - Added Docker benchmark results to performance documentation
Infrastructure
- Added
Clean.cmdscript for cleaning build artifacts on Windows and Unix platforms
12.0.45+0ee956d2
Release date: 2026-01-29
Performance
- Simplified
RpcOutboundMessageby replacingWhenSerializedTask with synchronousSendHandlercallback; dependencies likeRpcOutboundCall.SendXxx,RpcStream.OnItem,OnBatch, andOnEndtransitioned fromTasktovoidreturn type reducing task allocations in the RPC send path, bringing ~20% performance improvement.
Tests
- Added comprehensive unit tests for
TaskCompletionHandlercovering various task states (completed, faulted, cancelled) and all handler variants (1, 2, 3 state objects)
Infrastructure
- Removed obsolete
docs.slnfile - Removed JetBrains.Annotations package reference
- Updated package versions: Blazorise 1.8.9 (used only in samples), Bullseye 6.1.0, and CliWrap 3.10.0 (used in Build.csproj)
- Updated analyzer packages: Moq.Analyzers, xunit.analyzers, Roslynator.Analyzers, Meziantou.Analyzer.
12.0.34+842f172c
Release date: 2026-01-28
Added
- Native little-endian serialization support in RPC serializers with optimized memory handling
TestServiceProviderTagto tag testServiceProviders and allow services to detect whether they're running in test containers- New
MemoryReaderandSpanWriterhelpers inActualLab.Core.IO.Internal
Performance
- Improved RPC performance dedicated path for little endian serialization and delegate caching
- Eliminated
RpcPeer.Sendindirection layer -RpcTransport.Sendnow handles error handling directly, reducing call stack depth and improving RPC throughput - Introduced
TaskCompletionHandler- a pooled helper for attaching completion callbacks to tasks. Each instance caches its delegate, and instances are pooled using thread-static pools to minimize allocations in hot paths like RPC transport error handling - Enabled
UseUnsafeAccessorsbuild configuration for .NET 8+ targets, improving internal reflection-based operations performance
Fixed
- Big endian system support: serialization, RPC, and all dependent code now correctly use
BinaryPrimitives.WriteInt32LittleEndianand similar methods to ensure consistent byte ordering across different CPU architectures
Tests
- Added
ResetClientServices()calls to all relevant Fusion and RPC tests - Refactored RPC test organization for better separation of concerns.
Documentation
- Added interactive BarChart component for performance benchmarks visualization
- Improved Mermaid flowchart styles and edge label rendering
- Updated Performance page with new benchmark visualizations
- Integrated CHANGELOG into the documentation website
- Updated coding style guide to clarify async method naming conventions
12.0.9+3e71b6ef
Release date: 2026-01-27
Breaking Changes
- New
v6serialization format:mempack6,msgpack6and their variants withcsuffix - All serialization formats below
v5are gone, usev11.5.1if you still need them WebSocketChannelis replaced byRpcWebSocketTransport- All authentication-related types from
ActualLab.Fusion.Serverare moved toActualLab.Fusion.Ext.*assemblies and changed namespace fromActualLab.Fusion.Server.AuthenticationtoActualLab.Fusion.Authentication, so referencingActualLab.Fusion.Serverassembly now doesn't "drag" the authentication-related types into your project (and that was the reason for this change).
Added
mempack6andmsgpack6serialization format versions; they offer a tiny improvement (2 bytes per call) over v5, and that's only because the zero-copy serialization in RPC transport layer made v5 a bit less efficient (it has to reserve 5 bytes for the message length, because it uses WriteVarUInt32 for length, so v6 changes length encoding to regular UInt32).RpcStream.BatchSizeproperty for controlling stream batching behavior (default: 64, range: 1..1024)- Real-time stock ticker demo added to
TodoAppsample.
Changed
- Consolidated buffer size properties in
RpcWebSocketTransport(renamedWriteFrameSizetoFrameSize) - Very significant changes in
ActualLab.Rpcinternals. In particular, oldRpcMessageis now represented byRpcOutboundMessageandRpcInboundMessagetypes, all serialization-related methods now accept different arguments, etc.
Performance
- Zero-copy serialization in RPC transport layer. Earlier an intermediate buffer (
ArgumentData) was used to serialize RPC call arguments and results. LaterRpcMessage(envelope) serializer was combining that data with other required pieces (method reference, call ID, etc.) in the final buffer. Now the intermediate buffer is eliminated, which significantly boosts RPC performance on large messages.Stream10Ktest shows almost 3x speed improvement on 10KB items. - Switched
WriteChannelOptionstoUnboundedChannelOptions - Buffer renewal and reuse logic was significantly improved as well.
Documentation
- New documentation website: https://fusion.actuallab.net/
11.4.7+3045fd2c
Release date: 2025-01-05
Added
- Updated EntityFrameworkCore and Npgsql versions to 10.0. There is currently no MySql EF Core provider for EF10, so if you want to use Fusion with MySql, the latest version that targets EF9 is 11.4.3; you can also add binding redirects for EF9 manually in your project.
RpcMethodAttributefor method-level RPC configurationv5serialization formats with proper polymorphicnullvalue support, includingjson5,njson5,msgpack5,msgpack5c,mempackc, andmempack5c.- New
IRpcMiddlewarestack replacingIRpcInboundCallPreprocessor RpcLocalExecutionModeenum and newRpcLocalExecutionMode.Constrained,RpcLocalExecutionMode.ConstrainedEntrymodesIOperationEventSourceinterface withOperation.AddEvent(...)overload for event sourcingIWorker.Runoverload withCancellationToken.- Much more robust RPC (re)routing logic in
RpcInterceptor,RpcRoutingCommandHandler, andRemoteComputeMethodFunction.
Changed
- Moved all
RpcXxxdelegates toRpcXxxOptionsmembers for clarity. E.g.,RpcOutboundCallOptions.RouterFactoryreplacesRpcCallRouterdelegate. - Renamed
RpcShardRoutingModetoRpcLocalExecutionMode - Renamed
RpcDefaultSessionInboundCallPreprocessortoRpcDefaultSessionReplacer - Simplified
RpcSerializationFormatResolver(the legacy resolver for the "unspecified" format is gone) - Improved
RpcServiceDefandRpcMethodDefconstructors - Improved
ComputedOptionscaching - Updated .NET SDK to version 10.0.101.
Performance
- Multiple improvements in inbound call processing performance, such as handcrafted server invokers for most frequent RPC system calls like
$sys.Ok WebSocketChannel.OptionsgotReadMode, which can beBufferedorUnbuffered. The newUnbufferedmode allows reading directly fromWebSocketbypassingChannelReader, it's used by default now.GetUnsafeinGenericInstanceCacheto eliminate some unnecessary type casts- Overall, v11.4.X is ~5-10% faster on RPC benchmarks.
Documentation
- Migrated Parts 01-13 from the old tutorial, though only parts 01-03 are truly edited at this point
- Added TOCs to videos on Fusion and ActualLab.Rpc
- Added GitHub workflow for deploying documentation to GitHub Pages: https://fusion.actuallab.net/
- Documentation is a work in progress, and you're welcome to contribute!
Fixed
- Multiple issues related to RPC rerouting
RpcCommandHandlerrepeatedly sending commands to the server- A new bug in
FrameDelayers.MustDelaymethod (introduced in late v11.3.X), which effectively disabled RPC frame delaying - Use of incorrect Handshake index on some reconnection attempts – the issue was rare, but once it happened, it was blocking RPC reconnects for ~5 min.
Task.Resultusage is replaced with.GetAwaiter().GetResult()everywhere (it's faster and safer)$csys.Invalidatecalls (remote invalidation notifications) now triggerComputed.Invalidate(immediately: true)call rather than justComputed.Invalidate(), which eliminates double delay for RPC compute methods that use invalidation delay- Various minor fixes.
Tests
WebTestHelpers.GetUnusedLocalUrihelper method inActualLab.TestingCapturingLoggerandCapturingLoggerProviderinActualLab.Testing- Improved cancellation and timeout handling in RPC tests
- Added benchmark tests for
Task.ResultvsTask.GetAwaiter().GetResult() - Added
CapturingLoggerunit test
11.0.15+ec823882
Release date: 2025-11-05
Changed
- Returned back
IMutableState.Valuesetters (they were removed in v11.0.8) - Added "Must" prefix to
RpcDefaultCallTracer.TraceInboundandTraceOutboundflags for consistency with bool field naming rules.
Fixed
- Adjusted
RpcDefaultDelegates.CallTracerFactoryto enable full tracing for server (RuntimeInfo.IsServer), and no tracing for the client.
Changed in Samples
- TodoApp: Updated Aspire SDK to version 9.5.2 and centralized its version configuration
- TodoApp: Temporarily disabled Microsoft Account authentication (the current credentials are expired).
11.0.8+1fd1d61afb
Release date: 2025-11-02
Breaking Changes
- Revamped
RpcServiceBuilderAPI. Its newInjectmethod "injects" a service described byRpcServiceBuilderintoIServiceCollection RpcBuilderandFusionBuilder'sAddXxxmethods now rely onRpcServiceBuilder.InjectIMutableStateandMutableStatelostValuesetter; useSet(...)methods to set it. Invalidation path tracking is the reason of this change: new.Set(...)overloads use[CallerFilePath],[CallerMemberName], and[CallerLineNumber]to propagate the origin of change to the invalidation logic, and there is no way to achieve the same with.Valueproperty. I may end up returning it with[Obsolete]attribute though.- Renamed
IHasIsDisposedtoIHasDisposeStatus - Removed
RpcServiceMode.DistributedPairmode and related logic (Distributedmode offers more anyway) - Removed
RpcSwitchInterceptor;
Added
- Quite useful
ComputedOptions.ConsolidationDelayand related APIs (see Releases chat on https://voxt.ai/chat/s-1KCdcYy9z2-uJVPKZsbEo for details) - Invalidation path tracking:
InvalidationSource,Invalidation.TrackingMode, and other changes Computed.ToString(InvalidationSourceFormat)isComputed.ToString(), but with invalidation path infoFusionMonitoris now capable of gathering invalidation path statistics.
Changed
- More readable output of
MethodInfo.ToShortString()is now used to dump compute service methods
Fixed
- Another issue in
RpcCallTracker.TryReconnectthat may block stateful reconnect - A couple places in Fusion RPC stack where
Computed.Invalidate(immediately: false)was used instead ofComputed.Invalidate(immediately: true) - Incorrect use of
Sampler-s inFusionMonitor: missing "not" was making it to sample where it had to skip, and vice versa 😦 That's why statistics was typically 7x exaggerated there (the probability of loggingEveryNth(8)is 7/8, i.e. close to 1, but it was reporting it as 1/8).
10.6.38+254f4ef775
Release date: 2025-10-28
Changed
- Breaking: Replaced
RpcCallRouteOverridewithRpcCallOptions, expect more changes in this area - Removed
RpcNonRoutingInterceptorand related infrastructure;RpcRoutingInterceptordoes a bit more and equally fast - Updated CODING_STYLE.md
Fixed
RpcCallTracker.TryReconnect-IncreasingSeqCompressor.Serializewas getting a potentially misordered sequence making fast (stateful) reconnect impossible, though stateless reconnect still worked in these cases- Renamed
CompleteAsyncclass toCompletionto correct a previous wrong rename RpcRoutingInterceptornow properly reroutes local calls as well
Tests
- Added new MeshRpc tests; will be extending them in the near future
- Refactored
FusionTestBasedescendants to move DI of test-specific services to specific tests - Reorganized DB model classes under
DbModelnamespace in tests
10.6.18+af2a52320f
Fixed
- ActualLab.Generators now add #if-s suppressing [UnconditionalSuppressMessage] and [ModuleInitializer] for .NET Framework 4.7.2 and .NET Standard 2.0.
10.6.16+d0431715b7
Release date: 2025-10-27
Changed
- Renamed
ChannelReadModetoWebSocketChannelReadMode - Removed
IChannelWithReadMode, updatedWebSocketChannelto implementIAsyncEnumerabledirectly.
Performance
- Adjusted
BoundedChannelOptionsforReadChannel(120→100) andWriteChannel(120→500) inWebSocketChannelto optimize buffering behavior
Infrastructure
- Added
UnbufferedPushSequence<T>for unbuffered async data streaming - Added
[UnsafeAccessor]-based helpers forAsyncTaskMethodBuilder<T>inAsyncTaskMethodBuilderExt.
Documentation
- Replaced
.instructions.mdwith updatedAGENTS.mdandCODING_STYLE.md
10.6.4
Release date: 2025-10-25
Breaking Changes
- Breaking: Removed
FusionDefaultsandFusionModetypes. UseRuntimeInfo.IsServer,ComputedRegistry.Settings, andTimeouts.Settingsinstead. - Breaking: Removed
RpcModetype. UseRuntimeInfo.IsServerinstead. - Breaking:
ComputedRegistry.Instanceis gone, the whole class is now static - Breaking:
TimeoutsandIGenericTimeoutHandlerare moved toActualLab.Timenamespace. - Breaking: reworked
OperationEventAPI enabling events with quantized delay. Such events have Uuid, which includes its DelayUntil value, and have quantized DelayUntil. So you can use them to implement reliable throttling for such activities as post-change indexing or AI processing - Breaking:
IHasDelayUntilinterface is removed, and thus its support inOperationEventhandling logic. - Breaking: .NET Framework 4.7.1 target is replaced with .NET Framework 4.7.2
Added
- .NET 10 RC2 support
allowInconsistentflag inComputed/AnyState/ComputedSource.Use()and.UseUntyped()methods. Get the value even if it's inconsistent; when called inside a compute method, instantly invalidates the newly created computed if an inconsistent dependency gets captured.bool Operation.MustStoreproperty allowing to disable the operation log entry creation; this is useful when you're sure you don't need distributed invalidation for the current operation - e.g., you know that only the local machine is responsible for exposing modified data. WhenMustStoreisfalse, aDbEvent(inProcessedstate) is created instead of anDbOperationentry to verify commit in case of commit failure.OperationScope.CompletionHandlersallowing to register operation completion handler right inside the command handlerRpcCallRouteOverridetype allowing to override outgoing RPC call routing (e.g., set destination peer)Moment.Floor,Moment.Ceiling,Moment.Round, andMoment.Convertmethods;Moment.Ceilingis used to quantize delayed events.
Changed
- Moved command routing logic to
RpcRoutingCommandHandler - Renamed
ComputeServiceCommandCompletionInvalidatortoInvalidatingCommandCompletionHandler - Improvements in
RetryPolicy/IRetryPolicy, including exception filters ArgumentListG*<...>is now IL/AOT-trimmable viaArgumentList.AllowGenericsfeature switch
Fixed
- Native AOT support in .NET 10
- A bug in
ComputeRegistrythat could causeComputedRegistryto expose a wrongComputeddue to a race betweenComputedRegistry.GetandComputedRegistry.Register. IfGetgets paused between a moment it read a handle and resolved itsTarget, the handle with the sameIntPtrvalue might get re-allocated. This is extremely rare, but nevertheless, we saw this happening in production. - The identical bug in
RpcObjectTracker(it's used to trackRpcStream-s). IDelegatingCommandnow "implements"IOutermostCommand(it was supposed to, but didn't)StatefulComponent.StateChangedhandler now suppressesExecutionContextflow- Bug in
RpcSystemCallSender.OkbreakingRpcStreamserialization in call results - Bug in
RpcStreamenumerator - Proper framework version check in
CpuTimestampfor .NET 10 WASM - Removed
Microsoft.Extensions.Httpreference fromActualLab.Rpc
Performance
WebSocketChannel.ReadModeand corresponding option enabling unbuffered reads without use ofReader(which is backed by its ownChannel). This option alone improves RPC performance by 5%.- Use
CancellationToken.NoneinWebSocketoperations inWebSocketChannelwithout sacrificing cancellation support (WebSocketis properly disconnected on cancellation now) RpcStreamnow usesMemory<byte>instead ofbyte[]for its buffer.- Use of
Unsafe.As<T>(x)instead of(T)xin a few key places. - Improvements in key Fusion components, including
ComputedRegistry,Computed, andComputedState, - Improvements in Blazor components, including
StatefulComponentBaseandComputedStateComponentBase - Improvements in infrastructure components, including
ArgumentList,FixedArray,AsyncLock, andAsyncLockSet.
Infrastructure
- Added
WeakReferenceSlim(currently unused) - Added
TaskExt.NeverEnding(CancellationToken)helper - Added
FixedTimerSetandConcurrentFixedTimerSet - Improved
CancellationTokenSource-based timeout handling code across the board - Improved tests.
Documentation
- Added
.instructions.md(like AGENTS.md) for agent rules - Finalized Part01, added examples for
Computed<T>.When()andChanges()methods.
Key Changes in 2025
- Documentation website launch – https://fusion.actuallab.net/ with VitePress, migrated Parts 01–13 from the old tutorial, video TOCs, GitHub Pages deployment workflow.
- RPC routing and distributed services overhaul – new
RpcLocalExecutionMode,IRpcMiddlewarestack (replacingIRpcInboundCallPreprocessor),RpcRoutingCommandHandler,RpcCallOptions,RpcServiceBuilder.Inject, and much more robust rerouting logic for shard-aware distributed services. - Invalidation path tracking and consolidation –
InvalidationSource,Invalidation.TrackingMode,ComputedOptions.ConsolidationDelayfor grouping rapid invalidations, andFusionMonitorinvalidation path statistics. - .NET 10 support.
Key Changes in 2024
- RPC serialization formats revamp – introduced
mempack2/msgpack2binary formats with compact method name hashes (mempack2c/msgpack2c), serialization format negotiation, and zero-copy serialization in the transport layer.RpcByteMessageSerializerandFastRpcMessageByteSerializerbrought significant throughput gains. OperationEventintroduction – event handling withDbEventTestBase,- .NET 9 support and Native AOT – full .NET 9 support (RC1 → release), proxy generators updated with AOT-compatible code generation (
ProxyCodeKeeper,KeepCode), and[UnsafeAccessor]-based helpers for internal reflection. - .NET 9 support.
Key Changes in 2023
ActualLab.Rpc– new RPC framework – built from scratch starting April 2023, replacing the oldReplica/Publisher/Replicatorbridge with a modern WebSocket-based RPC system. IncludesRpcPeer,RpcStream,RpcHub, call routing, reconnection, shared object tracking, and binary serialization.- Compute caching (client-side) –
ClientComputeMethodFunctionimprovements andRpcCacheKeyfor caching RPC compute method results on the client side. - Blazor improvements –
ComputedState.Options.TryComputeSynchronouslyfor faster initial rendering, improvedComputedStateComponent,BlazorCircuitContextenhancements, andRpcPeerStateMonitorfor connection state UI. - Rename from
Stl.*toActualLab.*– all NuGet packages, namespaces, and project files renamed in December 2023 (e.g.,Stl.Fusion→ActualLab.Fusion,Stl.CommandR→ActualLab.CommandR). - .NET 8 support – full .NET 8 support including
[UnsafeAccessor]usage, IL trimming markup,[DynamicDependency]annotations, and AOT-related preparations.
Key Changes in 2022
- Roslyn proxy generators –
Stl.Generatorspackage withProxyGeneratorfor compile-time proxy generation via Roslyn source generators, replacing runtime Castle DynamicProxy for[ComputeMethod]and command service interception. - .NET 7 support – added .NET 7 targeting (RC2 → release), updated framework dependencies, and CI workflows.
Key Changes in 2021
- Binary serialization – initial
MessagePackserialization support,IByteSerializer/ITextSerializerabstractions, andTypeDecoratingSerializerfor polymorphic serialization. Refactored text serializers fromITextWriter/ITextReaderto simpler Read/Write overloads. - OpenTelemetry support – initial
System.Diagnostics.DiagnosticSourceintegration withActivitySource-based tracing for compute methods and command handlers. - .NET 6 support.
Key Changes in 2020
Year of project inception.
- Fusion core – established the foundational
Computed<T>,ComputeMethod,State/MutableState/LiveStateabstractions,ComputedRegistry, automatic dependency tracking, and invalidation pipeline. - CQRS / CommandR –
Stl.CommandRcommand processing pipeline withICommand<T>,CommandContext,CommandHandler, and middleware pipeline for orchestrating side-effect-producing operations alongside Fusion's read model. - Replica services and WebSocket bridge –
ReplicaService,Publisher/Replicatorbridge over WebSockets,SubscriptionProcessorfor managing live subscriptions. - Entity Framework integration –
Stl.Fusion.EntityFrameworkwithDbOperationScope,DbAuthService,DbSessionInfo, early multi-tenancy support, and PostgreSQL/MySQL/SQL Server provider compatibility. - .NET 5 and multi-targeting – migrated from .NET Core 3.1 to .NET 5 with multi-targeting.
