Skip to content

Commit 288dff0

Browse files
authored
Merge pull request #9 from kwan3854/v1.0.0
Adds async/await support to WebView RPC framework
2 parents 661bcb2 + 5022da9 commit 288dff0

File tree

14 files changed

+427
-79
lines changed

14 files changed

+427
-79
lines changed

CHANGELOG.md

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [1.0.0] - 2025-06-23
9+
10+
### Added
11+
- Full async/await support for both server and client implementations
12+
- `asyncMethodHandlers` dictionary in `ServiceDefinition` for async method registration
13+
- Async method handling in `WebViewRpcServer` with automatic fallback to sync handlers
14+
- Virtual-Virtual pattern in code generation templates for backward compatibility
15+
16+
### Changed
17+
- **BREAKING**: Generated server methods now use async pattern by default
18+
- C# servers must implement `UniTask<Response> MethodNameAsync(Request request)`
19+
- JavaScript servers must implement `async MethodNameAsync(request)`
20+
- **BREAKING**: Generated client methods now have `Async` suffix
21+
- C# clients call `await client.MethodNameAsync(request)`
22+
- JavaScript clients call `await client.MethodNameAsync(request)`
23+
- WebViewRpcServer now processes async handlers first, then falls back to sync handlers
24+
25+
### Migration Guide
26+
27+
#### For C# Server Implementations
28+
29+
**Before (v0.x):**
30+
```csharp
31+
public class MyService : MyServiceBase
32+
{
33+
public override Response MyMethod(Request request)
34+
{
35+
// Synchronous implementation
36+
return new Response { ... };
37+
}
38+
}
39+
```
40+
41+
**After (v1.0):**
42+
```csharp
43+
public class MyService : MyServiceBase
44+
{
45+
public override async UniTask<Response> MyMethodAsync(Request request)
46+
{
47+
// Asynchronous implementation
48+
await UniTask.Delay(100);
49+
return new Response { ... };
50+
}
51+
}
52+
```
53+
54+
#### For JavaScript Server Implementations
55+
56+
**Before (v0.x):**
57+
```javascript
58+
class MyService extends MyServiceBase {
59+
MyMethod(request) {
60+
// Synchronous implementation
61+
return { ... };
62+
}
63+
}
64+
```
65+
66+
**After (v1.0):**
67+
```javascript
68+
class MyService extends MyServiceBase {
69+
async MyMethodAsync(request) {
70+
// Asynchronous implementation
71+
await someAsyncOperation();
72+
return { ... };
73+
}
74+
}
75+
```
76+
77+
#### For Client Code
78+
79+
**Before (v0.x):**
80+
```csharp
81+
var response = await client.MyMethod(request);
82+
```
83+
84+
**After (v1.0):**
85+
```csharp
86+
var response = await client.MyMethodAsync(request);
87+
```
88+
89+
### Backward Compatibility
90+
91+
The library maintains backward compatibility through the Virtual-Virtual pattern:
92+
- Existing synchronous implementations will continue to work
93+
- You can gradually migrate methods to async as needed
94+
- The server automatically handles both sync and async methods
95+
96+
## [0.1.1] - Previous version
97+
- Initial release with basic RPC functionality

Packages/WebviewRpc/Runtime/ServiceDefinition.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using Cysharp.Threading.Tasks;
34
using Google.Protobuf;
45

56
namespace WebViewRPC
@@ -11,5 +12,8 @@ public class ServiceDefinition
1112
{
1213
public Dictionary<string, Func<ByteString, ByteString>> MethodHandlers
1314
= new Dictionary<string, Func<ByteString, ByteString>>();
15+
16+
public Dictionary<string, Func<ByteString, UniTask<ByteString>>> AsyncMethodHandlers
17+
= new Dictionary<string, Func<ByteString, UniTask<ByteString>>>();
1418
}
1519
}

Packages/WebviewRpc/Runtime/WebViewRpcClient.cs

Lines changed: 47 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.Threading.Tasks;
3+
using System.Threading;
4+
using Cysharp.Threading.Tasks;
45
using Google.Protobuf;
56

