Harden embedded browser runtime handshake
This commit is contained in:
parent
e185c86d50
commit
b2e158c8cc
10 changed files with 460 additions and 18 deletions
|
|
@ -36,6 +36,7 @@ void UHyperTwistBrowserBridgeObject::NotifyState(const FString& StateJson)
|
|||
void UHyperTwistBrowserBridgeObject::NotifyRuntimeReady(const FString& RuntimeReadyJson)
|
||||
{
|
||||
LastRuntimeReadyJson = RuntimeReadyJson;
|
||||
OnRuntimeReady.Broadcast(RuntimeReadyJson);
|
||||
}
|
||||
|
||||
void UHyperTwistBrowserBridgeObject::ResetReceivedMessages()
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ void UHyperTwistBrowserWidget::LoadBundledBrowserShell()
|
|||
InitialUrl = ResolveBundledBrowserShellUrl();
|
||||
if (BrowserWidget.IsValid())
|
||||
{
|
||||
ResetBrowserRuntimeHandshake();
|
||||
BrowserWidget->LoadURL(InitialUrl);
|
||||
}
|
||||
}
|
||||
|
|
@ -36,47 +37,47 @@ void UHyperTwistBrowserWidget::LoadBrowserUrl(const FString& NewUrl)
|
|||
InitialUrl = NewUrl;
|
||||
if (BrowserWidget.IsValid() && !NewUrl.IsEmpty())
|
||||
{
|
||||
ResetBrowserRuntimeHandshake();
|
||||
BrowserWidget->LoadURL(NewUrl);
|
||||
}
|
||||
}
|
||||
|
||||
void UHyperTwistBrowserWidget::DispatchCommandJson(const FString& CommandJson)
|
||||
{
|
||||
if (!BrowserWidget.IsValid() || CommandJson.IsEmpty())
|
||||
if (CommandJson.IsEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const FString EscapedJson = EscapeForJavaScriptSingleQuotedString(CommandJson);
|
||||
BrowserWidget->ExecuteJavascript(FString::Printf(
|
||||
TEXT("window.HyperTwistBrowserRuntime && window.HyperTwistBrowserRuntime.receiveCommand(JSON.parse('%s'));"),
|
||||
*EscapedJson
|
||||
));
|
||||
if (!IsBrowserRuntimeDispatchReady())
|
||||
{
|
||||
QueueCommandJson(CommandJson);
|
||||
return;
|
||||
}
|
||||
|
||||
ExecuteBrowserRuntimeScript(BuildCommandDispatchScript(CommandJson));
|
||||
}
|
||||
|
||||
void UHyperTwistBrowserWidget::PushShellStateJson(const FString& StateJson)
|
||||
{
|
||||
if (!BrowserWidget.IsValid() || StateJson.IsEmpty())
|
||||
if (StateJson.IsEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const FString EscapedJson = EscapeForJavaScriptSingleQuotedString(StateJson);
|
||||
BrowserWidget->ExecuteJavascript(FString::Printf(
|
||||
TEXT("window.HyperTwistBrowserRuntime && window.HyperTwistBrowserRuntime.setShellState(JSON.parse('%s'));"),
|
||||
*EscapedJson
|
||||
));
|
||||
if (!IsBrowserRuntimeDispatchReady())
|
||||
{
|
||||
QueueShellStateJson(StateJson);
|
||||
return;
|
||||
}
|
||||
|
||||
ExecuteBrowserRuntimeScript(BuildShellStateDispatchScript(StateJson));
|
||||
}
|
||||
|
||||
bool UHyperTwistBrowserWidget::PushTilingStateEnvelope(
|
||||
const FHyperTwistTrainingTilingStateEnvelope& StateEnvelope
|
||||
)
|
||||
{
|
||||
if (!BrowserWidget.IsValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
FString StateJson;
|
||||
if (!UHyperTwistTrainingTilingLibrary::TrySerializeStateEnvelopeToJson(
|
||||
StateEnvelope,
|
||||
|
|
@ -104,6 +105,21 @@ FString UHyperTwistBrowserWidget::GetLastStateJson() const
|
|||
return BrowserBridgeObject != nullptr ? BrowserBridgeObject->LastStateJson : FString();
|
||||
}
|
||||
|
||||
int32 UHyperTwistBrowserWidget::GetPendingCommandCount() const
|
||||
{
|
||||
return PendingCommandJsonQueue.Num();
|
||||
}
|
||||
|
||||
bool UHyperTwistBrowserWidget::HasPendingShellStateJson() const
|
||||
{
|
||||
return bHasPendingShellStateJson;
|
||||
}
|
||||
|
||||
bool UHyperTwistBrowserWidget::IsBrowserRuntimeReady() const
|
||||
{
|
||||
return bBrowserRuntimeReady;
|
||||
}
|
||||
|
||||
bool UHyperTwistBrowserWidget::TryGetLastTilingEnvelope(
|
||||
FHyperTwistTrainingTilingStateEnvelope& OutStateEnvelope
|
||||
) const
|
||||
|
|
@ -142,9 +158,20 @@ TSharedRef<SWidget> UHyperTwistBrowserWidget::RebuildWidget()
|
|||
}
|
||||
else
|
||||
{
|
||||
if (RuntimeReadyDelegateHandle.IsValid())
|
||||
{
|
||||
BrowserBridgeObject->OnRuntimeReady.Remove(RuntimeReadyDelegateHandle);
|
||||
RuntimeReadyDelegateHandle.Reset();
|
||||
}
|
||||
BrowserBridgeObject->ResetReceivedMessages();
|
||||
}
|
||||
|
||||
RuntimeReadyDelegateHandle = BrowserBridgeObject->OnRuntimeReady.AddUObject(
|
||||
this,
|
||||
&UHyperTwistBrowserWidget::HandleBrowserRuntimeReady
|
||||
);
|
||||
ResetBrowserRuntimeHandshake();
|
||||
|
||||
BrowserWidget =
|
||||
SNew(SWebBrowser)
|
||||
.InitialURL(ResolveInitialUrl())
|
||||
|
|
@ -158,6 +185,12 @@ void UHyperTwistBrowserWidget::ReleaseSlateResources(const bool bReleaseChildren
|
|||
{
|
||||
Super::ReleaseSlateResources(bReleaseChildren);
|
||||
|
||||
if (BrowserBridgeObject != nullptr && RuntimeReadyDelegateHandle.IsValid())
|
||||
{
|
||||
BrowserBridgeObject->OnRuntimeReady.Remove(RuntimeReadyDelegateHandle);
|
||||
RuntimeReadyDelegateHandle.Reset();
|
||||
}
|
||||
|
||||
if (BrowserWidget.IsValid() && BrowserBridgeObject != nullptr)
|
||||
{
|
||||
BrowserWidget->UnbindUObject(TEXT("hypertwist"), BrowserBridgeObject, true);
|
||||
|
|
@ -176,11 +209,112 @@ void UHyperTwistBrowserWidget::SynchronizeProperties()
|
|||
const FString ResolvedUrl = ResolveInitialUrl();
|
||||
if (!ResolvedUrl.IsEmpty())
|
||||
{
|
||||
ResetBrowserRuntimeHandshake();
|
||||
BrowserWidget->LoadURL(ResolvedUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool UHyperTwistBrowserWidget::IsBrowserRuntimeDispatchReady() const
|
||||
{
|
||||
#if WITH_AUTOMATION_TESTS
|
||||
if (bForceBrowserRuntimeDispatchReadyForTesting && !BrowserWidget.IsValid())
|
||||
{
|
||||
return bBrowserRuntimeReady;
|
||||
}
|
||||
#endif
|
||||
|
||||
return BrowserWidget.IsValid() && bBrowserRuntimeReady;
|
||||
}
|
||||
|
||||
void UHyperTwistBrowserWidget::ExecuteBrowserRuntimeScript(const FString& Script)
|
||||
{
|
||||
#if WITH_AUTOMATION_TESTS
|
||||
if (bForceBrowserRuntimeDispatchReadyForTesting && !BrowserWidget.IsValid())
|
||||
{
|
||||
ExecutedBrowserScriptsForTesting.Add(Script);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (BrowserWidget.IsValid())
|
||||
{
|
||||
BrowserWidget->ExecuteJavascript(Script);
|
||||
}
|
||||
}
|
||||
|
||||
void UHyperTwistBrowserWidget::HandleBrowserRuntimeReady(const FString& RuntimeReadyJson)
|
||||
{
|
||||
if (RuntimeReadyJson.IsEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bBrowserRuntimeReady = true;
|
||||
FlushPendingBrowserMessages();
|
||||
}
|
||||
|
||||
void UHyperTwistBrowserWidget::FlushPendingBrowserMessages()
|
||||
{
|
||||
if (!IsBrowserRuntimeDispatchReady())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (bHasPendingShellStateJson)
|
||||
{
|
||||
ExecuteBrowserRuntimeScript(BuildShellStateDispatchScript(PendingShellStateJson));
|
||||
PendingShellStateJson.Reset();
|
||||
bHasPendingShellStateJson = false;
|
||||
}
|
||||
|
||||
for (const FString& CommandJson : PendingCommandJsonQueue)
|
||||
{
|
||||
ExecuteBrowserRuntimeScript(BuildCommandDispatchScript(CommandJson));
|
||||
}
|
||||
PendingCommandJsonQueue.Reset();
|
||||
}
|
||||
|
||||
void UHyperTwistBrowserWidget::QueueCommandJson(const FString& CommandJson)
|
||||
{
|
||||
if (!CommandJson.IsEmpty())
|
||||
{
|
||||
PendingCommandJsonQueue.Add(CommandJson);
|
||||
}
|
||||
}
|
||||
|
||||
void UHyperTwistBrowserWidget::QueueShellStateJson(const FString& StateJson)
|
||||
{
|
||||
if (!StateJson.IsEmpty())
|
||||
{
|
||||
PendingShellStateJson = StateJson;
|
||||
bHasPendingShellStateJson = true;
|
||||
}
|
||||
}
|
||||
|
||||
void UHyperTwistBrowserWidget::ResetBrowserRuntimeHandshake()
|
||||
{
|
||||
bBrowserRuntimeReady = false;
|
||||
}
|
||||
|
||||
FString UHyperTwistBrowserWidget::BuildCommandDispatchScript(const FString& CommandJson) const
|
||||
{
|
||||
const FString EscapedJson = EscapeForJavaScriptSingleQuotedString(CommandJson);
|
||||
return FString::Printf(
|
||||
TEXT("window.HyperTwistBrowserRuntime && window.HyperTwistBrowserRuntime.receiveCommand(JSON.parse('%s'));"),
|
||||
*EscapedJson
|
||||
);
|
||||
}
|
||||
|
||||
FString UHyperTwistBrowserWidget::BuildShellStateDispatchScript(const FString& StateJson) const
|
||||
{
|
||||
const FString EscapedJson = EscapeForJavaScriptSingleQuotedString(StateJson);
|
||||
return FString::Printf(
|
||||
TEXT("window.HyperTwistBrowserRuntime && window.HyperTwistBrowserRuntime.setShellState(JSON.parse('%s'));"),
|
||||
*EscapedJson
|
||||
);
|
||||
}
|
||||
|
||||
FString UHyperTwistBrowserWidget::EscapeForJavaScriptSingleQuotedString(const FString& Input)
|
||||
{
|
||||
FString Escaped = Input;
|
||||
|
|
@ -206,3 +340,21 @@ void UHyperTwistBrowserWidget::EnsureBridgeBound()
|
|||
BrowserWidget->BindUObject(TEXT("hypertwist"), BrowserBridgeObject, true);
|
||||
}
|
||||
}
|
||||
|
||||
#if WITH_AUTOMATION_TESTS
|
||||
void UHyperTwistBrowserWidget::SetBrowserRuntimeDispatchReadyForTesting(const bool bInDispatchReady)
|
||||
{
|
||||
bForceBrowserRuntimeDispatchReadyForTesting = bInDispatchReady;
|
||||
ExecutedBrowserScriptsForTesting.Reset();
|
||||
}
|
||||
|
||||
void UHyperTwistBrowserWidget::SimulateBrowserRuntimeReadyForTesting(const FString& RuntimeReadyJson)
|
||||
{
|
||||
HandleBrowserRuntimeReady(RuntimeReadyJson);
|
||||
}
|
||||
|
||||
const TArray<FString>& UHyperTwistBrowserWidget::GetExecutedBrowserScriptsForTesting() const
|
||||
{
|
||||
return ExecutedBrowserScriptsForTesting;
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@
|
|||
#include "UObject/Object.h"
|
||||
#include "HyperTwistBrowserBridgeObject.generated.h"
|
||||
|
||||
DECLARE_MULTICAST_DELEGATE_OneParam(FHyperTwistBrowserRuntimeReadyDelegate, const FString&);
|
||||
|
||||
UCLASS(BlueprintType)
|
||||
class UNREALHYPERTWIST_API UHyperTwistBrowserBridgeObject : public UObject
|
||||
{
|
||||
|
|
@ -42,6 +44,8 @@ public:
|
|||
UPROPERTY(BlueprintReadOnly, Category = "HyperTwist|Browser")
|
||||
FString LastRuntimeReadyJson;
|
||||
|
||||
FHyperTwistBrowserRuntimeReadyDelegate OnRuntimeReady;
|
||||
|
||||
UPROPERTY(Transient)
|
||||
FHyperTwistTrainingTilingStateEnvelope LastTilingEnvelope;
|
||||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,15 @@ public:
|
|||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Browser")
|
||||
FString GetLastStateJson() const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Browser")
|
||||
int32 GetPendingCommandCount() const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Browser")
|
||||
bool HasPendingShellStateJson() const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Browser")
|
||||
bool IsBrowserRuntimeReady() const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Browser")
|
||||
bool TryGetLastTilingEnvelope(
|
||||
FHyperTwistTrainingTilingStateEnvelope& OutStateEnvelope
|
||||
|
|
@ -67,8 +76,17 @@ protected:
|
|||
virtual TSharedRef<SWidget> RebuildWidget() override;
|
||||
virtual void ReleaseSlateResources(bool bReleaseChildren) override;
|
||||
virtual void SynchronizeProperties() override;
|
||||
virtual bool IsBrowserRuntimeDispatchReady() const;
|
||||
virtual void ExecuteBrowserRuntimeScript(const FString& Script);
|
||||
|
||||
private:
|
||||
void HandleBrowserRuntimeReady(const FString& RuntimeReadyJson);
|
||||
void FlushPendingBrowserMessages();
|
||||
void QueueCommandJson(const FString& CommandJson);
|
||||
void QueueShellStateJson(const FString& StateJson);
|
||||
void ResetBrowserRuntimeHandshake();
|
||||
FString BuildCommandDispatchScript(const FString& CommandJson) const;
|
||||
FString BuildShellStateDispatchScript(const FString& StateJson) const;
|
||||
static FString EscapeForJavaScriptSingleQuotedString(const FString& Input);
|
||||
FString ResolveInitialUrl() const;
|
||||
void EnsureBridgeBound();
|
||||
|
|
@ -76,5 +94,29 @@ private:
|
|||
UPROPERTY(Transient)
|
||||
TObjectPtr<UHyperTwistBrowserBridgeObject> BrowserBridgeObject = nullptr;
|
||||
|
||||
UPROPERTY(Transient)
|
||||
TArray<FString> PendingCommandJsonQueue;
|
||||
|
||||
UPROPERTY(Transient)
|
||||
FString PendingShellStateJson;
|
||||
|
||||
UPROPERTY(Transient)
|
||||
bool bHasPendingShellStateJson = false;
|
||||
|
||||
UPROPERTY(Transient)
|
||||
bool bBrowserRuntimeReady = false;
|
||||
|
||||
TSharedPtr<SWebBrowser> BrowserWidget;
|
||||
FDelegateHandle RuntimeReadyDelegateHandle;
|
||||
|
||||
#if WITH_AUTOMATION_TESTS
|
||||
public:
|
||||
void SetBrowserRuntimeDispatchReadyForTesting(bool bInDispatchReady);
|
||||
void SimulateBrowserRuntimeReadyForTesting(const FString& RuntimeReadyJson);
|
||||
const TArray<FString>& GetExecutedBrowserScriptsForTesting() const;
|
||||
|
||||
private:
|
||||
bool bForceBrowserRuntimeDispatchReadyForTesting = false;
|
||||
TArray<FString> ExecutedBrowserScriptsForTesting;
|
||||
#endif
|
||||
};
|
||||
|
|
|
|||
|
|
@ -84,6 +84,53 @@ bool FHyperTwistBrowserBridgeObjectNotificationTest::RunTest(const FString& Para
|
|||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FHyperTwistBrowserBridgeObjectRuntimeReadyDelegateTest,
|
||||
"HyperTwist.Browser.Bridge.RuntimeReadyDelegate",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
|
||||
)
|
||||
|
||||
bool FHyperTwistBrowserBridgeObjectRuntimeReadyDelegateTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
UHyperTwistBrowserBridgeObject* BridgeObject = NewObject<UHyperTwistBrowserBridgeObject>();
|
||||
TestNotNull(TEXT("The browser bridge object must be constructible."), BridgeObject);
|
||||
if (BridgeObject == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int32 RuntimeReadyBroadcastCount = 0;
|
||||
FString LastRuntimeReadyPayload;
|
||||
const FDelegateHandle DelegateHandle = BridgeObject->OnRuntimeReady.AddLambda(
|
||||
[&RuntimeReadyBroadcastCount, &LastRuntimeReadyPayload](const FString& RuntimeReadyJson)
|
||||
{
|
||||
++RuntimeReadyBroadcastCount;
|
||||
LastRuntimeReadyPayload = RuntimeReadyJson;
|
||||
}
|
||||
);
|
||||
|
||||
BridgeObject->NotifyRuntimeReady(TEXT("{\"status\":\"ready\",\"mode\":\"fallback-js\"}"));
|
||||
|
||||
TestEqual(
|
||||
TEXT("Runtime-ready notification must be retained on the bridge object."),
|
||||
BridgeObject->LastRuntimeReadyJson,
|
||||
TEXT("{\"status\":\"ready\",\"mode\":\"fallback-js\"}")
|
||||
);
|
||||
TestEqual(
|
||||
TEXT("Runtime-ready notification must broadcast once to bridge listeners."),
|
||||
RuntimeReadyBroadcastCount,
|
||||
1
|
||||
);
|
||||
TestEqual(
|
||||
TEXT("Runtime-ready notification must preserve the broadcast payload."),
|
||||
LastRuntimeReadyPayload,
|
||||
TEXT("{\"status\":\"ready\",\"mode\":\"fallback-js\"}")
|
||||
);
|
||||
|
||||
BridgeObject->OnRuntimeReady.Remove(DelegateHandle);
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FHyperTwistBrowserBridgeObjectTilingStateEnvelopeDecodeTest,
|
||||
"HyperTwist.Browser.Bridge.TilingStateEnvelopeDecode",
|
||||
|
|
@ -155,6 +202,81 @@ bool FHyperTwistBrowserBridgeObjectTilingStateEnvelopeDecodeTest::RunTest(
|
|||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FHyperTwistBrowserWidgetQueuedOutboundMessagesTest,
|
||||
"HyperTwist.Browser.Widget.QueuedOutboundMessages",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
|
||||
)
|
||||
|
||||
bool FHyperTwistBrowserWidgetQueuedOutboundMessagesTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
UHyperTwistBrowserWidget* BrowserWidget = NewObject<UHyperTwistBrowserWidget>();
|
||||
TestNotNull(TEXT("The browser widget must be constructible."), BrowserWidget);
|
||||
if (BrowserWidget == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
BrowserWidget->DispatchCommandJson(TEXT("{\"command\":\"alpha\"}"));
|
||||
BrowserWidget->PushShellStateJson(TEXT("{\"phase\":\"7A\"}"));
|
||||
BrowserWidget->DispatchCommandJson(TEXT("{\"command\":\"beta\"}"));
|
||||
|
||||
TestEqual(
|
||||
TEXT("Command payloads must queue while the browser runtime is not ready."),
|
||||
BrowserWidget->GetPendingCommandCount(),
|
||||
2
|
||||
);
|
||||
TestTrue(
|
||||
TEXT("The last shell-state payload must stay queued while the browser runtime is not ready."),
|
||||
BrowserWidget->HasPendingShellStateJson()
|
||||
);
|
||||
TestFalse(
|
||||
TEXT("A fresh browser widget must begin with runtime-ready false."),
|
||||
BrowserWidget->IsBrowserRuntimeReady()
|
||||
);
|
||||
|
||||
BrowserWidget->SetBrowserRuntimeDispatchReadyForTesting(true);
|
||||
BrowserWidget->SimulateBrowserRuntimeReadyForTesting(TEXT("{\"status\":\"ready\"}"));
|
||||
|
||||
TestTrue(
|
||||
TEXT("Runtime-ready simulation must mark the browser widget ready."),
|
||||
BrowserWidget->IsBrowserRuntimeReady()
|
||||
);
|
||||
TestEqual(
|
||||
TEXT("Queued command payloads must flush when runtime-ready arrives."),
|
||||
BrowserWidget->GetPendingCommandCount(),
|
||||
0
|
||||
);
|
||||
TestFalse(
|
||||
TEXT("Queued shell-state payload must flush when runtime-ready arrives."),
|
||||
BrowserWidget->HasPendingShellStateJson()
|
||||
);
|
||||
|
||||
const TArray<FString>& ExecutedScripts = BrowserWidget->GetExecutedBrowserScriptsForTesting();
|
||||
TestEqual(
|
||||
TEXT("Flushing the queued outbound payloads must execute one shell-state script plus both queued commands."),
|
||||
ExecutedScripts.Num(),
|
||||
3
|
||||
);
|
||||
if (ExecutedScripts.Num() == 3)
|
||||
{
|
||||
TestTrue(
|
||||
TEXT("Shell-state must flush before queued commands so the browser runtime sees the latest state first."),
|
||||
ExecutedScripts[0].Contains(TEXT("setShellState"))
|
||||
);
|
||||
TestTrue(
|
||||
TEXT("The first queued command must flush through receiveCommand."),
|
||||
ExecutedScripts[1].Contains(TEXT("receiveCommand"))
|
||||
);
|
||||
TestTrue(
|
||||
TEXT("The second queued command must also flush through receiveCommand."),
|
||||
ExecutedScripts[2].Contains(TEXT("receiveCommand"))
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FHyperTwistBrowserWidgetBundledShellUrlTest,
|
||||
"HyperTwist.Browser.Widget.BundledShellUrl",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,107 @@
|
|||
# HyperTwist Phase 8A embedded browser runtime hardening implementation packet
|
||||
|
||||
Created on `2026-06-19`
|
||||
|
||||
## Status
|
||||
|
||||
- first-party HyperTwist packet
|
||||
- bounded first-party `Phase 8A` continuation slice
|
||||
|
||||
## Purpose
|
||||
|
||||
This packet hardens the already-landed embedded browser shipping lane without
|
||||
widening into a browser-first topology change or any native renderer branch.
|
||||
|
||||
The landed slice is:
|
||||
|
||||
- queue outbound Unreal command payloads until the embedded browser runtime
|
||||
reports ready
|
||||
- retain the latest outbound shell-state payload until runtime-ready, then
|
||||
flush that shell state before queued commands
|
||||
- broadcast browser runtime-ready notifications through the first-party bridge
|
||||
object so the widget can complete a deterministic handshake
|
||||
- add focused automation proving the queue, flush order, and runtime-ready
|
||||
bridge signal
|
||||
|
||||
It is not:
|
||||
|
||||
- a full-browser client implementation
|
||||
- a new browser authority inversion over Unreal runtime state
|
||||
- a native `MagicTile` renderer widening packet
|
||||
|
||||
## Current authority basis
|
||||
|
||||
This implementation packet stands on:
|
||||
|
||||
- `docs/ops/HYPERTWIST_COMPREHENSIVE_RECONSTRUCTION_ROADMAP_2026-06-10.md`
|
||||
- `docs/v6_5_deep_manual_pack/HyperTwist/ROADMAP.md`
|
||||
- `docs/v6_5_deep_manual_pack/HyperTwist/ARCHITECTURE.md`
|
||||
- `docs/v6_5_deep_manual_pack/HyperTwist/FEATURE_REGISTRY.md`
|
||||
- `docs/arch/HYPERTWIST_PHASE7C_MAGICTILE_NATIVE_BEHAVIOR_PROOF_IMPLEMENTATION_PACKET_2026-06-19.md`
|
||||
- `docs/arch/HYPERTWIST_PHASE7D_MAGICTILE_RENDERER_PORT_DECISION_PACKET_2026-06-19.md`
|
||||
- `docs/arch/HYPERTWIST_PHASE8B_OPTIONAL_FULL_BROWSER_CLIENT_HTTP_BACKEND_PREPARATION_PACKET_2026-06-19.md`
|
||||
|
||||
## Landed scope
|
||||
|
||||
Current code now hardens the live embedded-browser lane through:
|
||||
|
||||
- `UHyperTwistBrowserWidget` outbound queue ownership for:
|
||||
- pending command JSON payloads
|
||||
- latest pending shell-state JSON payload
|
||||
- runtime-ready handshake state
|
||||
- first-party flush ordering that applies shell state before queued commands
|
||||
once the browser runtime is ready
|
||||
- `UHyperTwistBrowserBridgeObject` runtime-ready delegate broadcast
|
||||
- focused automation in `HyperTwistBrowserBridgeObjectTest.cpp` for:
|
||||
- runtime-ready delegate retention/broadcast
|
||||
- queued outbound browser messages and flush ordering
|
||||
|
||||
## Why this hardening mattered
|
||||
|
||||
The earlier browser-runtime bootstrap already queued messages once the browser
|
||||
JavaScript runtime existed. The remaining deterministic gap was earlier in the
|
||||
handshake: Unreal could still try to send command or shell-state payloads
|
||||
before the embedded widget/runtime bridge was actually ready to receive them.
|
||||
|
||||
This packet closes that gap on the Unreal side while preserving the already
|
||||
landed first-party browser shell and bridge topology.
|
||||
|
||||
## Validation
|
||||
|
||||
Windows build validation:
|
||||
|
||||
- `Build.bat UnrealHyperTwistEditor Win64 Development -Project='C:\HyperTwist_worktrees\phase10validate\UnrealHyperTwist\UnrealHyperTwist.uproject' -WaitMutex -NoHotReloadFromIDE -NoUba`
|
||||
- result on `2026-06-19`: `Result: Succeeded` with UnrealBuildTool
|
||||
`Total execution time: 96.96 seconds` on isolated worktree
|
||||
`C:\HyperTwist_worktrees\phase10validate`
|
||||
|
||||
Focused browser validation:
|
||||
|
||||
- `Automation RunTests HyperTwist.Browser`
|
||||
- result on `2026-06-19`: exported
|
||||
`Saved\AutomationReports\Browser-Hardening-Verify` with all `6`
|
||||
`HyperTwist.Browser.*` tests passing, including:
|
||||
- `Bridge.RuntimeReadyDelegate`
|
||||
- `Widget.QueuedOutboundMessages`
|
||||
|
||||
Adjacent regression validation:
|
||||
|
||||
- `Automation RunTests HyperTwist.FirstParty.MagicTile`
|
||||
- `Automation RunTests HyperTwist.Permissive.MagicTile`
|
||||
- result on `2026-06-19`: exported
|
||||
`Saved\AutomationReports\Phase7-MagicTile-BrowserHardening` with all `7`
|
||||
`HyperTwist.FirstParty.MagicTile.*` tests passing and
|
||||
`Saved\AutomationReports\Phase7-Permissive-MagicTile-BrowserHardening` with
|
||||
all `6` `HyperTwist.Permissive.MagicTile.*` tests passing
|
||||
|
||||
## Queue effect
|
||||
|
||||
This packet keeps the live embedded browser/CEF shipping lane intact, but it
|
||||
raises its determinism and test coverage:
|
||||
|
||||
- Unreal-side browser startup is now tolerant of early outbound traffic before
|
||||
runtime-ready
|
||||
- browser bridge readiness is now a first-party signal rather than a silent
|
||||
assumption
|
||||
- current browser/MagicTile validation grows from earlier green proof into a
|
||||
stronger shipping-lane hardening packet without reopening renderer decisions
|
||||
|
|
@ -420,6 +420,9 @@ Closure read:
|
|||
- [x] Add `WebBrowserWidget` plugin to `.uproject`
|
||||
- [x] Create `UHyperTwistBrowserWidget` wrapping `SWebBrowser`
|
||||
- [x] Load the authoritative local browser shell in `Content/Browser/index.html`, with bundled-runtime upgrade plus clean-checkout plain-JS fallback
|
||||
- [x] Queue outbound Unreal command and shell-state payloads until the embedded browser runtime reports ready, then flush shell state before queued commands
|
||||
- Hardening continuation on `2026-06-19`: current code now keeps the live embedded browser lane deterministic above the already-landed shell/bootstrap path by retaining pending widget outbound traffic until the first-party runtime-ready bridge fires, then flushing the latest shell-state payload before queued commands through the browser runtime
|
||||
- Validation evidence on `2026-06-19`: the primary reverse-SSH `localhost:22022` lane rebuilt isolated worktree `C:\HyperTwist_worktrees\phase10validate` with `Result: Succeeded` and UnrealBuildTool `Total execution time: 96.96 seconds`, then exported green automation reports for `6` `HyperTwist.Browser.*` tests at `Saved\AutomationReports\Browser-Hardening-Verify`, `7` `HyperTwist.FirstParty.MagicTile.*` tests at `Saved\AutomationReports\Phase7-MagicTile-BrowserHardening`, and `6` `HyperTwist.Permissive.MagicTile.*` tests at `Saved\AutomationReports\Phase7-Permissive-MagicTile-BrowserHardening`
|
||||
|
||||
### 8B — State Synchronization
|
||||
- [x] Bridge UE cube state to browser via embedded Unreal bridge object plus JavaScript `postMessage` fallback
|
||||
|
|
|
|||
|
|
@ -94,6 +94,11 @@ Current shipping browser posture remains the landed embedded browser/CEF shell
|
|||
inside Unreal through `UHyperTwistBrowserWidget` and the authoritative
|
||||
`Content/Browser/index.html` surface.
|
||||
|
||||
That live shipping lane was further hardened on `2026-06-19`: Unreal-side
|
||||
outbound command and shell-state payloads now queue until the first-party
|
||||
browser runtime-ready bridge arrives, then flush shell state before queued
|
||||
commands so the embedded browser lane stays deterministic across startup.
|
||||
|
||||
The optional full-browser client path now exists only as a spec-defined
|
||||
first-party branch:
|
||||
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ Do not collapse those three tiers into one undifferentiated "features" voice.
|
|||
| First-party provider-neutral custom-endpoint runtime routing | Implemented now | landed first-party `Phase 6R-AI` | First-party per-session provider-profile endpoint selection, runtime routing, fallback-health reflection, and bounded provider-backed transport-failure posture are now live above the landed provider-profile/BYOK and provider-routing seams. Provider-specific overlays, payment execution, provider-portal ownership, and payload shipping remain deferred. |
|
||||
| Analytics/reporting surfaces | Implemented now | landed analytics/reporting packets | Reporting is real, but bounded to accepted retained slices. |
|
||||
| Rewritten training analytics and report reference grounding | Implemented now | `apache/echarts` retained permissive lane + first-party current code | Current live `Training Analytics` reference side includes four rewritten first-party targets grounded in retained `apache/echarts`: session outcome and progress reporting, analytics data-view and export, timing-trend history and overview interaction, and the optional richer explainer or sidecar boundary. This does not displace the landed `Phase 3R-C` first-party analytics/reporting owner or elevate `ecomfe/echarts-gl` and `ecomfe/zrender` beyond support-only sidecars. |
|
||||
| Browser/spatial/media adjunct surfaces | Implemented now | landed `three.js`, `react-three-fiber`, `xr`, `model-viewer`, `remotion` packets | These are implemented bounded families, and the current Phase 1 browser-runtime landing now includes a first-party authoritative `Content/Browser/index.html` shell, bundled-runtime upgrade path, committed clean-checkout plain-JS fallback runtime, and embedded `UHyperTwistBrowserWidget` bridge. This still is not proof of unlimited browser-shell parity. |
|
||||
| Browser/spatial/media adjunct surfaces | Implemented now | landed `three.js`, `react-three-fiber`, `xr`, `model-viewer`, `remotion` packets | These are implemented bounded families, and the current Phase 1 browser-runtime landing now includes a first-party authoritative `Content/Browser/index.html` shell, bundled-runtime upgrade path, committed clean-checkout plain-JS fallback runtime, embedded `UHyperTwistBrowserWidget` bridge, and the `2026-06-19` runtime-ready queue hardening that retains outbound Unreal shell traffic until the browser runtime is ready. This still is not proof of unlimited browser-shell parity. |
|
||||
| Optional full-browser client path above Unreal backend authority | Deep-source grounded retained | landed first-party `Phase 8B` preparation packet | This branch is now defined only as a spec-only first-party option: a browser-owned rendering and input shell may sit above explicit Unreal backend seams while Unreal remains authoritative for puzzle, training, timer, replay, and persistence state. The landed embedded browser/CEF shell remains the current shipping posture, and this branch stays separate from any native `MagicTile` behavior or renderer widening. |
|
||||
| Rewritten browser spatial scene and renderer reference grounding | Implemented now | `mrdoob/three.js` retained permissive lane + first-party current code | Current live `Browser 3D and XR Support` reference side includes one rewritten first-party target grounded in retained `mrdoob/three.js`: the browser spatial scene and renderer contract. This does not displace the landed `Phase 3R-F` first-party browser spatial owner trio or absorb the adjacent `react-three-fiber` renderer-bridge and `xr` session slices. |
|
||||
| Rewritten React-side browser renderer and event-bridge grounding | Implemented now | `pmndrs/react-three-fiber` retained permissive lane + first-party current code | Current live `Browser 3D and XR Support` reference side includes one rewritten first-party target grounded in retained `react-three-fiber`: the React scene renderer and event bridge. This does not displace the landed `Phase 3R-F` first-party browser spatial owner trio or absorb the adjacent `three.js` scene substrate and `xr` session slices. |
|
||||
|
|
|
|||
|
|
@ -107,6 +107,12 @@ Current consolidated milestone snapshot:
|
|||
scored against the gate and kept native renderer widening at explicit
|
||||
`No-Go` because no real browser-host failure or bounded renderer target is
|
||||
currently evidenced
|
||||
- the current embedded browser shipping lane was also hardened on `2026-06-19`
|
||||
so Unreal-side outbound browser traffic now queues until runtime-ready,
|
||||
flushes shell state before commands, and remains live-validated on primary
|
||||
reverse-SSH lane `localhost:22022` with `6` `HyperTwist.Browser.*`, `7`
|
||||
`HyperTwist.FirstParty.MagicTile.*`, and `6`
|
||||
`HyperTwist.Permissive.MagicTile.*` tests green
|
||||
- classic-cube `Phase 9A` replay recording is now closed through first-party
|
||||
runtime capture, `.json` replay persistence, local playback reconstruction,
|
||||
schema-light replay normalization across save/load/viewer import, and live
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue