Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions .pubnub.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
name: c-sharp
version: "8.3.3"
version: "8.3.4"
schema: 1
scm: github.com/pubnub/c-sharp
changelog:
- date: 2026-07-29
version: v8.3.4
changes:
- type: bug
text: "Fixed issue related to Reconnect to retain subscription state."
- type: improvement
text: "Removed value cap from maxRetry in Linear/Exponential policies."
- type: improvement
text: "Emit status PNUnexpectedDisconnectCategory when retry attempts exhausted. Instead every retry attempt failures."
- date: 2026-07-20
version: v8.3.3
changes:
Expand Down Expand Up @@ -1009,14 +1018,14 @@ features:
- QUERY-PARAM
supported-platforms:
-
version: Pubnub 'C#' 8.3.3
version: Pubnub 'C#' 8.3.4
platforms:
- Windows 10 and up
- Windows Server 2008 and up
frameworks:
- .Net Framework 4.5+
-
version: PubnubPCL 'C#' 8.3.3
version: PubnubPCL 'C#' 8.3.4
platforms:
- Xamarin.Android
- Xamarin.iOS
Expand All @@ -1028,7 +1037,7 @@ supported-platforms:
frameworks:
- .Net 4.5+
-
version: PubnubUWP 'C#' 8.3.3
version: PubnubUWP 'C#' 8.3.4
platforms:
- Windows Phone 10
- Universal Windows Apps
Expand All @@ -1052,7 +1061,7 @@ sdks:
distribution-type: source
distribution-repository: GitHub
package-name: Pubnub
location: https://github.com/pubnub/c-sharp/releases/tag/v8.3.3
location: https://github.com/pubnub/c-sharp/releases/tag/v8.3.4
requires:
-
name: ".Net"
Expand Down Expand Up @@ -1293,7 +1302,7 @@ sdks:
distribution-type: source
distribution-repository: GitHub
package-name: PubNubPCL
location: https://github.com/pubnub/c-sharp/releases/tag/v8.3.3
location: https://github.com/pubnub/c-sharp/releases/tag/v8.3.4
requires:
-
name: ".Net"
Expand Down Expand Up @@ -1644,7 +1653,7 @@ sdks:
distribution-type: source
distribution-repository: GitHub
package-name: PubnubUWP
location: https://github.com/pubnub/c-sharp/releases/tag/v8.3.3
location: https://github.com/pubnub/c-sharp/releases/tag/v8.3.4
requires:
-
name: "Universal Windows Platform Development"
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
v8.3.4 - July 29 2026
-----------------------------
- Fixed: fixed issue related to Reconnect to retain subscription state.

- Modified: removed value cap from maxRetry in Linear/Exponential policies.
- Modified: emit status PNUnexpectedDisconnectCategory when retry attempts exhausted. Instead every retry attempt failures.

v8.3.3 - July 20 2026
-----------------------------
- Added: obscure exceptions thrown by cryptors to be available in logs only.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ public override TransitionResult Transition(IEvent e)
Events.HandshakeReconnectGiveUpEvent handshakeReconnectGiveUp => new HandshakeFailedState() {
Channels = this.Channels,
ChannelGroups = this.ChannelGroups,
// Preserve the cursor so a Reconnect() after handshake retries are exhausted
// resumes from the last known timetoken instead of dropping it.
Cursor = this.Cursor,
}.With(
new EmitStatusInvocation(new PNStatus(handshakeReconnectGiveUp.Status) )
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,16 +70,19 @@ public override TransitionResult Transition(IEvent e)
Cursor = this.Cursor,
AttemptedRetries = this.AttemptedRetries + 1,
Reason = receiveReconnectFailure.Status
}.With(new EmitStatusInvocation(PNStatusCategory.PNUnexpectedDisconnectCategory)),
},

