Skip to content

Commit

Permalink
Simplify the interface between elm app and web host
Browse files Browse the repository at this point in the history
+ Remove redundant requests to be notified on the arrival of time.
+ Clarify there is no dependency between the different tasks, as outlined at https://github.com/elm-fullstack/elm-fullstack/blob/6fa473cd9a6287e1ef452a1d736d445330e79739/explore/2019-08-31.interface-between-process-and-host/2019-08-31.interface-between-process-and-host.md
+ For now, continue supporting the old interface. Implement a branch in the host to use the version of the interface matching the Elm app code.
+ Adapt enough examples in tests to cover the different messages on the new interface.
+ Update the names of JSON fields on the interface to use the consistent casing of the first letters.
+ Also in the new version of the interface Elm module: Adapt to recent observations of conversions between `Bytes.Bytes` and other representations: Use base64 for now because the conversions cost a lot of time with the current execution engine. As can be seen in some examples, we need to convert back to base64 again anyway.
  • Loading branch information
Viir committed Jun 20, 2020
1 parent 6fa473c commit 2545325
Show file tree
Hide file tree
Showing 12 changed files with 396 additions and 264 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,29 @@

namespace Kalmit.PersistentProcess.InterfaceToHost
{
public class Event
public class AppEventStructure
{
public HttpRequestEvent httpRequest;
public ArrivedAtTimeEventStructure ArrivedAtTimeEvent;

public ResultFromTaskWithId taskComplete;
public HttpRequestEvent HttpRequestEvent;

public ArrivedAtTimeEventStructure ArrivedAtTimeEvent;
public ResultFromTaskWithId TaskCompleteEvent;
}

public class ResponseOverSerialInterface
{
public string decodeEventError;
public string DecodeEventError;

public ProcessRequest[] decodeEventSuccess;
public AppEventResponseStructure DecodeEventSuccess;
}

public class ProcessRequest
public class AppEventResponseStructure
{
public HttpResponseRequest completeHttpResponse;
public NotifyWhenArrivedAtTimeRequestStructure notifyWhenArrivedAtTime;

public StartTask startTask;
public StartTask[] startTasks;

public NotifyWhenArrivedAtTimeRequestStructure NotifyWhenArrivedAtTimeRequest;
public HttpResponseRequest[] completeHttpResponses;
}

public class HttpRequestEvent
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using System;
using System.Linq;

namespace Kalmit.PersistentProcess.InterfaceToHost_Before_2020_06_20
{
public class Event
{
public InterfaceToHost.HttpRequestEvent httpRequest;

public InterfaceToHost.ResultFromTaskWithId taskComplete;

public InterfaceToHost.ArrivedAtTimeEventStructure ArrivedAtTimeEvent;

static public Event FromAppEvent(InterfaceToHost.AppEventStructure appEvent)
{
if (appEvent.ArrivedAtTimeEvent != null)
return new Event { ArrivedAtTimeEvent = appEvent.ArrivedAtTimeEvent };

if (appEvent.TaskCompleteEvent != null)
return new Event { taskComplete = appEvent.TaskCompleteEvent };

if (appEvent.HttpRequestEvent != null)
return new Event { httpRequest = appEvent.HttpRequestEvent };

throw new NotImplementedException("Programming error: Case not implemented.");
}

public InterfaceToHost.AppEventStructure AsAppEvent()
{
if (ArrivedAtTimeEvent != null)
return new InterfaceToHost.AppEventStructure { ArrivedAtTimeEvent = ArrivedAtTimeEvent };

if (httpRequest != null)
return new InterfaceToHost.AppEventStructure { HttpRequestEvent = httpRequest };

if (taskComplete != null)
return new InterfaceToHost.AppEventStructure { TaskCompleteEvent = taskComplete };

throw new NotImplementedException("Programming error: Case not implemented.");
}
}

public class ResponseOverSerialInterface
{
public string decodeEventError;

public ProcessRequest[] decodeEventSuccess;

public InterfaceToHost.ResponseOverSerialInterface TranslateToNewStructure()
{
if (decodeEventSuccess == null)
return new InterfaceToHost.ResponseOverSerialInterface { DecodeEventError = decodeEventError };

var notifyWhenArrivedAtTime =
decodeEventSuccess.Select(request => request.NotifyWhenArrivedAtTimeRequest).WhereNotNull().FirstOrDefault();

var startTasks =
decodeEventSuccess.Select(request => request.startTask).WhereNotNull().ToArray();

var completeHttpResponses =
decodeEventSuccess.Select(request => request.completeHttpResponse).WhereNotNull().ToArray();

return new InterfaceToHost.ResponseOverSerialInterface
{
DecodeEventSuccess = new InterfaceToHost.AppEventResponseStructure
{
startTasks = startTasks,
completeHttpResponses = completeHttpResponses,
notifyWhenArrivedAtTime = notifyWhenArrivedAtTime,
}
};
}
}

public class ProcessRequest
{
public InterfaceToHost.HttpResponseRequest completeHttpResponse;

public InterfaceToHost.StartTask startTask;

public InterfaceToHost.NotifyWhenArrivedAtTimeRequestStructure NotifyWhenArrivedAtTimeRequest;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ void letTimePassInPersistentProcessHost(TimeSpan amount) =>
{
IEnumerable<string> EnumerateStoredProcessEventsHttpRequestsBodies() =>
testSetup.EnumerateStoredUpdateElmAppStateForEvents()
.Select(processEvent => processEvent?.httpRequest?.request?.bodyAsBase64)
.Select(processEvent => processEvent?.HttpRequestEvent?.request?.bodyAsBase64)
.WhereNotNull()
.Select(bodyBase64 => System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(bodyBase64)));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,11 +167,11 @@ public WebHost.ProcessStoreSupportingMigrations.ProcessStoreReaderInFileStore Bu
new WebHost.ProcessStoreSupportingMigrations.ProcessStoreReaderInFileStore(
BuildProcessStoreFileStoreReaderInFileDirectory());

public IEnumerable<PersistentProcess.InterfaceToHost.Event> EnumerateStoredUpdateElmAppStateForEvents()
public IEnumerable<PersistentProcess.InterfaceToHost.AppEventStructure> EnumerateStoredUpdateElmAppStateForEvents()
{
var processStoreReader = BuildProcessStoreReaderInFileDirectory();

PersistentProcess.InterfaceToHost.Event eventFromHash(string eventComponentHash)
PersistentProcess.InterfaceToHost.AppEventStructure eventFromHash(string eventComponentHash)
{
var component = processStoreReader.LoadComponent(eventComponentHash);

Expand All @@ -183,7 +183,19 @@ PersistentProcess.InterfaceToHost.Event eventFromHash(string eventComponentHash)

var eventString = Encoding.UTF8.GetString(component.BlobContent.ToArray());

return Newtonsoft.Json.JsonConvert.DeserializeObject<PersistentProcess.InterfaceToHost.Event>(eventString);
{
/*
2020-06-20 TODO: Remove temporary branch for older app interface to host.
*/

var asOldStructure =
Newtonsoft.Json.JsonConvert.DeserializeObject<PersistentProcess.InterfaceToHost_Before_2020_06_20.Event>(eventString);

if (asOldStructure?.httpRequest != null || asOldStructure?.taskComplete != null)
return asOldStructure.AsAppEvent();
}

return Newtonsoft.Json.JsonConvert.DeserializeObject<PersistentProcess.InterfaceToHost.AppEventStructure>(eventString);
}

return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ namespace Kalmit.PersistentProcess.WebHost
{
public class Program
{
static public string AppVersionId => "2020-06-18";
static public string AppVersionId => "2020-06-20";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,8 @@ IWebHost buildWebHost()

return elmEventResponse;
}
}
},
appCodeUsesInterface_Before_2020_06_20 = WebAppAndElmAppConfig.appCodeUsesInterface_Before_2020_06_20_from_appConfigTree(appConfigTree),
});
})
.Build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,9 +192,9 @@ void performProcessTaskAndFeedbackEvent(InterfaceToHost.StartTask taskWithId)
{
var taskResult = performProcessTask(taskWithId.task);

var interfaceEvent = new InterfaceToHost.Event
var interfaceEvent = new InterfaceToHost.AppEventStructure
{
taskComplete = new InterfaceToHost.ResultFromTaskWithId
TaskCompleteEvent = new InterfaceToHost.ResultFromTaskWithId
{
taskId = taskWithId.taskId,
taskResult = taskResult,
Expand All @@ -206,22 +206,42 @@ void performProcessTaskAndFeedbackEvent(InterfaceToHost.StartTask taskWithId)

var processRequestCompleteHttpResponse = new ConcurrentDictionary<string, InterfaceToHost.HttpResponse>();

void processEventAndResultingRequests(InterfaceToHost.Event interfaceEvent)
InterfaceToHost.NotifyWhenArrivedAtTimeRequestStructure nextTimeToNotify = null;

void processEventAndResultingRequests(InterfaceToHost.AppEventStructure interfaceEvent)
{
if (tasksCancellationTokenSource.IsCancellationRequested)
return;

var serializedInterfaceEvent = Newtonsoft.Json.JsonConvert.SerializeObject(interfaceEvent, jsonSerializerSettings);

{
if (webAppAndElmAppConfig.appCodeUsesInterface_Before_2020_06_20)
{
serializedInterfaceEvent = Newtonsoft.Json.JsonConvert.SerializeObject(
InterfaceToHost_Before_2020_06_20.Event.FromAppEvent(interfaceEvent),
jsonSerializerSettings);
}
}

var serializedResponse = webAppAndElmAppConfig.ProcessEventInElmApp(serializedInterfaceEvent);

InterfaceToHost.ResponseOverSerialInterface structuredResponse = null;

try
{
structuredResponse =
Newtonsoft.Json.JsonConvert.DeserializeObject<InterfaceToHost.ResponseOverSerialInterface>(
serializedResponse);
if (webAppAndElmAppConfig.appCodeUsesInterface_Before_2020_06_20)
{
structuredResponse =
Newtonsoft.Json.JsonConvert.DeserializeObject<InterfaceToHost_Before_2020_06_20.ResponseOverSerialInterface>(
serializedResponse).TranslateToNewStructure();
}
else
{
structuredResponse =
Newtonsoft.Json.JsonConvert.DeserializeObject<InterfaceToHost.ResponseOverSerialInterface>(
serializedResponse);
}
}
catch (Exception parseException)
{
Expand All @@ -230,38 +250,55 @@ void processEventAndResultingRequests(InterfaceToHost.Event interfaceEvent)
parseException);
}

if (structuredResponse?.decodeEventSuccess == null)
if (structuredResponse?.DecodeEventSuccess == null)
{
throw new Exception("Hosted app failed to decode the event: " + structuredResponse.decodeEventError);
throw new Exception("Hosted app failed to decode the event: " + structuredResponse.DecodeEventError);
}

foreach (var requestFromProcess in structuredResponse.decodeEventSuccess)
if (structuredResponse.DecodeEventSuccess.notifyWhenArrivedAtTime != null)
{
if (requestFromProcess.completeHttpResponse != null)
processRequestCompleteHttpResponse[requestFromProcess.completeHttpResponse.httpRequestId] =
requestFromProcess.completeHttpResponse.response;
System.Threading.Tasks.Task.Run(() =>
{
if (tasksCancellationTokenSource.IsCancellationRequested)
return;

if (requestFromProcess.startTask != null)
System.Threading.Tasks.Task.Run(() => performProcessTaskAndFeedbackEvent(requestFromProcess.startTask));
nextTimeToNotify = structuredResponse.DecodeEventSuccess.notifyWhenArrivedAtTime;

if (requestFromProcess.NotifyWhenArrivedAtTimeRequest != null)
System.Threading.Tasks.Task.Run(() =>
while (!tasksCancellationTokenSource.IsCancellationRequested)
{
var delayMilliseconds =
requestFromProcess.NotifyWhenArrivedAtTimeRequest.posixTimeMilli -
getDateTimeOffset().ToUnixTimeMilliseconds();
if (nextTimeToNotify != structuredResponse.DecodeEventSuccess.notifyWhenArrivedAtTime)
return;

if (0 < delayMilliseconds)
System.Threading.Tasks.Task.Delay((int)delayMilliseconds, tasksCancellationTokenSource.Token);
var remainingDelayMilliseconds =
structuredResponse.DecodeEventSuccess.notifyWhenArrivedAtTime.posixTimeMilli -
getDateTimeOffset().ToUnixTimeMilliseconds();

processEventAndResultingRequests(new InterfaceToHost.Event
if (remainingDelayMilliseconds <= 0)
{
ArrivedAtTimeEvent = new InterfaceToHost.ArrivedAtTimeEventStructure
processEventAndResultingRequests(new InterfaceToHost.AppEventStructure
{
posixTimeMilli = getDateTimeOffset().ToUnixTimeMilliseconds()
}
});
});
ArrivedAtTimeEvent = new InterfaceToHost.ArrivedAtTimeEventStructure
{
posixTimeMilli = getDateTimeOffset().ToUnixTimeMilliseconds()
}
});
return;
}

System.Threading.Thread.Sleep(10);
}
});
}

