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
93 changes: 36 additions & 57 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,93 +5,72 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.0.0] - 2025-06-23

### Added
- Full async/await support for both server and client implementations
- `asyncMethodHandlers` dictionary in `ServiceDefinition` for async method registration
- Async method handling in `WebViewRpcServer` with automatic fallback to sync handlers
- Virtual-Virtual pattern in code generation templates for backward compatibility
## [1.0.1] - 2025-06-23

### Changed
- **BREAKING**: Generated server methods now use async pattern by default
- C# servers must implement `UniTask<Response> MethodNameAsync(Request request)`
- JavaScript servers must implement `async MethodNameAsync(request)`
- **BREAKING**: Generated client methods now have `Async` suffix
- C# clients call `await client.MethodNameAsync(request)`
- JavaScript clients call `await client.MethodNameAsync(request)`
- WebViewRpcServer now processes async handlers first, then falls back to sync handlers

### Migration Guide
- **BREAKING**: Complete redesign for async-only architecture
- **BREAKING**: Removed all synchronous method support
- **BREAKING**: Removed `Async` suffix from all method names
- **BREAKING**: Changed from Virtual-Virtual pattern to Abstract pattern
- Server implementations must now use `abstract` methods (mandatory override)
- Cleaner, more explicit async-only design
- Simplified codebase by removing dual sync/async handlers
- Improved type safety with abstract methods

### Removed
- Synchronous method handlers
- `AsyncMethodHandlers` dictionary (merged into single `MethodHandlers`)
- Virtual-Virtual pattern fallback mechanism
- `Async` suffix from all generated methods

### Migration Guide from v1.0.0

#### For C# Server Implementations

**Before (v0.x):**
**v1.0.0:**
```csharp
public class MyService : MyServiceBase
{
public override Response MyMethod(Request request)
public override async UniTask<Response> MyMethodAsync(Request request)
{
// Synchronous implementation
return new Response { ... };
// Implementation
}
}
```

**After (v1.0):**
**v1.0.1:**
```csharp
public class MyService : MyServiceBase
{
public override async UniTask<Response> MyMethodAsync(Request request)
public override async UniTask<Response> MyMethod(Request request)
{
// Asynchronous implementation
await UniTask.Delay(100);
return new Response { ... };
// Implementation (remove Async suffix)
}
}
```

#### For JavaScript Server Implementations

**Before (v0.x):**
```javascript
class MyService extends MyServiceBase {
MyMethod(request) {
// Synchronous implementation
return { ... };
}
}
```
#### For Client Code (C# and JavaScript)