67
namespace WebViewRPC
@@ -12,9 +13,9 @@ public class WebViewRpcClient : IDisposable
1213
{
1314
private readonly IWebViewBridge _bridge;
1415

15-
// Mapping RequestId -> TaskCompletionSource
16-
private readonly Dictionary<string, TaskCompletionSource<RpcEnvelope>> _pendingRequests
17-
= new Dictionary<string, TaskCompletionSource<RpcEnvelope>>();
16+
// Mapping RequestId -> UniTaskCompletionSource
17+
private readonly Dictionary<string, UniTaskCompletionSource<RpcEnvelope>> _pendingRequests
18+
= new Dictionary<string, UniTaskCompletionSource<RpcEnvelope>>();
1819

1920
private bool _disposed;
2021

@@ -29,44 +30,57 @@ public WebViewRpcClient(IWebViewBridge bridge)
2930
/// User should not call this method directly.
3031
/// Instead, use generated method from .proto file.
3132
/// </summary>
32-
public async Task<TResponse> CallMethodAsync<TResponse>(string methodName, IMessage request)
33+
public async UniTask<TResponse> CallMethodAsync<TResponse>(string methodName, IMessage request, CancellationToken cancellationToken = default)
3334
where TResponse : IMessage<TResponse>, new()
3435
{
3536
if (_disposed) throw new ObjectDisposedException(nameof(WebViewRpcClient));
3637

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

39-
var tcs = new TaskCompletionSource<RpcEnvelope>();
40+
var utcs = new UniTaskCompletionSource<RpcEnvelope>();
4041
lock (_pendingRequests)
4142
{
42-
_pendingRequests[requestId] = tcs;
43+
_pendingRequests[requestId] = utcs;
4344
}
4445

45-
// (Protobuf -> byte[] -> Base64)
46-
var requestBytes = request.ToByteArray();
47-
var env = new RpcEnvelope
46+
// Handle cancellation
47+
using (cancellationToken.Register(() =>
4848
{
49-
RequestId = requestId,
50-
IsRequest = true,
51-
Method = methodName,
52-
Payload = ByteString.CopyFrom(requestBytes)
53-
};
54-
var envBytes = env.ToByteArray();
55-
var envBase64 = Convert.ToBase64String(envBytes);
56-
57-
// Send to WebView
58-
_bridge.SendMessageToWeb(envBase64);
59-
60-
var responseEnv = await tcs.Task;
61-
if (!string.IsNullOrEmpty(responseEnv.Error))
49+
lock (_pendingRequests)
50+
{
51+
if (_pendingRequests.Remove(requestId, out var cancelled))
52+
{
53+
cancelled.TrySetCanceled();
54+
}
55+
}
56+
}))
6257
{
63-
throw new Exception($"RPC Error: {responseEnv.Error}");
64-
}
58+
// (Protobuf -> byte[] -> Base64)
59+
var requestBytes = request.ToByteArray();
60+
var env = new RpcEnvelope
61+
{
62+
RequestId = requestId,
63+
IsRequest = true,
64+
Method = methodName,
65+
Payload = ByteString.CopyFrom(requestBytes)
66+
};
67+
var envBytes = env.ToByteArray();
68+
var envBase64 = Convert.ToBase64String(envBytes);
6569

66-
// Payload -> TResponse
67-
var resp = new TResponse();
68-
resp.MergeFrom(responseEnv.Payload);
69-
return resp;
70+
// Send to WebView
71+
_bridge.SendMessageToWeb(envBase64);
72+
73+
var responseEnv = await utcs.Task;
74+
if (!string.IsNullOrEmpty(responseEnv.Error))
75+
{
76+
throw new Exception($"RPC Error: {responseEnv.Error}");
77+
}
78+
79+
// Payload -> TResponse
80+
var resp = new TResponse();
81+
resp.MergeFrom(responseEnv.Payload);
82+
return resp;
83+
}
7084
}
7185

7286
private void OnBridgeMessage(string base64)
@@ -91,14 +105,14 @@ private void OnBridgeMessage(string base64)
91105

92106
private void HandleResponse(RpcEnvelope env)
93107
{
94-
TaskCompletionSource<RpcEnvelope> tcs = null;
108+
UniTaskCompletionSource<RpcEnvelope> utcs = null;
95109
lock (_pendingRequests)
96110
{
97-
_pendingRequests.Remove(env.RequestId, out tcs);
111+
_pendingRequests.Remove(env.RequestId, out utcs);
98112
}
99-
if (tcs != null)
113+
if (utcs != null)
100114
{
101-
tcs.TrySetResult(env);
115+
utcs.TrySetResult(env);
102116
}
103117
}
104118

Packages/WebviewRpc/Runtime/WebViewRpcServer.cs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using Cysharp.Threading.Tasks;
34
using Google.Protobuf;
45
using UnityEngine;
56

@@ -16,6 +17,7 @@ public class WebViewRpcServer : IDisposable
1617
public List<ServiceDefinition> Services { get; } = new();
1718

1819
private readonly Dictionary<string, Func<ByteString, ByteString>> _methodHandlers = new();
20+
private readonly Dictionary<string, Func<ByteString, UniTask<ByteString>>> _asyncMethodHandlers = new();
1921

2022
public WebViewRpcServer(IWebViewBridge bridge)
2123
{
@@ -34,10 +36,15 @@ public void Start()
3436
{
3537
_methodHandlers[kv.Key] = kv.Value;
3638
}
39+
40+
foreach (var kv in sd.AsyncMethodHandlers)
41+
{
42+
_asyncMethodHandlers[kv.Key] = kv.Value;
43+
}
3744
}
3845
}
3946

40-
private void OnBridgeMessage(string base64)
47+
private async void OnBridgeMessage(string base64)
4148
{
4249
if (_disposed) return;
4350

@@ -48,7 +55,7 @@ private void OnBridgeMessage(string base64)
4855

4956
if (env.IsRequest)
5057
{
51-
HandleRequest(env);
58+
await HandleRequestAsync(env);
5259
}
5360
}
5461
catch (Exception ex)
@@ -57,7 +64,7 @@ private void OnBridgeMessage(string base64)
5764
}
5865
}
5966