foreach (var startTask in structuredResponse.DecodeEventSuccess.startTasks)
{
System.Threading.Tasks.Task.Run(() => performProcessTaskAndFeedbackEvent(startTask));
}

foreach (var completeHttpResponse in structuredResponse.DecodeEventSuccess.completeHttpResponses)
{
processRequestCompleteHttpResponse[completeHttpResponse.httpRequestId] =
completeHttpResponse.response;
}
}

Expand All @@ -276,11 +313,12 @@ void processEventAndResultingRequests(InterfaceToHost.Event interfaceEvent)
var httpRequestId = timeMilli.ToString() + "-" + httpRequestIndex.ToString();

{
var httpEvent = await AsPersistentProcessInterfaceHttpRequestEvent(context, httpRequestId, currentDateTime);
var httpRequestEvent =
await AsPersistentProcessInterfaceHttpRequestEvent(context, httpRequestId, currentDateTime);

var httpRequestInterfaceEvent = new InterfaceToHost.Event
var httpRequestInterfaceEvent = new InterfaceToHost.AppEventStructure
{
httpRequest = httpEvent,
HttpRequestEvent = httpRequestEvent,
};

processEventAndResultingRequests(httpRequestInterfaceEvent);
Expand Down Expand Up @@ -375,5 +413,25 @@ public class WebAppAndElmAppConfig
public WebAppConfigurationJsonStructure WebAppConfiguration;

public Func<string, string> ProcessEventInElmApp;

/*
2020-06-20 TODO: Remove temporary branches to maintain compatibility with older app codes.
*/
public bool appCodeUsesInterface_Before_2020_06_20;

static public bool appCodeUsesInterface_Before_2020_06_20_from_appConfigTree(Composition.TreeComponent appConfigTree)
{
return
appConfigTree.EnumerateBlobsTransitive()
.Any(blobPathAndContent =>
{
if (!blobPathAndContent.path.Last().SequenceEqual(System.Text.Encoding.UTF8.GetBytes("InterfaceToHost.elm")))
return false;

return
System.Text.Encoding.UTF8.GetString(blobPathAndContent.blobContent.ToArray())
.Contains("DecodeEventSuccess (List ProcessRequest)");
});
}
}
}
Loading

0 comments on commit 2545325

Please sign in to comment.