Events.ReceiveReconnectGiveUpEvent receiveReconnectGiveUp => new ReceiveFailedState()
{
Channels = receiveReconnectGiveUp.Channels,
ChannelGroups = receiveReconnectGiveUp.ChannelGroups,
Cursor = receiveReconnectGiveUp.Cursor,

}.With(new EmitStatusInvocation(receiveReconnectGiveUp.Status)),
// Read from the current state, not the give-up event: the event is enqueued
// by ReceivingReconnectEffectHandler with only Status set (channels/groups/cursor
// are null), whereas 'this' preserves them across every retry. Carrying them here
// lets Reconnect() restore the subscription and resume from the last timetoken.
Channels = this.Channels,
ChannelGroups = this.ChannelGroups,
Cursor = this.Cursor,

}.With(new EmitStatusInvocation(PNStatusCategory.PNUnexpectedDisconnectCategory)),
_ => null
};
}
Expand Down
4 changes: 2 additions & 2 deletions src/Api/PubnubApi/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
[assembly: AssemblyProduct("Pubnub C# SDK")]
[assembly: AssemblyCopyright("Copyright © 2021")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyVersion("8.3.3")]
[assembly: AssemblyFileVersion("8.3.3")]
[assembly: AssemblyVersion("8.3.4")]
[assembly: AssemblyFileVersion("8.3.4")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
Expand Down
6 changes: 4 additions & 2 deletions src/Api/PubnubApi/PubnubApi.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,17 @@

<PropertyGroup>
<PackageId>Pubnub</PackageId>
<PackageVersion>8.3.3</PackageVersion>
<PackageVersion>8.3.4</PackageVersion>
<Title>PubNub C# .NET - Web Data Push API</Title>
<Authors>Pandu Masabathula</Authors>
<Owners>PubNub</Owners>
<PackageLicenseFile>LICENSE.txt</PackageLicenseFile>
<PackageIconUrl>http://pubnub.s3.amazonaws.com/2011/powered-by-pubnub/pubnub-icon-600x600.png</PackageIconUrl>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<RepositoryUrl>https://github.com/pubnub/c-sharp/</RepositoryUrl>
<PackageReleaseNotes>Obscure exceptions thrown by cryptors to be available in logs only.</PackageReleaseNotes>
<PackageReleaseNotes>Fixed issue related to Reconnect to retain subscription state.
Removed value cap from maxRetry in Linear/Exponential policies.
Emit status PNUnexpectedDisconnectCategory when retry attempts exhausted. Instead every retry attempt failures.</PackageReleaseNotes>
<PackageTags>Web Data Push Real-time Notifications ESB Message Broadcasting Distributed Computing</PackageTags>
<!--<Summary>PubNub is a Massively Scalable Web Push Service for Web and Mobile Games. This is a cloud-based service for broadcasting messages to thousands of web and mobile clients simultaneously</Summary>-->
<Description>PubNub is a Massively Scalable Web Push Service for Web and Mobile Games. This is a cloud-based service for broadcasting messages to thousands of web and mobile clients simultaneously</Description>
Expand Down
6 changes: 3 additions & 3 deletions src/Api/PubnubApi/RetryConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ internal class LinearRetryPolicy : IRetryPolicy
public LinearRetryPolicy(int delay, int maxRetry)
{
this.delay = Math.Max(2, delay);
this.maxRetry = Math.Min(10, maxRetry);
this.maxRetry = maxRetry;
}


Expand Down Expand Up @@ -65,14 +65,14 @@ public ExponentialRetryPolicy(int minDelay, int maxDelay, int maxRetry)
{
this.minDelay = Math.Max(2, minDelay);
this.maxDelay = Math.Min(150, maxDelay);
this.maxRetry = Math.Min(6, maxRetry);
this.maxRetry = maxRetry;
}

public int GetDelay(int attemptedRetries, PNStatus status, int? retryAfter)
{
if (status.StatusCode == 429 && retryAfter.HasValue && retryAfter > 0) return (int)retryAfter;
if (attemptedRetries == 0) return minDelay * 1000 + numGenerator.Next(1000);
return Math.Min((int)(Math.Pow(2, attemptedRetries) * 1000 + numGenerator.Next(1000)), maxDelay * 1000);
return (int)Math.Min(Math.Pow(2, attemptedRetries) * 1000 + numGenerator.Next(1000), maxDelay * 1000.0);
}

public bool ShouldRetry(int attemptedRetries, PNStatus status)
Expand Down
6 changes: 4 additions & 2 deletions src/Api/PubnubApiPCL/PubnubApiPCL.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,17 @@

<PropertyGroup>
<PackageId>PubnubPCL</PackageId>
<PackageVersion>8.3.3</PackageVersion>
<PackageVersion>8.3.4</PackageVersion>
<Title>PubNub C# .NET - Web Data Push API</Title>
<Authors>Pandu Masabathula</Authors>
<Owners>PubNub</Owners>
<PackageLicenseFile>LICENSE.txt</PackageLicenseFile>
<PackageIconUrl>http://pubnub.s3.amazonaws.com/2011/powered-by-pubnub/pubnub-icon-600x600.png</PackageIconUrl>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<RepositoryUrl>https://github.com/pubnub/c-sharp/</RepositoryUrl>
<PackageReleaseNotes>Obscure exceptions thrown by cryptors to be available in logs only.</PackageReleaseNotes>
<PackageReleaseNotes>Fixed issue related to Reconnect to retain subscription state.
Removed value cap from maxRetry in Linear/Exponential policies.
Emit status PNUnexpectedDisconnectCategory when retry attempts exhausted. Instead every retry attempt failures.</PackageReleaseNotes>
<PackageTags>Web Data Push Real-time Notifications ESB Message Broadcasting Distributed Computing</PackageTags>
<!--<Summary>PubNub is a Massively Scalable Web Push Service for Web and Mobile Games. This is a cloud-based service for broadcasting messages to thousands of web and mobile clients simultaneously</Summary>-->
<Description>PubNub is a Massively Scalable Web Push Service for Web and Mobile Games. This is a cloud-based service for broadcasting messages to thousands of web and mobile clients simultaneously</Description>
Expand Down
6 changes: 4 additions & 2 deletions src/Api/PubnubApiUWP/PubnubApiUWP.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,17 @@

<PropertyGroup>
<PackageId>PubnubUWP</PackageId>
<PackageVersion>8.3.3</PackageVersion>
<PackageVersion>8.3.4</PackageVersion>
<Title>PubNub C# .NET - Web Data Push API</Title>
<Authors>Pandu Masabathula</Authors>
<Owners>PubNub</Owners>
<PackageLicenseFile>LICENSE.txt</PackageLicenseFile>
<PackageIconUrl>http://pubnub.s3.amazonaws.com/2011/powered-by-pubnub/pubnub-icon-600x600.png</PackageIconUrl>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<RepositoryUrl>https://github.com/pubnub/c-sharp/</RepositoryUrl>
<PackageReleaseNotes>Obscure exceptions thrown by cryptors to be available in logs only.</PackageReleaseNotes>
<PackageReleaseNotes>Fixed issue related to Reconnect to retain subscription state.
Removed value cap from maxRetry in Linear/Exponential policies.
Emit status PNUnexpectedDisconnectCategory when retry attempts exhausted. Instead every retry attempt failures.</PackageReleaseNotes>
<PackageTags>Web Data Push Real-time Notifications ESB Message Broadcasting Distributed Computing</PackageTags>
<!--<Summary>PubNub is a Massively Scalable Web Push Service for Web and Mobile Games. This is a cloud-based service for broadcasting messages to thousands of web and mobile clients simultaneously</Summary>-->
<Description>PubNub is a Massively Scalable Web Push Service for Web and Mobile Games. This is a cloud-based service for broadcasting messages to thousands of web and mobile clients simultaneously</Description>
Expand Down
2 changes: 1 addition & 1 deletion src/Api/PubnubApiUnity/PubnubApiUnity.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

<PropertyGroup>
<PackageId>PubnubApiUnity</PackageId>
<PackageVersion>8.3.3</PackageVersion>
<PackageVersion>8.3.4</PackageVersion>
<Title>PubNub C# .NET - Web Data Push API</Title>
<Authors>Pandu Masabathula</Authors>
<Owners>PubNub</Owners>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,45 @@ public void HandshakeReconnectingState_OnEvent_TransitionToHandshakingState(

private HandshakeReconnectingState CreateHandshakeReconnectingState()
{
return new HandshakeReconnectingState()
{
Channels = new string[] { "ch1", "ch2" },
return new HandshakeReconnectingState()
{
Channels = new string[] { "ch1", "ch2" },
ChannelGroups = new string[] { "cg1", "cg2" } ,
};
}

[Test]
public void HandshakeReconnectingState_OnHandshakeReconnectGiveupEvent_PreservesCursorInHandshakeFailedState()
{
// Regression: the cursor must survive give-up so a Reconnect() after handshake retries
// are exhausted resumes from the last known timetoken instead of dropping it.
//Arrange
var currentState = new HandshakeReconnectingState()
{
Channels = new string[] { "ch1", "ch2" },
ChannelGroups = new string[] { "cg1", "cg2" },
Cursor = new SubscriptionCursor() { Region = 1, Timetoken = 1234567890 }
};
var eventToTriggerTransition = new HandshakeReconnectGiveUpEvent() { };
var expectedState = new HandshakeFailedState()
{
Channels = new string[] { "ch1", "ch2" },
ChannelGroups = new string[] { "cg1", "cg2" },
Cursor = new SubscriptionCursor() { Region = 1, Timetoken = 1234567890 }
};

//Act
var result = currentState.Transition(@eventToTriggerTransition);

//Assert
Assert.IsInstanceOf<HandshakeFailedState>(result.State);
CollectionAssert.AreEqual(expectedState.Channels, ((HandshakeFailedState)result.State).Channels);
CollectionAssert.AreEqual(expectedState.ChannelGroups, ((HandshakeFailedState)result.State).ChannelGroups);
Assert.IsNotNull(((HandshakeFailedState)result.State).Cursor);
Assert.AreEqual(expectedState.Cursor.Region, ((HandshakeFailedState)result.State).Cursor.Region);
Assert.AreEqual(expectedState.Cursor.Timetoken, ((HandshakeFailedState)result.State).Cursor.Timetoken);
}

[Test]
public void HandshakeReconnectingState_OnHandshakeReconnectFailureEvent_TransitionToHandshakeReconnectingState()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,61 @@ public void ReceiveReconnectingState_OnReceiveReconnectGiveupEvent_TransitionToR
Assert.AreEqual(expectedState.Cursor.Region, ((ReceiveFailedState)result.State).Cursor.Region);
Assert.AreEqual(expectedState.Cursor.Timetoken, ((ReceiveFailedState)result.State).Cursor.Timetoken);
Assert.IsInstanceOf<EmitStatusInvocation>(result.Invocations.ElementAt(0));
Assert.AreEqual(PNStatusCategory.PNUnknownCategory, ((EmitStatusInvocation)result.Invocations.ElementAt(0)).StatusCategory);
Assert.AreEqual(PNStatusCategory.PNUnexpectedDisconnectCategory, ((EmitStatusInvocation)result.Invocations.ElementAt(0)).StatusCategory);
}

[Test]
public void ReceiveReconnectingState_OnReceiveReconnectGiveupEvent_EmitsUnexpectedDisconnectStatus()
{
// The give-up transition always emits a PNUnexpectedDisconnectCategory status,
// regardless of the category carried on the give-up event's own Status.
//Arrange
var currentState = CreateReceiveReconnectingState();
var eventToTriggerTransition = new ReceiveReconnectGiveUpEvent()
{
Status = new PNStatus(null, PNOperationType.PNSubscribeOperation, PNStatusCategory.PNUnknownCategory)
};

//Act
var result = currentState.Transition(eventToTriggerTransition);

//Assert
Assert.IsInstanceOf<ReceiveFailedState>(result.State);
Assert.IsInstanceOf<EmitStatusInvocation>(result.Invocations.ElementAt(0));
Assert.AreEqual(PNStatusCategory.PNUnexpectedDisconnectCategory, ((EmitStatusInvocation)result.Invocations.ElementAt(0)).StatusCategory);
}

[Test]
public void ReceiveReconnectingState_OnReceiveReconnectGiveupEventWithEmptyEventData_TransitionToReceiveFailedStatePreservingStateData()
{
// Regression: the give-up event enqueued by ReceivingReconnectEffectHandler only sets
// Status; its Channels/ChannelGroups/Cursor are null. The failed state must therefore
// carry the reconnecting state's own channels/groups/cursor so that a subsequent
// Reconnect() can restore the subscription and resume from the last timetoken.
//Arrange
var currentState = CreateReceiveReconnectingState();
var eventToTriggerTransition = new ReceiveReconnectGiveUpEvent()
{
// Channels, ChannelGroups and Cursor intentionally left null, mirroring the real event.
Status = new PNStatus(null, PNOperationType.PNSubscribeOperation, PNStatusCategory.PNUnexpectedDisconnectCategory)
};
var expectedState = new ReceiveFailedState()
{
Channels = new string[] { "ch1", "ch2" },
ChannelGroups = new string[] { "cg1", "cg2" },
Cursor = new SubscriptionCursor() { Region = 1, Timetoken = 1234567890 }
};

//Act
var result = currentState.Transition(eventToTriggerTransition);

//Assert
Assert.IsInstanceOf<ReceiveFailedState>(result.State);
CollectionAssert.AreEqual(expectedState.Channels, ((ReceiveFailedState)result.State).Channels);
CollectionAssert.AreEqual(expectedState.ChannelGroups, ((ReceiveFailedState)result.State).ChannelGroups);
Assert.IsNotNull(((ReceiveFailedState)result.State).Cursor);
Assert.AreEqual(expectedState.Cursor.Region, ((ReceiveFailedState)result.State).Cursor.Region);
Assert.AreEqual(expectedState.Cursor.Timetoken, ((ReceiveFailedState)result.State).Cursor.Timetoken);
}

[Test]
Expand Down
Loading
Loading