60-
private void HandleRequest(RpcEnvelope reqEnv)
67+
private async UniTask HandleRequestAsync(RpcEnvelope reqEnv)
6168
{
6269
var respEnv = new RpcEnvelope
6370
{
@@ -68,7 +75,14 @@ private void HandleRequest(RpcEnvelope reqEnv)
6875

6976
try
7077
{
71-
if (_methodHandlers.TryGetValue(reqEnv.Method, out var handler))
78+
// Check async handlers first
79+
if (_asyncMethodHandlers.TryGetValue(reqEnv.Method, out var asyncHandler))
80+
{
81+
var responsePayload = await asyncHandler(reqEnv.Payload);
82+
respEnv.Payload = ByteString.CopyFrom(responsePayload.ToByteArray());
83+
}
84+
// Fallback to sync handlers
85+
else if (_methodHandlers.TryGetValue(reqEnv.Method, out var handler))
7286
{
7387
var responsePayload = handler(reqEnv.Payload);
7488
respEnv.Payload = ByteString.CopyFrom(responsePayload.ToByteArray());

Packages/WebviewRpc/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "com.kwanjoong.webviewrpc",
3-
"version": "0.2.0",
3+
"version": "1.0.0",
44
"displayName": "Webview RPC",
55
"description": "The webview Remote Procedure Call bridge.",
66
"unity": "2022.3",

README.md

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,21 @@ npm install
7373
npm run build
7474
```
7575

76+
## What's New in v1.0.0
77+
78+
### Full Async/Await Support
79+
WebView RPC now supports full async/await patterns for both server and client implementations:
80+
81+
- **C# Integration**: Uses `UniTask` for better Unity performance
82+
- **JavaScript Integration**: Native `async/await` support
83+
- **Backward Compatibility**: Existing synchronous code continues to work through the Virtual-Virtual pattern
84+
85+
### Breaking Changes
86+
- Generated methods now have `Async` suffix (e.g., `SayHelloAsync`)
87+
- Server implementations must use async patterns
88+
89+
For detailed migration guide, see [CHANGELOG.md](CHANGELOG.md).
90+
7691
## Installation
7792

7893
### Adding WebView RPC to a Unity Project
@@ -373,18 +388,23 @@ public class WebViewRpcTester : MonoBehaviour
373388
```
374389

375390
```csharp
391+
using Cysharp.Threading.Tasks;
376392
using HelloWorld;
377393
using UnityEngine;
378394

379395
namespace SampleRpc
380396
{
381-
// Inherit HelloWorldService and implement the SayHello method.
382-
// HelloWorldService is generated from HelloWorld.proto.
397+
// Inherit HelloServiceBase and implement the SayHelloAsync method.
398+
// HelloServiceBase is generated from HelloWorld.proto.
383399
public class HelloWorldService : HelloServiceBase
384400
{
385-
public override HelloResponse SayHello(HelloRequest request)
401+
public override async UniTask<HelloResponse> SayHelloAsync(HelloRequest request)
386402
{
387403
Debug.Log($"Received request: {request.Name}");
404+
405+
// Example async operation
406+
await UniTask.Delay(100);
407+
388408
return new HelloResponse()
389409
{
390410
// Process the request and return a response
@@ -408,7 +428,8 @@ document.getElementById('btnSayHello').addEventListener('click', async () => {
408428
const reqObj = { name: "Hello World! From WebView" };
409429
console.log("Request to Unity: ", reqObj);
410430

411-
const resp = await helloClient.SayHello(reqObj);
431+
// Note: Method now has Async suffix
432+
const resp = await helloClient.SayHelloAsync(reqObj);
412433
console.log("Response from Unity: ", resp.greeting);
413434
} catch (err) {
414435
console.error("Error: ", err);
@@ -467,8 +488,8 @@ public class WebViewRpcTester : MonoBehaviour
467488
// Create a HelloServiceClient
468489
var client = new HelloServiceClient(rpcClient);
469490

470-
// Send a request
471-
var response = await client.SayHello(new HelloRequest()
491+
// Send a request (note the Async suffix)
492+
var response = await client.SayHelloAsync(new HelloRequest()
472493
{
473494
Name = "World"
474495
});
@@ -508,9 +529,12 @@ import { HelloServiceBase } from "./HelloWorld_HelloServiceBase.js";
508529

509530
// Inherit HelloServiceBase from the auto-generated HelloWorld_HelloServiceBase.js
510531
export class MyHelloServiceImpl extends HelloServiceBase {
511-
SayHello(requestObj) {
532+
async SayHelloAsync(requestObj) {
512533
// Check the incoming request
513534
console.log("JS Server received: ", requestObj);
535+
536+
// Example async operation
537+
await new Promise(resolve => setTimeout(resolve, 100));
514538

515539
// Process the request and return a response
516540
return {

0 commit comments

Comments
 (0)