Files
suitelinkclient/docs/plans/2026-03-17-runtime-reconnect-implementation-plan.md
2026-03-17 11:04:19 -04:00

18 KiB

SuiteLink Runtime Reconnect Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Add a background receive loop with automatic reconnect and subscription replay so the client continues dispatching updates after transport/session failures.

Architecture: The implementation extends SuiteLinkClient with a supervised runtime loop and reconnect flow while keeping durable subscription intent separate from ephemeral session mappings. Recovery rebuilds transport/session state, replays subscriptions, and resumes update dispatch without caller polling.

Tech Stack: .NET 10, C#, xUnit, SemaphoreSlim, CancellationTokenSource, existing SuiteLink codec/session/transport layers


Task 1: Add Durable Subscription Registry

Files:

  • Create: /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/Internal/SubscriptionRegistrationEntry.cs
  • Modify: /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs
  • Test: /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientSubscriptionRegistryTests.cs

Step 1: Write the failing test

[Fact]
public async Task SubscribeAsync_StoresDurableSubscriptionIntent()
{
    var client = TestClientFactory.CreateReadyClient();

    await client.SubscribeAsync("Pump001.Run", _ => { });

    Assert.True(client.HasSubscription("Pump001.Run"));
}

Step 2: Run test to verify it fails

Run: dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx --filter SuiteLinkClientSubscriptionRegistryTests -v minimal Expected: FAIL with missing durable registry behavior

Step 3: Write minimal implementation

Add a durable registry entry model storing:

  • ItemName
  • callback
  • requested tag id

Store these entries in SuiteLinkClient separately from SuiteLinkSession.

Step 4: Run test to verify it passes

Run: dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx --filter SuiteLinkClientSubscriptionRegistryTests -v minimal Expected: PASS

Step 5: Commit

git add /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/Internal/SubscriptionRegistrationEntry.cs /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientSubscriptionRegistryTests.cs
git commit -m "feat: add durable subscription registry"

Task 2: Make Subscription Handles Remove Durable Intent

Files:

  • Modify: /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs
  • Modify: /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SubscriptionHandle.cs
  • Test: /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientSubscriptionRegistryTests.cs

Step 1: Write the failing test

[Fact]
public async Task DisposingSubscription_RemovesDurableSubscriptionIntent()
{
    var client = TestClientFactory.CreateReadyClient();
    var handle = await client.SubscribeAsync("Pump001.Run", _ => { });

    await handle.DisposeAsync();

    Assert.False(client.HasSubscription("Pump001.Run"));
}

Step 2: Run test to verify it fails

Run: dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx --filter DisposingSubscription_RemovesDurableSubscriptionIntent -v minimal Expected: FAIL

Step 3: Write minimal implementation

Ensure handle disposal removes durable registry entries even when wire unadvise cannot be sent.

Step 4: Run test to verify it passes

Run: dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx --filter SuiteLinkClientSubscriptionRegistryTests -v minimal Expected: PASS

Step 5: Commit

git add /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SubscriptionHandle.cs /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientSubscriptionRegistryTests.cs
git commit -m "feat: persist subscription intent across reconnects"

Task 3: Add Runtime State For Background Loop

Files:

  • Modify: /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/Internal/SuiteLinkSessionState.cs
  • Modify: /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs
  • Test: /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientConnectionTests.cs

Step 1: Write the failing test

[Fact]
public async Task ConnectAsync_TransitionsToReadyOnlyAfterRuntimeStarts()
{
    var client = TestClientFactory.CreateReadyHandshakeClient();

    await client.ConnectAsync(TestOptions.Create());

    Assert.True(client.IsConnected);
}

Step 2: Run test to verify it fails

Run: dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx --filter ConnectAsync_TransitionsToReadyOnlyAfterRuntimeStarts -v minimal Expected: FAIL with missing ready/runtime state

Step 3: Write minimal implementation

Add:

  • Ready
  • Reconnecting

and transition ConnectAsync into Ready when the runtime loop has been established.

Step 4: Run test to verify it passes

Run: dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx --filter SuiteLinkClientConnectionTests -v minimal Expected: PASS

Step 5: Commit

git add /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/Internal/SuiteLinkSessionState.cs /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientConnectionTests.cs
git commit -m "feat: add ready and reconnecting runtime states"

Task 4: Start Background Receive Loop

Files:

  • Modify: /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs
  • Test: /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientRuntimeLoopTests.cs

Step 1: Write the failing test

[Fact]
public async Task ConnectAsync_StartsBackgroundLoop_AndDispatchesUpdateWithoutManualPolling()
{
    var updateReceived = new TaskCompletionSource<SuiteLinkTagUpdate>();
    var client = TestClientFactory.CreateClientWithQueuedUpdate(updateReceived);

    await client.ConnectAsync(TestOptions.Create());
    await client.SubscribeAsync("Pump001.Run", update => updateReceived.TrySetResult(update));

    var update = await updateReceived.Task.WaitAsync(TimeSpan.FromSeconds(1));
    Assert.True(update.Value.TryGetBoolean(out var value) && value);
}

