Skip to content

Commit

Permalink
Introducing WebWidget
Browse files Browse the repository at this point in the history
WebWidget is userinterface controller via regular lua scripts, but are
implemented via HTML/CSS/Javascript, served by Slipstream via a HTTP and
WebSocket endpoint.

HTTP is used to serve the assets, while websocket is used for
communication between Lua and Javascript.

LuaScripts have been reorganized, now that the samples might also
include WebWidgets. Instead of LuaScripts you'll find a Samples
directory
  • Loading branch information
dennis committed Aug 4, 2021
1 parent 2bc2876 commit c38e74a
Show file tree
Hide file tree
Showing 47 changed files with 1,137 additions and 52 deletions.
10 changes: 10 additions & 0 deletions Backend/WebWidget/InstanceIndex.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<html>
<head>
<title>Slipstream Actice WebWidget Instances</title>
</head>
<body>
<h1>Slipstream Actice WebWidget Instances</h1>

{{CONTENT}}
</body>
</html>
52 changes: 52 additions & 0 deletions Backend/WebWidget/ss.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
var INSTANCE_ID
var WEB_WIDGET_TYPE
var ASSETS

function connect() {
let socket = a = new WebSocket("ws://" + document.location.host + "/events/" + INSTANCE_ID)

socket.onopen = function (e) {
console.log("[slipstream ws] [open] Connection established")

if (typeof(onConnect) === "function") {
onConnect()
}
}

socket.onmessage = function (event) {
if (typeof (onData) !== "function") {
console.log("[slipstream ws] [onmessage] got data, but no onData() function defined. Ignored", event.data)
}
else {
console.log("[slipstream ws] [onmessage] got data", event.data)
onData(JSON.parse(event.data))
}
}

socket.onclose = function (event) {
if (event.wasClean) {
console.log("[slipstream ws] [close] Connection closed cleanly, code=" + event.code + " reason=" + event.reason)
} else {
console.log('[slipstream ws] [close] Connection died')
}

if (typeof (onDisconnect) === "function") {
onDisconnect()
}

setTimeout(connect, 1000);
}

socket.onerror = function (error) {
console.log("[slipstream ws] [error] " + error.message)
socket.close()
}
}

window.addEventListener('load', function () {
INSTANCE_ID = document.body.getAttribute("data-instance-id")
WEB_WIDGET_TYPE = document.body.getAttribute("data-web-widget-type")
ASSETS = document.body.getAttribute("data-assets")

connect()
})
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
And only scripts using that instance will be notified of its events. Meaning,
you will not get IRacing events, if you don't have require("api/iracing"):instance(..)
- You need a new init.lua, so delete your existing init.lua to get a new one.
- IRacing: Adds pit-stop lua functions: pit_clear_all(), pit_clear_tyres(),
pit_fast_repair(), pit_add_fuel(), pit_change_(left|right)_(front|rear)_tyre()
and pit_clean_windshield()
- New Component: WebWidgets
- Merged UI components into WinFormUI. You need to change `require("api/ui")` to
`require("api/winformui")`

Expand Down
2 changes: 0 additions & 2 deletions Components/Twitch/Events/TwitchReceivedMessage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ namespace Slipstream.Components.Twitch.Events
public class TwitchReceivedMessage : IEvent
{
public string EventType => nameof(TwitchReceivedMessage);

public IEventEnvelope Envelope { get; set; } = new EventEnvelope();


[Description("User that sent the message")]
public string From { get; set; } = string.Empty;
Expand Down
16 changes: 16 additions & 0 deletions Components/WebWidget/EventFactory/WebWidgetEventFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Slipstream.Components.WebWidget;
using Slipstream.Components.WebWidget.Events;
using Slipstream.Shared;

#nullable enable

namespace Slipstream.Components.Internal.EventFactory
{
public class WebWidgetEventFactory : IWebWidgetEventFactory
{
public WebWidgetCommandEvent CreateWebWidgetCommandEvent(IEventEnvelope envelope, string data)
{
return new WebWidgetCommandEvent { Envelope = envelope, Data = data };
}
}
}
32 changes: 32 additions & 0 deletions Components/WebWidget/EventHandler/WebWidgetEventHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#nullable enable

using Slipstream.Components.WebWidget.Events;
using Slipstream.Shared;
using System;

namespace Slipstream.Components.WebWidget.EventHandler
{
internal class WebWidgetEventHandler : IEventHandler
{
public event EventHandler<WebWidgetCommandEvent>? OnWebWidgetCommandEvent;

public IEventHandler.HandledStatus HandleEvent(IEvent @event)
{
return @event switch
{
WebWidgetCommandEvent tev => OnEvent(OnWebWidgetCommandEvent, tev),
_ => IEventHandler.HandledStatus.NotMine,
};
}

private IEventHandler.HandledStatus OnEvent<TEvent>(EventHandler<TEvent>? onEvent, TEvent args)
{
if (onEvent != null)
{
onEvent.Invoke(this, args);
return IEventHandler.HandledStatus.Handled;
}
return IEventHandler.HandledStatus.UseDefault;
}
}
}
13 changes: 13 additions & 0 deletions Components/WebWidget/Events/WebWidgetCommandEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#nullable enable

using Slipstream.Shared;

namespace Slipstream.Components.WebWidget.Events
{
public class WebWidgetCommandEvent : IEvent
{
public string EventType => typeof(WebWidgetCommandEvent).Name;
public IEventEnvelope Envelope { get; set; } = new EventEnvelope();
public string Data { get; set; } = string.Empty;
}
}
133 changes: 133 additions & 0 deletions Components/WebWidget/HttpServer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
#nullable enable

using System;
using System.Threading;
using System.Threading.Tasks;
using EmbedIO;
using Serilog;
using Slipstream.Components.WebWidget.EventHandler;
using Slipstream.Shared;

namespace Slipstream.Components.WebWidget
{
public class HttpServer : IHttpServer, IHttpServerApi
{
private const string WEB_WIDGET_ROOT_DIRECTORY = "WebWidgets/";

private readonly Object Lock = new object();
private Thread? ServiceThread;
private readonly ILogger Logger;
private readonly IEventBusSubscription Subscription;
private readonly WebSocketsEventsServer EventsServerModule;
private readonly IEventHandlerController EventHandlerController;
private readonly IWebWidgetInstances Instances = new WebWidgetInstances();
private const string Url = "http://127.0.0.1:1919"; // Must NOT end with slash

public HttpServer(ILogger logger, IEventBusSubscription subscription, IEventHandlerController eventHandlerController)
{
Logger = logger;
Subscription = subscription;
EventHandlerController = eventHandlerController;

EventsServerModule = new WebSocketsEventsServer(Logger, Instances);

System.IO.Directory.CreateDirectory(WEB_WIDGET_ROOT_DIRECTORY);
}

public void AddInstance(string instanceId, string webWidgetType, string? data)
{
lock (Lock)
{
if (ServiceThread == null)
{
ServiceThread = new Thread(new ThreadStart(ThreadMain))
{
Name = GetType().Name,
};
ServiceThread.Start();
}
}

// Crude sanity check
var indexFile = WEB_WIDGET_ROOT_DIRECTORY + webWidgetType + "/index.html";
if (System.IO.File.Exists(indexFile))
{
Instances.Add(instanceId, webWidgetType, data);
Subscription.AddImpersonate(instanceId);

Logger.Information($"HttpServer: {Url}/instances/{instanceId} added");
}
else
{
Logger.Error($"HttpServer: {Url}/instances/{instanceId} not added, as {indexFile} does not exist");
}
}

private void ThreadMain()
{
Logger.Information("HttpServer started");

using (var server = CreateWebServer(Url))
{
Task serverTask = server.RunAsync();
bool stopping = false;

var internalHandler = EventHandlerController.Get<Internal.EventHandler.Internal>();
var webWidgetHandler = EventHandlerController.Get<WebWidgetEventHandler>();

internalHandler.OnInternalCommandShutdown += (_, e) => stopping = true;
webWidgetHandler.OnWebWidgetCommandEvent += (_, e) =>
{
if (e.Envelope.Recipients == null)
return;
foreach (var recipient in e.Envelope.Recipients)
{
EventsServerModule.Broadcast(recipient, e.Data);
}
};

while (!stopping)
{
IEvent? @event = Subscription.NextEvent(100);
EventHandlerController.HandleEvent(@event);

stopping = stopping || serverTask.IsCompleted;
}
}

Logger.Information("HttpServer stopped");
}

public void RemoveInstance(string instanceId)
{
Instances.Remove(instanceId);
Subscription.DeleteImpersonation(instanceId);
Logger.Information($"HttpServer: {Url}/instances/{instanceId} removed");
}

private WebServer CreateWebServer(string url)
{
// endpoints:
// /
// /ss.js
// /instances/<instanceid>/
// /webwidgets/<webwidgettype>/
// /events/<instanceid>/

var server = new WebServer(o => o
.WithUrlPrefix(url))
.WithStaticFolder("/webwidgets/", WEB_WIDGET_ROOT_DIRECTORY, false)
.WithModule(EventsServerModule)
.WithModule(new JavascriptWebModule())
.WithModule(new InstanceWebModule(Instances, WEB_WIDGET_ROOT_DIRECTORY, Logger))
.WithModule(new InstanceIndexWebModule(Instances, Logger));

return server;
}

public void Dispose()
{
}
}
}
10 changes: 10 additions & 0 deletions Components/WebWidget/IHttpServer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#nullable enable

using System;

namespace Slipstream.Components.WebWidget
{
public interface IHttpServer : IDisposable
{
}
}
11 changes: 11 additions & 0 deletions Components/WebWidget/IHttpServerApi.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#nullable enable

namespace Slipstream.Components.WebWidget
{
public interface IHttpServerApi
{
// These needs to be threadsafe!
void AddInstance(string instanceId, string instanceType, string? data);
void RemoveInstance(string instanceId);
}
}
12 changes: 12 additions & 0 deletions Components/WebWidget/IWebWidgetEventFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Slipstream.Components.WebWidget.Events;
using Slipstream.Shared;

#nullable enable

namespace Slipstream.Components.WebWidget
{
public interface IWebWidgetEventFactory
{
WebWidgetCommandEvent CreateWebWidgetCommandEvent(IEventEnvelope envelope, string data);
}
}
16 changes: 16 additions & 0 deletions Components/WebWidget/IWebWidgetInstances.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#nullable enable


using System.Collections.Generic;

namespace Slipstream.Components.WebWidget
{
public interface IWebWidgetInstances
{
void Add(string id, string type, string? data);
void Remove(string id);
string this[string id] {get;}
ICollection<string> GetIds();
string? InitData(string id);
}
}
Loading

0 comments on commit c38e74a

Please sign in to comment.