**After (v1.0):**
```javascript
class MyService extends MyServiceBase {
async MyMethodAsync(request) {
// Asynchronous implementation
await someAsyncOperation();
return { ... };
}
}
**v1.0.0:**
```csharp
var response = await client.MyMethodAsync(request);
```

#### For Client Code

**Before (v0.x):**
**v1.0.1:**
```csharp
var response = await client.MyMethod(request);
```

**After (v1.0):**
```csharp
var response = await client.MyMethodAsync(request);
```
### Notes
- This version prioritizes clean, maintainable code over backward compatibility
- All RPC methods are now async-only by design
- Simpler mental model: one method = one async implementation

### Backward Compatibility
## [1.0.0] - 2025-06-23

The library maintains backward compatibility through the Virtual-Virtual pattern:
- Existing synchronous implementations will continue to work
- You can gradually migrate methods to async as needed
- The server automatically handles both sync and async methods
### Added
- Initial async/await support with Virtual-Virtual pattern
- Backward compatibility with synchronous methods

## [0.1.1] - Previous version
- Initial release with basic RPC functionality
14 changes: 8 additions & 6 deletions Packages/WebviewRpc/Runtime/ServiceDefinition.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Cysharp.Threading.Tasks;
using Google.Protobuf;

namespace WebViewRPC
{
/// <summary>
/// Set of 'MethodName' -> 'Handler' mappings
/// Represents a service definition with method handlers.
/// All methods are async by default.
/// </summary>
public class ServiceDefinition
{
public Dictionary<string, Func<ByteString, ByteString>> MethodHandlers
= new Dictionary<string, Func<ByteString, ByteString>>();
public Dictionary<string, Func<ByteString, UniTask<ByteString>>> AsyncMethodHandlers
= new Dictionary<string, Func<ByteString, UniTask<ByteString>>>();
/// <summary>
/// Dictionary mapping method names to their async handlers.
/// </summary>
public Dictionary<string, Func<ByteString, UniTask<ByteString>>> MethodHandlers { get; set; } =
new Dictionary<string, Func<ByteString, UniTask<ByteString>>>();
}
}
104 changes: 41 additions & 63 deletions Packages/WebviewRpc/Runtime/WebViewRpcClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Threading;
using Cysharp.Threading.Tasks;
using Google.Protobuf;
using UnityEngine;

namespace WebViewRPC
{
Expand All @@ -12,11 +13,8 @@ namespace WebViewRPC
public class WebViewRpcClient : IDisposable
{
private readonly IWebViewBridge _bridge;

// Mapping RequestId -> UniTaskCompletionSource
private readonly Dictionary<string, UniTaskCompletionSource<RpcEnvelope>> _pendingRequests
= new Dictionary<string, UniTaskCompletionSource<RpcEnvelope>>();

private readonly Dictionary<string, UniTaskCompletionSource<RpcEnvelope>> _pendingRequests = new();
private int _requestIdCounter = 1;
private bool _disposed;

public WebViewRpcClient(IWebViewBridge bridge)
Expand All @@ -30,56 +28,41 @@ public WebViewRpcClient(IWebViewBridge bridge)
/// User should not call this method directly.
/// Instead, use generated method from .proto file.
/// </summary>
public async UniTask<TResponse> CallMethodAsync<TResponse>(string methodName, IMessage request, CancellationToken cancellationToken = default)
public async UniTask<TResponse> CallMethod<TResponse>(string method, IMessage request)
where TResponse : IMessage<TResponse>, new()
{
if (_disposed) throw new ObjectDisposedException(nameof(WebViewRpcClient));

var requestId = Guid.NewGuid().ToString("N");

var utcs = new UniTaskCompletionSource<RpcEnvelope>();
lock (_pendingRequests)
var requestId = (_requestIdCounter++).ToString();
var envelope = new RpcEnvelope
{
_pendingRequests[requestId] = utcs;
}
RequestId = requestId,
IsRequest = true,
Method = method,
Payload = ByteString.CopyFrom(request.ToByteArray())
};

// Handle cancellation
using (cancellationToken.Register(() =>
{
lock (_pendingRequests)
{
if (_pendingRequests.Remove(requestId, out var cancelled))
{
cancelled.TrySetCanceled();
}
}
}))
var tcs = new UniTaskCompletionSource<RpcEnvelope>();
_pendingRequests[requestId] = tcs;

try
{
// (Protobuf -> byte[] -> Base64)
var requestBytes = request.ToByteArray();
var env = new RpcEnvelope
{
RequestId = requestId,
IsRequest = true,
Method = methodName,
Payload = ByteString.CopyFrom(requestBytes)
};
var envBytes = env.ToByteArray();
var envBase64 = Convert.ToBase64String(envBytes);
var bytes = envelope.ToByteArray();
var base64 = Convert.ToBase64String(bytes);
_bridge.SendMessageToWeb(base64);

// Send to WebView
_bridge.SendMessageToWeb(envBase64);

var responseEnv = await utcs.Task;
if (!string.IsNullOrEmpty(responseEnv.Error))
var responseEnvelope = await tcs.Task;

if (!string.IsNullOrEmpty(responseEnvelope.Error))
{
throw new Exception($"RPC Error: {responseEnv.Error}");
throw new Exception($"RPC Error: {responseEnvelope.Error}");
}

// Payload -> TResponse
var resp = new TResponse();
resp.MergeFrom(responseEnv.Payload);
return resp;
var response = new TResponse();
response.MergeFrom(responseEnvelope.Payload);
return response;
}
finally
{
_pendingRequests.Remove(requestId);
}
}

Expand All @@ -90,29 +73,16 @@ private void OnBridgeMessage(string base64)
try
{
var bytes = Convert.FromBase64String(base64);
var env = RpcEnvelope.Parser.ParseFrom(bytes);
var envelope = RpcEnvelope.Parser.ParseFrom(bytes);

if (!env.IsRequest)
if (!envelope.IsRequest && _pendingRequests.TryGetValue(envelope.RequestId, out var tcs))
{
HandleResponse(env);
tcs.TrySetResult(envelope);
}
}
catch (Exception ex)
{
throw new Exception($"Exception while handling message: {ex}");
}
}

private void HandleResponse(RpcEnvelope env)
{
UniTaskCompletionSource<RpcEnvelope> utcs = null;
lock (_pendingRequests)
{
_pendingRequests.Remove(env.RequestId, out utcs);
}
if (utcs != null)
{
utcs.TrySetResult(env);
Debug.LogError($"Error processing message: {ex}");
}
}

Expand All @@ -121,6 +91,14 @@ public void Dispose()
if (!_disposed)
{
_bridge.OnMessageReceived -= OnBridgeMessage;

// Cancel all pending requests
foreach (var pending in _pendingRequests.Values)
{
pending.TrySetCanceled();
}
_pendingRequests.Clear();

_disposed = true;
}
}
Expand Down
Loading