Step 2: Run test to verify it fails

Run: dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx --filter SuiteLinkClientRuntimeLoopTests -v minimal Expected: FAIL because manual processing is still required

Step 3: Write minimal implementation

Start a long-lived receive loop task after initial connect, and dispatch updates through existing session logic.

Step 4: Run test to verify it passes

Run: dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx --filter SuiteLinkClientRuntimeLoopTests -v minimal Expected: PASS

Step 5: Commit

git add /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientRuntimeLoopTests.cs
git commit -m "feat: add suitelink background receive loop"

Task 5: Make ProcessIncomingAsync Internal Or Non-Primary

Files:

  • Modify: /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs
  • Modify: /Users/dohertj2/Desktop/suitelinkclient/README.md
  • Test: /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientRuntimeLoopTests.cs

Step 1: Write the failing documentation/runtime check

Define the intended runtime contract:

  • normal operation uses background receive
  • manual polling is not required for normal subscriptions

Step 2: Run targeted tests

Run: dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx --filter SuiteLinkClientRuntimeLoopTests -v minimal Expected: PASS after Task 4

Step 3: Write minimal implementation

Keep ProcessIncomingAsync only as an internal/test helper or document it as non-primary API.

Step 4: Run test and docs verification

Run: rg -n "background receive|manual polling|ProcessIncomingAsync" /Users/dohertj2/Desktop/suitelinkclient/README.md Expected: PASS with updated wording

Step 5: Commit

git add /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs /Users/dohertj2/Desktop/suitelinkclient/README.md
git commit -m "docs: describe background runtime model"

Task 6: Detect EOF And Trigger Reconnect

Files:

  • Modify: /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs
  • Test: /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientReconnectTests.cs

Step 1: Write the failing test

[Fact]
public async Task ReceiveLoop_Eof_TransitionsToReconnecting()
{
    var client = TestClientFactory.CreateClientThatEofsAfterConnect();

    await client.ConnectAsync(TestOptions.Create());

    await Eventually.AssertAsync(() => Assert.Equal(SuiteLinkSessionState.Reconnecting, client.DebugState));
}

Step 2: Run test to verify it fails

Run: dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx --filter SuiteLinkClientReconnectTests -v minimal Expected: FAIL

Step 3: Write minimal implementation

Treat ReceiveAsync == 0 as a disconnect trigger and start recovery.

Step 4: Run test to verify it passes

Run: dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx --filter SuiteLinkClientReconnectTests -v minimal Expected: PASS

Step 5: Commit

git add /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientReconnectTests.cs
git commit -m "feat: detect disconnects and enter reconnect state"

Task 7: Add Bounded Reconnect Backoff

Files:

  • Modify: /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs
  • Test: /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientReconnectTests.cs

Step 1: Write the failing test

[Fact]
public async Task Reconnect_UsesBoundedRetrySchedule()
{
    var delays = new List<TimeSpan>();
    var client = TestClientFactory.CreateReconnectTestClient(delays);

    await client.ConnectAsync(TestOptions.Create());

    Assert.Contains(TimeSpan.FromSeconds(1), delays);
}

Step 2: Run test to verify it fails

Run: dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx --filter Reconnect_UsesBoundedRetrySchedule -v minimal Expected: FAIL

Step 3: Write minimal implementation

Add a small capped delay schedule:

  • 0s
  • 1s
  • 2s
  • 5s
  • 10s capped

Inject delay behavior for tests if needed.

Step 4: Run test to verify it passes

Run: dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx --filter SuiteLinkClientReconnectTests -v minimal Expected: PASS

Step 5: Commit

git add /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientReconnectTests.cs
git commit -m "feat: add bounded reconnect backoff"

Task 8: Replay Subscriptions After Reconnect

Files:

  • Modify: /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs
  • Modify: /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/Internal/SuiteLinkSession.cs
  • Test: /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientReconnectTests.cs

Step 1: Write the failing test

[Fact]
public async Task Reconnect_ReplaysSubscriptions_AndRestoresDispatch()
{
    var callbackCount = 0;
    var client = TestClientFactory.CreateReconnectReplayClient(() => callbackCount++);

    await client.ConnectAsync(TestOptions.Create());
    await client.SubscribeAsync("Pump001.Run", _ => callbackCount++);

    await client.WaitForReconnectReadyAsync();
    Assert.True(callbackCount > 0);
}

Step 2: Run test to verify it fails

Run: dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx --filter Reconnect_ReplaysSubscriptions_AndRestoresDispatch -v minimal Expected: FAIL

Step 3: Write minimal implementation

On successful reconnect:

  • reset live session mappings
  • replay all durable subscriptions one-by-one
  • rebuild tag mappings from fresh ACKs
  • return to Ready

Step 4: Run test to verify it passes

Run: dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx --filter SuiteLinkClientReconnectTests -v minimal Expected: PASS

Step 5: Commit

git add /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/Internal/SuiteLinkSession.cs /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientReconnectTests.cs
git commit -m "feat: replay subscriptions after reconnect"

Task 9: Reject Writes During Reconnect

Files:

  • Modify: /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs
  • Test: /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientWriteTests.cs

Step 1: Write the failing test

[Fact]
public async Task WriteAsync_DuringReconnect_ThrowsClearException()
{
    var client = TestClientFactory.CreateReconnectingClient();

    await Assert.ThrowsAsync<InvalidOperationException>(() =>
        client.WriteAsync("Pump001.Run", SuiteLinkValue.FromBoolean(true)));
}

Step 2: Run test to verify it fails

Run: dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx --filter WriteAsync_DuringReconnect_ThrowsClearException -v minimal Expected: FAIL

Step 3: Write minimal implementation

Guard WriteAsync so it succeeds only in Ready.

Step 4: Run test to verify it passes

Run: dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx --filter SuiteLinkClientWriteTests -v minimal Expected: PASS

Step 5: Commit

git add /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientWriteTests.cs
git commit -m "feat: reject writes while reconnecting"

Task 10: Stop Runtime Cleanly On Disconnect

Files:

  • Modify: /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs
  • Test: /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientConnectionTests.cs

Step 1: Write the failing test

[Fact]
public async Task DisconnectAsync_StopsReceiveAndReconnectLoops()
{
    var client = TestClientFactory.CreateRunningClient();

    await client.ConnectAsync(TestOptions.Create());
    await client.DisconnectAsync();

    Assert.False(client.IsConnected);
}

Step 2: Run test to verify it fails

Run: dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx --filter DisconnectAsync_StopsReceiveAndReconnectLoops -v minimal Expected: FAIL

Step 3: Write minimal implementation

Cancel runtime loop tokens and stop reconnect attempts on disconnect/dispose.

Step 4: Run test to verify it passes

Run: dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx --filter SuiteLinkClientConnectionTests -v minimal Expected: PASS

Step 5: Commit

git add /Users/dohertj2/Desktop/suitelinkclient/src/SuiteLink.Client/SuiteLinkClient.cs /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientConnectionTests.cs
git commit -m "feat: stop runtime loops on disconnect"

Task 11: Update README And Integration Docs

Files:

  • Modify: /Users/dohertj2/Desktop/suitelinkclient/README.md
  • Modify: /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.IntegrationTests/README.md

Step 1: Write the failing documentation check

Define required README terms:

  • background receive loop
  • automatic reconnect
  • subscription replay
  • writes rejected during reconnect

Step 2: Run documentation review

Run: rg -n "background receive|automatic reconnect|subscription replay|reconnecting" /Users/dohertj2/Desktop/suitelinkclient/README.md /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.IntegrationTests/README.md Expected: FAIL until docs are updated

Step 3: Write minimal implementation

Update docs to describe the runtime model and recovery behavior honestly.

Step 4: Run documentation review

Run: rg -n "background receive|automatic reconnect|subscription replay|reconnecting" /Users/dohertj2/Desktop/suitelinkclient/README.md /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.IntegrationTests/README.md Expected: PASS

Step 5: Commit

git add /Users/dohertj2/Desktop/suitelinkclient/README.md /Users/dohertj2/Desktop/suitelinkclient/tests/SuiteLink.Client.IntegrationTests/README.md
git commit -m "docs: describe runtime reconnect behavior"

Task 12: Full Verification Pass

Files:

  • Modify: /Users/dohertj2/Desktop/suitelinkclient/docs/plans/2026-03-17-runtime-reconnect-design.md
  • Modify: /Users/dohertj2/Desktop/suitelinkclient/docs/plans/2026-03-17-runtime-reconnect-implementation-plan.md

Step 1: Run full test suite

Run: dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx -v minimal Expected: PASS with integration harness still conditional by default

Step 2: Run release build

Run: dotnet build /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx -c Release Expected: PASS

Step 3: Run reconnect-focused tests

Run: dotnet test /Users/dohertj2/Desktop/suitelinkclient/SuiteLink.Client.slnx --filter Reconnect -v minimal Expected: PASS

Step 4: Update plan notes if implementation deviated

Add short notes to the design/plan docs if final runtime behavior differs from original assumptions.

Step 5: Commit

git add /Users/dohertj2/Desktop/suitelinkclient/docs/plans/2026-03-17-runtime-reconnect-design.md /Users/dohertj2/Desktop/suitelinkclient/docs/plans/2026-03-17-runtime-reconnect-implementation-plan.md
git commit -m "docs: finalize reconnect implementation verification"