diff --git a/CHANGELOG.md b/CHANGELOG.md index 85a8871..93fa131 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 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 MyMethodAsync(Request request) { - // Synchronous implementation - return new Response { ... }; + // Implementation } } ``` -**After (v1.0):** +**v1.0.1:** ```csharp public class MyService : MyServiceBase { - public override async UniTask MyMethodAsync(Request request) + public override async UniTask 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 \ No newline at end of file diff --git a/Packages/WebviewRpc/Runtime/ServiceDefinition.cs b/Packages/WebviewRpc/Runtime/ServiceDefinition.cs index 669c54e..f24676b 100644 --- a/Packages/WebviewRpc/Runtime/ServiceDefinition.cs +++ b/Packages/WebviewRpc/Runtime/ServiceDefinition.cs @@ -1,19 +1,21 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Cysharp.Threading.Tasks; using Google.Protobuf; namespace WebViewRPC { /// - /// Set of 'MethodName' -> 'Handler' mappings + /// Represents a service definition with method handlers. + /// All methods are async by default. /// public class ServiceDefinition { - public Dictionary> MethodHandlers - = new Dictionary>(); - - public Dictionary>> AsyncMethodHandlers - = new Dictionary>>(); + /// + /// Dictionary mapping method names to their async handlers. + /// + public Dictionary>> MethodHandlers { get; set; } = + new Dictionary>>(); } } \ No newline at end of file diff --git a/Packages/WebviewRpc/Runtime/WebViewRpcClient.cs b/Packages/WebviewRpc/Runtime/WebViewRpcClient.cs index d13f47a..d89c55b 100644 --- a/Packages/WebviewRpc/Runtime/WebViewRpcClient.cs +++ b/Packages/WebviewRpc/Runtime/WebViewRpcClient.cs @@ -3,6 +3,7 @@ using System.Threading; using Cysharp.Threading.Tasks; using Google.Protobuf; +using UnityEngine; namespace WebViewRPC { @@ -12,11 +13,8 @@ namespace WebViewRPC public class WebViewRpcClient : IDisposable { private readonly IWebViewBridge _bridge; - - // Mapping RequestId -> UniTaskCompletionSource - private readonly Dictionary> _pendingRequests - = new Dictionary>(); - + private readonly Dictionary> _pendingRequests = new(); + private int _requestIdCounter = 1; private bool _disposed; public WebViewRpcClient(IWebViewBridge bridge) @@ -30,56 +28,41 @@ public WebViewRpcClient(IWebViewBridge bridge) /// User should not call this method directly. /// Instead, use generated method from .proto file. /// - public async UniTask CallMethodAsync(string methodName, IMessage request, CancellationToken cancellationToken = default) + public async UniTask CallMethod(string method, IMessage request) where TResponse : IMessage, new() { - if (_disposed) throw new ObjectDisposedException(nameof(WebViewRpcClient)); - - var requestId = Guid.NewGuid().ToString("N"); - - var utcs = new UniTaskCompletionSource(); - 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(); + _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); } } @@ -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 utcs = null; - lock (_pendingRequests) - { - _pendingRequests.Remove(env.RequestId, out utcs); - } - if (utcs != null) - { - utcs.TrySetResult(env); + Debug.LogError($"Error processing message: {ex}"); } } @@ -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; } } diff --git a/Packages/WebviewRpc/Runtime/WebViewRpcServer.cs b/Packages/WebviewRpc/Runtime/WebViewRpcServer.cs index 6e1f287..c6f66b6 100644 --- a/Packages/WebviewRpc/Runtime/WebViewRpcServer.cs +++ b/Packages/WebviewRpc/Runtime/WebViewRpcServer.cs @@ -16,8 +16,7 @@ public class WebViewRpcServer : IDisposable /// public List Services { get; } = new(); - private readonly Dictionary> _methodHandlers = new(); - private readonly Dictionary>> _asyncMethodHandlers = new(); + private readonly Dictionary>> _methodHandlers = new(); public WebViewRpcServer(IWebViewBridge bridge) { @@ -30,16 +29,11 @@ public WebViewRpcServer(IWebViewBridge bridge) /// public void Start() { - foreach (var sd in Services) + foreach (var service in Services) { - foreach (var kv in sd.MethodHandlers) + foreach (var handler in service.MethodHandlers) { - _methodHandlers[kv.Key] = kv.Value; - } - - foreach (var kv in sd.AsyncMethodHandlers) - { - _asyncMethodHandlers[kv.Key] = kv.Value; + _methodHandlers[handler.Key] = handler.Value; } } } @@ -51,11 +45,11 @@ private async 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) { - await HandleRequestAsync(env); + await HandleRequestAsync(envelope); } } catch (Exception ex) @@ -64,42 +58,35 @@ private async void OnBridgeMessage(string base64) } } - private async UniTask HandleRequestAsync(RpcEnvelope reqEnv) + private async UniTask HandleRequestAsync(RpcEnvelope requestEnvelope) { - var respEnv = new RpcEnvelope + var responseEnvelope = new RpcEnvelope { - RequestId = reqEnv.RequestId, + RequestId = requestEnvelope.RequestId, IsRequest = false, - Method = reqEnv.Method, + Method = requestEnvelope.Method, }; try { - // Check async handlers first - if (_asyncMethodHandlers.TryGetValue(reqEnv.Method, out var asyncHandler)) - { - var responsePayload = await asyncHandler(reqEnv.Payload); - respEnv.Payload = ByteString.CopyFrom(responsePayload.ToByteArray()); - } - // Fallback to sync handlers - else if (_methodHandlers.TryGetValue(reqEnv.Method, out var handler)) + if (_methodHandlers.TryGetValue(requestEnvelope.Method, out var handler)) { - var responsePayload = handler(reqEnv.Payload); - respEnv.Payload = ByteString.CopyFrom(responsePayload.ToByteArray()); + var responsePayload = await handler(requestEnvelope.Payload); + responseEnvelope.Payload = responsePayload; } else { - respEnv.Error = $"Unknown method: {reqEnv.Method}"; + responseEnvelope.Error = $"Unknown method: {requestEnvelope.Method}"; } } catch (Exception ex) { - respEnv.Error = ex.Message; + responseEnvelope.Error = ex.Message; } - var respBytes = respEnv.ToByteArray(); - var respBase64 = Convert.ToBase64String(respBytes); - _bridge.SendMessageToWeb(respBase64); + var responseBytes = responseEnvelope.ToByteArray(); + var responseBase64 = Convert.ToBase64String(responseBytes); + _bridge.SendMessageToWeb(responseBase64); } public void Dispose() diff --git a/Packages/WebviewRpc/package.json b/Packages/WebviewRpc/package.json index bf1c8a6..87a63b9 100644 --- a/Packages/WebviewRpc/package.json +++ b/Packages/WebviewRpc/package.json @@ -1,6 +1,6 @@ { "name": "com.kwanjoong.webviewrpc", - "version": "1.0.0", + "version": "1.0.1", "displayName": "Webview RPC", "description": "The webview Remote Procedure Call bridge.", "unity": "2022.3", diff --git a/README.md b/README.md index 5174905..344bc32 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,18 @@ Unity WebView RPC provides an abstraction layer that allows communication betwee It extends the traditional `JavaScript bridge` communication method to work similarly to a Remote Procedure Call (RPC). To avoid dependency on a specific WebView library, it provides a Bridge interface so that communication can be achieved with the same code, regardless of the WebView library used. +## What's New in v1.0.1 + +### Clean Async-Only Architecture +WebView RPC v1.0.1 has been completely redesigned for a cleaner, async-only architecture: + +- **Abstract Pattern**: Server implementations must now override abstract methods +- **No Async Suffix**: Method names are clean without the `Async` suffix +- **Simplified Design**: Removed all synchronous method support +- **Better Type Safety**: Abstract methods ensure compile-time safety + +For detailed migration guide, see [CHANGELOG.md](CHANGELOG.md). + ## Architecture WebView RPC simplifies the workflow compared to the traditional `JavaScript bridge` method. @@ -73,21 +85,6 @@ npm install npm run build ``` -## What's New in v1.0.0 - -### Full Async/Await Support -WebView RPC now supports full async/await patterns for both server and client implementations: - -- **C# Integration**: Uses `UniTask` for better Unity performance -- **JavaScript Integration**: Native `async/await` support -- **Backward Compatibility**: Existing synchronous code continues to work through the Virtual-Virtual pattern - -### Breaking Changes -- Generated methods now have `Async` suffix (e.g., `SayHelloAsync`) -- Server implementations must use async patterns - -For detailed migration guide, see [CHANGELOG.md](CHANGELOG.md). - ## Installation ### Adding WebView RPC to a Unity Project @@ -172,7 +169,7 @@ Download the latest release from the [WebViewRPC Code Generator repository](http HelloWorld is a simple RPC service that receives a `HelloRequest` message and returns a `HelloResponse` message. In this example, we will implement HelloWorld and verify communication between the Unity client and the WebView client. -The HelloWorld service takes a `HelloRequest` and returns a `HelloResponse`. First, let’s look at the example where the C# side acts as the server and the JavaScript side acts as the client. +The HelloWorld service takes a `HelloRequest` and returns a `HelloResponse`. First, let's look at the example where the C# side acts as the server and the JavaScript side acts as the client. ### Defining the protobuf File @@ -265,7 +262,7 @@ protoc \ - The bridge code mediates communication between C# and JavaScript. - WebViewRpc is abstracted so it can be used with any WebView library. - Implement the bridge code according to your chosen WebView library. -- Below is an example using Viewplex’s CanvasWebViewPrefab. +- Below is an example using Viewplex's CanvasWebViewPrefab. ```csharp using System; @@ -335,7 +332,7 @@ export class VuplexBridge { if (this._isVuplexReady && window.vuplex) { window.vuplex.postMessage(base64Str); } else { - // If vuplex isn’t ready yet, store messages in a queue + // If vuplex isn't ready yet, store messages in a queue this._pendingMessages.push(base64Str); } } @@ -394,11 +391,11 @@ using UnityEngine; namespace SampleRpc { - // Inherit HelloServiceBase and implement the SayHelloAsync method. + // Inherit HelloServiceBase and implement the SayHello method. // HelloServiceBase is generated from HelloWorld.proto. public class HelloWorldService : HelloServiceBase { - public override async UniTask SayHelloAsync(HelloRequest request) + public override async UniTask SayHello(HelloRequest request) { Debug.Log($"Received request: {request.Name}"); @@ -428,8 +425,7 @@ document.getElementById('btnSayHello').addEventListener('click', async () => { const reqObj = { name: "Hello World! From WebView" }; console.log("Request to Unity: ", reqObj); - // Note: Method now has Async suffix - const resp = await helloClient.SayHelloAsync(reqObj); + const resp = await helloClient.SayHello(reqObj); console.log("Response from Unity: ", resp.greeting); } catch (err) { console.error("Error: ", err); @@ -488,8 +484,8 @@ public class WebViewRpcTester : MonoBehaviour // Create a HelloServiceClient var client = new HelloServiceClient(rpcClient); - // Send a request (note the Async suffix) - var response = await client.SayHelloAsync(new HelloRequest() + // Send a request + var response = await client.SayHello(new HelloRequest() { Name = "World" }); @@ -529,7 +525,7 @@ import { HelloServiceBase } from "./HelloWorld_HelloServiceBase.js"; // Inherit HelloServiceBase from the auto-generated HelloWorld_HelloServiceBase.js export class MyHelloServiceImpl extends HelloServiceBase { - async SayHelloAsync(requestObj) { + async SayHello(requestObj) { // Check the incoming request console.log("JS Server received: ", requestObj); diff --git a/webview_rpc_runtime_library/CHANGELOG.md b/webview_rpc_runtime_library/CHANGELOG.md index 85a8871..93fa131 100644 --- a/webview_rpc_runtime_library/CHANGELOG.md +++ b/webview_rpc_runtime_library/CHANGELOG.md @@ -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 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 MyMethodAsync(Request request) { - // Synchronous implementation - return new Response { ... }; + // Implementation } } ``` -**After (v1.0):** +**v1.0.1:** ```csharp public class MyService : MyServiceBase { - public override async UniTask MyMethodAsync(Request request) + public override async UniTask 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 \ No newline at end of file diff --git a/webview_rpc_runtime_library/package.json b/webview_rpc_runtime_library/package.json index 3faebee..0984496 100644 --- a/webview_rpc_runtime_library/package.json +++ b/webview_rpc_runtime_library/package.json @@ -1,6 +1,6 @@ { "name": "app-webview-rpc", - "version": "1.0.0", + "version": "1.0.1", "type": "module", "description": "WebView RPC provides an abstraction layer that allows communication between the App (e.g. Unity C#) and WebView (HTML, JS) using protobuf, similar to gRPC.", "main": "./dist/cjs/index.js", diff --git a/webview_rpc_runtime_library/src/core/service_definition.js b/webview_rpc_runtime_library/src/core/service_definition.js index cc8309c..28038b5 100644 --- a/webview_rpc_runtime_library/src/core/service_definition.js +++ b/webview_rpc_runtime_library/src/core/service_definition.js @@ -1,12 +1,14 @@ +/** + * Service definition for WebView RPC + * All methods are async by default + */ export class ServiceDefinition { constructor() { - // methodName -> handlerFn - // handlerFn: (requestBytes: Uint8Array) => Uint8Array - this.methodHandlers = {}; - - // methodName -> asyncHandlerFn - // asyncHandlerFn: (requestBytes: Uint8Array) => Promise - this.asyncMethodHandlers = {}; + /** + * Dictionary mapping method names to their async handlers + * @type {Object.>} + */ + this.methodHandlers = {}; } - } +} \ No newline at end of file diff --git a/webview_rpc_runtime_library/src/core/webview_rpc_client.js b/webview_rpc_runtime_library/src/core/webview_rpc_client.js index a82e298..b4102c6 100644 --- a/webview_rpc_runtime_library/src/core/webview_rpc_client.js +++ b/webview_rpc_runtime_library/src/core/webview_rpc_client.js @@ -1,84 +1,98 @@ -import { decodeEnvelopeFromBase64, encodeEnvelopeToBase64 } from "./webview_rpc_utils.js"; +import { decodeRpcEnvelope, encodeRpcEnvelope } from './rpc_envelope.js'; +import { base64ToUint8Array, uint8ArrayToBase64 } from './webview_rpc_utils.js'; +/** + * WebView RPC Client + * Makes RPC calls to Unity server + */ export class WebViewRpcClient { - /** - * @param {IJsBridge} bridge - */ - constructor(bridge) { - this.bridge = bridge; - this._disposed = false; + constructor(bridge) { + this._bridge = bridge; + this._pendingRequests = new Map(); + this._requestIdCounter = 1; + this._disposed = false; - // requestId -> { resolve, reject } - this.pendingMap = new Map(); - - this.bridge.onMessage((base64Str) => this._onBridgeMessage(base64Str)); - } - - /** - * JS -> 서버 RPC 호출 - * @param {string} methodName - * @param {Uint8Array} requestBytes - * @returns {Promise} responseBytes - */ - callMethod(methodName, requestBytes) { - if (this._disposed) { - return Promise.reject(new Error("RpcClient disposed")); + // Listen for responses from Unity + this._bridge.onMessage((base64Message) => { + this._handleMessage(base64Message); + }); } - return new Promise((resolve, reject) => { - const requestId = generateRequestId(); - this.pendingMap.set(requestId, { resolve, reject }); + /** + * Call a remote method + * @param {string} method - Method name + * @param {Uint8Array} requestPayload - Request payload + * @returns {Promise} Response payload + */ + async callMethod(method, requestPayload) { + if (this._disposed) { + throw new Error('RpcClient is disposed'); + } - const envelopeObj = { - requestId, - isRequest: true, - method: methodName, - payload: requestBytes, - error: "" - }; + const requestId = String(this._requestIdCounter++); + + const envelope = { + requestId, + isRequest: true, + method, + payload: requestPayload, + error: null + }; - const base64Str = encodeEnvelopeToBase64(envelopeObj); - this.bridge.sendMessage(base64Str); - }); - } + // Create promise for this request + const promise = new Promise((resolve, reject) => { + this._pendingRequests.set(requestId, { resolve, reject }); + }); - _onBridgeMessage(base64Str) { - if (this._disposed) return; + // Send request to Unity + const bytes = encodeRpcEnvelope(envelope); + const base64 = uint8ArrayToBase64(bytes); + this._bridge.sendMessage(base64); - let env; - try { - env = decodeEnvelopeFromBase64(base64Str); - } catch (ex) { - console.warn("[WebViewRpcClient] decode error:", ex); - return; + return promise; } - if (env.isRequest) { - // 클라이언트 입장 -> Request는 무시 - return; - } + /** + * Handle incoming message from Unity + * @param {string} base64Message + */ + _handleMessage(base64Message) { + if (this._disposed) return; - // 응답 처리 - const { requestId, payload, error } = env; - const pending = this.pendingMap.get(requestId); - if (!pending) return; - this.pendingMap.delete(requestId); + try { + const bytes = base64ToUint8Array(base64Message); + const envelope = decodeRpcEnvelope(bytes); - if (error) { - pending.reject(new Error(error)); - } else { - pending.resolve(payload); + if (!envelope.isRequest) { + // Handle response + const pending = this._pendingRequests.get(envelope.requestId); + if (pending) { + this._pendingRequests.delete(envelope.requestId); + + if (envelope.error) { + pending.reject(new Error(envelope.error)); + } else { + pending.resolve(envelope.payload); + } + } + } + } catch (error) { + console.error('Error handling message:', error); + } } - } - dispose() { - if (!this._disposed) { - this._disposed = true; - // onMessage 해제 등 + /** + * Dispose the client + */ + dispose() { + if (!this._disposed) { + this._disposed = true; + + // Reject all pending requests + for (const pending of this._pendingRequests.values()) { + pending.reject(new Error('Client disposed')); + } + this._pendingRequests.clear(); + } } - } -} - -function generateRequestId() { - return Math.random().toString(36).slice(2) + Date.now().toString(36); } diff --git a/webview_rpc_runtime_library/src/core/webview_rpc_server.js b/webview_rpc_runtime_library/src/core/webview_rpc_server.js index d2a80c2..cb3c2cd 100644 --- a/webview_rpc_runtime_library/src/core/webview_rpc_server.js +++ b/webview_rpc_runtime_library/src/core/webview_rpc_server.js @@ -1,102 +1,95 @@ -import { decodeEnvelopeFromBase64, encodeEnvelopeToBase64 } from "./webview_rpc_utils.js"; -import { ServiceDefinition } from "./service_definition.js"; +import { decodeRpcEnvelope, encodeRpcEnvelope } from './rpc_envelope.js'; +import { base64ToUint8Array, uint8ArrayToBase64 } from './webview_rpc_utils.js'; +/** + * WebView RPC Server + * Handles incoming RPC requests from Unity + */ export class WebViewRpcServer { - /** - * @param {IJsBridge} bridge - { sendMessage(str), onMessage(cb) } - */ - constructor(bridge) { - this.bridge = bridge; - - this.services = []; // Array - this.methodHandlers = {}; // 최종 (methodName -> function(bytes) => bytes) - this.asyncMethodHandlers = {}; // 최종 (methodName -> async function(bytes) => Promise) - - this._started = false; - this._disposed = false; - - // 수신 이벤트 - this.bridge.onMessage((base64Str) => this._onBridgeMessage(base64Str)); - } - - start() { - if (this._started) return; - this._started = true; - - // Services 배열을 쭉 돌면서, 모든 methodHandler를 합침 - for (const svcDef of this.services) { - for (const [methodName, handlerFn] of Object.entries(svcDef.methodHandlers)) { - this.methodHandlers[methodName] = handlerFn; - } - - // asyncMethodHandlers도 합침 - for (const [methodName, asyncHandlerFn] of Object.entries(svcDef.asyncMethodHandlers || {})) { - this.asyncMethodHandlers[methodName] = asyncHandlerFn; - } + constructor(bridge) { + this._bridge = bridge; + this._services = []; + this._methodHandlers = {}; + + // Listen for messages from Unity + this._bridge.onMessage((base64Message) => { + this._handleMessage(base64Message); + }); } - } - async _onBridgeMessage(base64Str) { - if (this._disposed) return; + /** + * Add a service to the server + * @param {ServiceDefinition} service + */ + addService(service) { + this._services.push(service); + } - let env; - try { - env = decodeEnvelopeFromBase64(base64Str); - } catch (ex) { - console.warn("Exception while parsing envelope:", ex); - return; + /** + * Start the server and register all method handlers + */ + start() { + // Merge all service handlers into one map + for (const service of this._services) { + for (const [methodName, handler] of Object.entries(service.methodHandlers)) { + this._methodHandlers[methodName] = handler; + } + } } - if (!env.isRequest) { - // 응답이면 서버 입장에선 무시 - return; + /** + * Handle incoming message from Unity + * @param {string} base64Message + */ + async _handleMessage(base64Message) { + try { + const bytes = base64ToUint8Array(base64Message); + const envelope = decodeRpcEnvelope(bytes); + + if (envelope.isRequest) { + await this._handleRequest(envelope); + } + } catch (error) { + console.error('Error handling message:', error); + } } - // 요청 처리 - const { requestId, method, payload } = env; // payload = Uint8Array - const respEnv = { - requestId, - isRequest: false, - method, - payload: new Uint8Array(), // 기본값 - error: "" - }; + /** + * Handle RPC request + * @param {Object} requestEnvelope + */ + async _handleRequest(requestEnvelope) { + const responseEnvelope = { + requestId: requestEnvelope.requestId, + isRequest: false, + method: requestEnvelope.method, + payload: null, + error: null + }; - // 먼저 asyncMethodHandlers를 확인 - const asyncHandler = this.asyncMethodHandlers[method]; - if (asyncHandler) { - try { - // asyncHandler( requestBytes ) => Promise - const responseBytes = await asyncHandler(payload); - respEnv.payload = responseBytes; - } catch (ex) { - respEnv.error = ex.message || String(ex); - } - } else { - // 동기 핸들러 폴백 - const handler = this.methodHandlers[method]; - if (!handler) { - respEnv.error = `Unknown method: ${method}`; - } else { try { - // handler( requestBytes ) => responseBytes - const responseBytes = handler(payload); - respEnv.payload = responseBytes; - } catch (ex) { - respEnv.error = ex.message || String(ex); + const handler = this._methodHandlers[requestEnvelope.method]; + if (handler) { + const responsePayload = await handler(requestEnvelope.payload); + responseEnvelope.payload = responsePayload; + } else { + responseEnvelope.error = `Unknown method: ${requestEnvelope.method}`; + } + } catch (error) { + responseEnvelope.error = error.message || 'Internal server error'; } - } - } - // 응답 전송 - const respBase64 = encodeEnvelopeToBase64(respEnv); - this.bridge.sendMessage(respBase64); - } + // Send response back to Unity + const responseBytes = encodeRpcEnvelope(responseEnvelope); + const responseBase64 = uint8ArrayToBase64(responseBytes); + this._bridge.sendMessage(responseBase64); + } - dispose() { - if (!this._disposed) { - // 실제론 onMessage 해제 등 - this._disposed = true; + /** + * Legacy property for compatibility + * @deprecated Use addService() instead + */ + get services() { + return this._services; } - } }