Skip to content

Instantly share code, notes, and snippets.

@WoLfulus
Last active May 18, 2022 01:55
Show Gist options
  • Save WoLfulus/068ab49923cfe598bdd3eecedb17bc86 to your computer and use it in GitHub Desktop.
Save WoLfulus/068ab49923cfe598bdd3eecedb17bc86 to your computer and use it in GitHub Desktop.
Bridge
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
using Vuplex.WebView;
namespace UI.Interop
{
#pragma warning disable CS8632
public class Event
{
[JsonRequired]
[JsonProperty("name")]
public string Name { get; set; }
[JsonRequired]
[JsonProperty("data")]
public JToken Data { get; set; }
}
public class Call
{
[JsonRequired]
[JsonProperty("id")]
public string Id { get; set; }
[JsonRequired]
[JsonProperty("method")]
public string Method { get; set; }
[JsonRequired]
[JsonProperty("params")]
public JToken Params { get; set; }
}
public class Error
{
[JsonProperty("code")]
public int? Code { get; set; }
[JsonRequired]
[JsonProperty("message")]
public string Message { get; set; }
}
public class Result
{
[JsonRequired]
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("error")]
public Error? Error { get; set; }
[JsonProperty("result")]
public JToken Value { get; set; }
}
public class Bridge
{
private delegate object InvokeDelegate(object[] @params);
private delegate void ResultDelegate(JToken result, Error? error);
private delegate void EventDelegate(JToken value);
private IWebView view;
private readonly Dictionary<string, ResultDelegate> pending = new();
private readonly Dictionary<string, InvokeDelegate> methods = new();
private readonly Dictionary<string, List<EventDelegate>> events = new();
public void Initialize(IWebView view)
{
this.view = view;
this.view.MessageEmitted += OnMessage;
}
public void Set<T>(string name, T value)
{
this.view.ExecuteJavaScript($"window.bridge.set({JsonConvert.SerializeObject(name)}, {JsonConvert.SerializeObject(value)})");
}
public void On(string name, Action action)
{
if (!this.events.TryGetValue(name, out List<EventDelegate> events))
{
events = this.events[name] = new List<EventDelegate>();
}
events.Add((JToken value) => action());
}
public void On<T>(string name, Action<T> action)
{
if (!this.events.TryGetValue(name, out List<EventDelegate> events))
{
events = this.events[name] = new List<EventDelegate>();
}
events.Add((JToken value) => action(value.ToObject<T>()));
}
public void Procedure(string name, Action action)
{
this.RegisterDelegate(name, action);
}
public void Procedure<T1>(string name, Action<T1> action)
{
this.RegisterDelegate(name, action);
}
public void Procedure<T1, T2>(string name, Action<T1, T2> action)
{
this.RegisterDelegate(name, action);
}
public void Procedure<T1, T2, T3>(string name, Action<T1, T2, T3> action)
{
this.RegisterDelegate(name, action);
}
public void Procedure<T1, T2, T3, T4>(string name, Action<T1, T2, T3, T4> action)
{
this.RegisterDelegate(name, action);
}
public void Procedure<T1, T2, T3, T4, T5>(string name, Action<T1, T2, T3, T4, T5> action)
{
this.RegisterDelegate(name, action);
}
public void Procedure<T1, T2, T3, T4, T5, T6>(string name, Action<T1, T2, T3, T4, T5, T6> action)
{
this.RegisterDelegate(name, action);
}
public void Procedure<T1, T2, T3, T4, T5, T6, T7>(string name, Action<T1, T2, T3, T4, T5, T6, T7> action)
{
this.RegisterDelegate(name, action);
}
public void Procedure<T1, T2, T3, T4, T5, T6, T7, T8>(string name, Action<T1, T2, T3, T4, T5, T6, T7, T8> action)
{
this.RegisterDelegate(name, action);
}
public void Procedure<T1, T2, T3, T4, T5, T6, T7, T8, T9>(string name, Action<T1, T2, T3, T4, T5, T6, T7, T8, T9> action)
{
this.RegisterDelegate(name, action);
}
public void Procedure<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>(string name, Action<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10> action)
{
this.RegisterDelegate(name, action);
}
public void Procedure<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11>(string name, Action<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11> action)
{
this.RegisterDelegate(name, action);
}
public void Procedure<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12>(string name, Action<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12> action)
{
this.RegisterDelegate(name, action);
}
public void Procedure<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13>(string name, Action<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13> action)
{
this.RegisterDelegate(name, action);
}
public void Procedure<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14>(string name, Action<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14> action)
{
this.RegisterDelegate(name, action);
}
public void Procedure<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>(string name, Action<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> action)
{
this.RegisterDelegate(name, action);
}
public void Procedure<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16>(string name, Action<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16> action)
{
this.RegisterDelegate(name, action);
}
public void Function<R>(string name, Func<R> action)
{
this.RegisterDelegate(name, action);
}
public void Function<T1, R>(string name, Func<T1, R> action)
{
this.RegisterDelegate(name, action);
}
public void Function<T1, T2, R>(string name, Func<T1, T2, R> action)
{
this.RegisterDelegate(name, action);
}
public void Function<T1, T2, T3, R>(string name, Func<T1, T2, T3, R> action)
{
this.RegisterDelegate(name, action);
}
public void Function<T1, T2, T3, T4, R>(string name, Func<T1, T2, T3, T4, R> action)
{
this.RegisterDelegate(name, action);
}
public void Function<T1, T2, T3, T4, T5, R>(string name, Func<T1, T2, T3, T4, T5, R> action)
{
this.RegisterDelegate(name, action);
}
public void Function<T1, T2, T3, T4, T5, T6, R>(string name, Func<T1, T2, T3, T4, T5, T6, R> action)
{
this.RegisterDelegate(name, action);
}
public void Function<T1, T2, T3, T4, T5, T6, T7, R>(string name, Func<T1, T2, T3, T4, T5, T6, T7, R> action)
{
this.RegisterDelegate(name, action);
}
public void Function<T1, T2, T3, T4, T5, T6, T7, T8, R>(string name, Func<T1, T2, T3, T4, T5, T6, T7, T8, R> action)
{
this.RegisterDelegate(name, action);
}
public void Function<T1, T2, T3, T4, T5, T6, T7, T8, T9, R>(string name, Func<T1, T2, T3, T4, T5, T6, T7, T8, T9, R> action)
{
this.RegisterDelegate(name, action);
}
public void Function<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, R>(string name, Func<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, R> action)
{
this.RegisterDelegate(name, action);
}
public void Function<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, R>(string name, Func<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, R> action)
{
this.RegisterDelegate(name, action);
}
public void Function<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, R>(string name, Func<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, R> action)
{
this.RegisterDelegate(name, action);
}
public void Function<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, R>(string name, Func<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, R> action)
{
this.RegisterDelegate(name, action);
}
public void Function<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, R>(string name, Func<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, R> action)
{
this.RegisterDelegate(name, action);
}
public void Function<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, R>(string name, Func<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, R> action)
{
this.RegisterDelegate(name, action);
}
public void Function<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, R>(string name, Func<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, R> action)
{
this.RegisterDelegate(name, action);
}
public Task Invoke(string method, params object[] @params)
{
var id = Guid.NewGuid().ToString();
var task = new TaskCompletionSource<bool>();
var payload = new JObject
{
{ "type", "call" },
{ "data",
new JObject {
{ "id", id },
{ "method", method },
{ "params", new JArray(@params) }
}
}
};
pending[id] = (JToken _, Error? error) => {
this.Resolve(task, error);
pending.Remove(id);
};
this.Dispatch(payload);
return task.Task;
}
public Task<T?> Invoke<T>(string method, params object[] @params)
{
var id = Guid.NewGuid().ToString();
var task = new TaskCompletionSource<T?>();
var payload = new JObject
{
{ "type", "call" },
{ "data",
new JObject {
{ "id", id },
{ "method", method },
{ "params", new JArray(@params) }
}
}
};
pending[id] = (JToken token, Error? error) => {
this.Resolve<T>(task, token, error);
pending.Remove(id);
};
this.Dispatch(payload);
return task.Task;
}
public void Emit<T>(string name, T data)
{
var value = JToken.FromObject(data);
var payload = new JObject
{
{ "type", "event" },
{ "data",
new JObject {
{ "name", name },
{ "data", value }
}
}
};
this.Dispatch(payload);
}
private void OnMessage(object sender, EventArgs<string> e)
{
this.Receive(e.Value);
}
private void Receive(string json)
{
var message = JObject.Parse(json);
if (message == null)
{
throw new Exception("Invalid UI message received");
}
var type = message["type"].ToString();
if (type == "result")
{
var data = JsonConvert.DeserializeObject<Result>(message["data"].ToString());
if (data == null)
{
throw new Exception("Failed to deserialize call message");
}
if (!this.pending.TryGetValue(data.Id, out ResultDelegate handler))
{
throw new Exception($"Unknown call '{data.Id}'");
}
handler(data.Value, data.Error);
}
else if (type == "call")
{
var data = JsonConvert.DeserializeObject<Call>(message["data"].ToString());
if (data == null)
{
throw new Exception("Failed to deserialize call message");
}
if (!this.methods.TryGetValue(data.Method, out InvokeDelegate handler))
{
this.DispatchError(data.Id, new Exception($"Unknown method: {data.Method}"));
return;
}
try
{
var result = handler(data.Params.ToObject<object[]>());
this.DispatchResult(data.Id, result);
}
catch (Exception ex)
{
this.DispatchError(data.Id, ex);
}
}
else if (type == "event")
{
var data = JsonConvert.DeserializeObject<Event>(message["data"].ToString());
if (data == null)
{
throw new Exception("Failed to deserialize call message");
}
if (!this.events.TryGetValue(data.Name, out List<EventDelegate> events))
{
return;
}
foreach (var e in events)
{
try
{
e.Invoke(data.Data);
}
catch (Exception)
{
}
}
}
else
{
Debug.LogWarning($"Unhandled Message: {json}");
}
}
private void RegisterDelegate(string name, Delegate delg)
{
this.methods[name] = (object[] @params) =>
{
return delg.DynamicInvoke(@params);
};
}
private void Resolve(TaskCompletionSource<bool> task, Error? error)
{
try
{
if (error != null)
{
task.SetException(new Exception(error.Message));
}
else
{
task.SetResult(true);
}
}
catch (Exception ex)
{
task.SetException(new Exception(ex.Message));
}
}
private void Resolve<T>(TaskCompletionSource<T?> task, JToken token, Error? error)
{
try
{
if (error != null)
{
task.SetException(new Exception(error.Message));
}
else
{
task.SetResult(token.ToObject<T>());
}
}
catch (Exception ex)
{
task.SetException(new Exception(ex.Message));
}
}
private void DispatchResult(string id, object result)
{
this.Dispatch(new JObject {
{ "type", "result" },
{ "data",
new JObject {
{ "id", id },
{ "result", JToken.FromObject(result) }
}
}
});
}
private void DispatchError(string id, Exception exception)
{
this.Dispatch(new JObject {
{ "type", "result" },
{ "data",
new JObject {
{ "id", id },
{ "error",
new JObject {
{ "message", exception.Message }
}
}
}
}
});
}
private void Dispatch(object data)
{
var json = JsonConvert.SerializeObject(data);
this.view.ExecuteJavaScript($"vuplex._emit('message', JSON.stringify({json}))", null);
}
}
#pragma warning restore CS8632
}
class Bridge {
constructor() {
this.initialized = false;
this.events = new EventTarget();
this.variables = {};
this.methods = {};
this.pending = {};
}
on(event, handler) {
this.events.addEventListener(event, handler);
return () => {
this.events.removeEventListener(event, handler);
};
}
method(message, handler) {
this.methods[message] = handler;
}
set(name, value) {
if (name in this.variables) {
if (value === this.variables[name]) {
return;
}
}
this.variables[name] = value;
this.events.dispatchEvent(new CustomEvent('engine_variable_' + name, { bubbles: true, detail: value }));
}
get(name, value = undefined) {
if (name in this.variables) {
return this.variables[name];
} else {
return value;
}
}
async invoke(method, ...params) {
return this._dispatch_call(method, params);
}
emit(name, data) {
this._dispatch_event(name, data);
}
ready(handler) {
if (this.initialized) {
handler();
} else {
this.events.addEventListener('ready', handler);
}
}
initialize() {
this.initialized = true;
window.vuplex.addEventListener('message', async (message) => await this.receive(message));
this.events.dispatchEvent(new CustomEvent('ready', { bubbles: true }));
}
async receive(message) {
try {
message = JSON.parse(message);
} catch (error) {
console.error(`Invalid message received`);
console.error({ message, error });
return;
}
const { type, data } = message;
switch (type)
{
case "call":
return await this._handle_call(data.id, data.method, data.params);
case "result":
return await this._handle_result(data.id, data.result, data.error);
case "event":
return await this._handle_event(data.name, data.data);
default:
throw new Error(`Invalid message type: ${type}`);
}
}
async _handle_result(id, result, error) {
if (!(id in this.pending)) {
throw new Error(`Unknown call ${id}`);
}
const promise = this.pending[id];
if (error) {
promise.reject(new Error(error.message));
} else {
promise.resolve(result);
}
delete this.pending[id];
}
async _handle_call(id, method, params) {
if (method in this.methods) {
let result;
try {
result = await this.methods[method].apply(this, [...params]);
} catch (err) {
return this._dispatch_error(id, err);
}
this._dispatch_result(id, result);
} else {
this._dispatch_error(id, new Error(`Method "${method}" not found`));
}
}
async _handle_event(name, data) {
this.events.dispatchEvent(new CustomEvent(name, { bubbles: true, detail: data }));
}
async _dispatch_call(method, params) {
if (!window.vuplex) {
throw new Error('Host is not initialized');
}
const id = self.crypto.randomUUID();
let resolver, rejector;
const promise = new Promise((resolve, reject) => {
resolver = resolve;
rejector = reject;
});
promise.resolve = resolver;
promise.reject = rejector;
this.pending[id] = promise;
window.vuplex.postMessage({
type: "call",
data: {
id,
method,
params
}
});
return promise;
}
_dispatch_result(id, result) {
if (!window.vuplex) {
throw new Error('Host is not initialized');
}
window.vuplex.postMessage({
type: "result",
data: {
id,
result
}
});
}
_dispatch_event(name, data) {
if (!window.vuplex) {
throw new Error('Host is not initialized');
}
window.vuplex.postMessage({
type: "event",
data: {
name,
data
},
});
}
_dispatch_error(id, error) {
if (!window.vuplex) {
throw new Error('Host is not initialized');
}
window.vuplex.postMessage({
type: "result",
data: {
id,
error: {
code: error.code,
message: error.message,
},
}
});
}
};
const bridge = global.bridge = window.bridge = document.bridge = new Bridge();
if (window.vuplex) {
bridge.initialize();
} else {
window.addEventListener('vuplexready', function() {
bridge.initialize();
});
}

Initialize

const bridge = new Bridge();
bridge.ready(function() {
  // ...
});
await view.WaitUntilInitialized();
bridge = new Bridge();
bridge.Initialize(view);

Messages

JavaScript will listen for vuplex.message, CSharp will listen to MessageReceived. Both calls process to handle messages.

Exporting a function

bridge.method('sum', function(a, b) { // can be async
  return a + b;
});
bridge.Function("sum", new Func<long, long, long>((long a, long b) => {
  return a + b;
});

Calling an exported method/function/procedure

const res = await bridge.invoke('sum', 2, 3);
// res = 5 (from csharp)
var res = await bridge.Invoke("sum", 2, 3);
// res = 5 (from javascript)

Listening for events

const unsubscribe = bridge.on('scene_changed', function (e) {
  // typeof(e) == CustomEvent
  // e.name == 'scene_changed'
  // e.detail == data
  console.log('Scene changed to: ' + e.detail);
});

// call unsubscribe() will remove the event handler
public class DialogClosedEvent {
  [JsonProperty("dialog_id")]
  public string Id { get; set; }
}
// ...
bridge.On("dialog_closed", (DialogClosedEvent e) => {
  Debug.LogWarningFormat("Dialog '{0}' was closed", e.Id);
});
// Lacking unsubscription

Dispatching events

bridge.emit('dialog_closed', { dialog_id: 'login_box' });
bridge.Emit("scene_changed", "some_scene_name");
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment