mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 22:11:38 +00:00
Make "Always" confirmation behavior setting work by handling it for ongoing sync patches
- Add "Always" option to confirmationBehavior setting - Route WebSocket patches through the confirmation callback - Fix UI not transitioning back to Connected after confirming ongoing patches - Fix DomLabel Flipper motor cleanup on unmount to prevent errors
This commit is contained in:
parent
a7f36fab5e
commit
a05acf8d35
7 changed files with 638 additions and 90 deletions
|
|
@ -105,6 +105,10 @@ function ServeSession.new(options)
|
|||
__instanceMap = instanceMap,
|
||||
__changeBatcher = changeBatcher,
|
||||
__statusChangedCallback = nil,
|
||||
__userConfirmCallback = nil,
|
||||
__patchUpdateCallback = nil,
|
||||
__serverInfo = nil,
|
||||
__confirmingPatch = nil,
|
||||
__connections = connections,
|
||||
__precommitCallbacks = {},
|
||||
__postcommitCallbacks = {},
|
||||
|
|
@ -139,6 +143,10 @@ function ServeSession:setConfirmCallback(callback)
|
|||
self.__userConfirmCallback = callback
|
||||
end
|
||||
|
||||
function ServeSession:setPatchUpdateCallback(callback)
|
||||
self.__patchUpdateCallback = callback
|
||||
end
|
||||
|
||||
function ServeSession:setUpdateLoadingTextCallback(callback)
|
||||
self.__updateLoadingText = callback
|
||||
end
|
||||
|
|
@ -189,6 +197,62 @@ function ServeSession:hookPostcommit(callback)
|
|||
end
|
||||
end
|
||||
|
||||
function ServeSession:__onWebSocketMessage(messagesPacket)
|
||||
if self.__status == Status.Disconnected then
|
||||
return
|
||||
end
|
||||
|
||||
Log.debug("Received {} messages from Rojo server", #messagesPacket.messages)
|
||||
|
||||
-- Combine all messages into a single patch
|
||||
local combinedPatch = PatchSet.newEmpty()
|
||||
for _, message in messagesPacket.messages do
|
||||
PatchSet.assign(combinedPatch, message)
|
||||
end
|
||||
|
||||
-- If we're already waiting for confirmation, merge into the patch being confirmed
|
||||
if self.__confirmingPatch ~= nil then
|
||||
Log.trace("Already confirming, merging into current patch")
|
||||
PatchSet.merge(self.__confirmingPatch, combinedPatch, self.__instanceMap)
|
||||
-- Notify the UI to update the displayed patch
|
||||
if self.__patchUpdateCallback ~= nil then
|
||||
self.__patchUpdateCallback(self.__instanceMap, self.__confirmingPatch)
|
||||
end
|
||||
self.__apiContext:setMessageCursor(messagesPacket.messageCursor)
|
||||
return
|
||||
end
|
||||
|
||||
-- Set confirming patch before spawning to prevent race condition
|
||||
-- where another message arrives before the spawned thread starts
|
||||
self.__confirmingPatch = combinedPatch
|
||||
|
||||
-- Spawn a new thread to handle potentially yielding confirmation
|
||||
task.spawn(function()
|
||||
Log.trace("Processing WebSocket patch, callback exists: {}", self.__userConfirmCallback ~= nil)
|
||||
Log.trace("ServerInfo exists: {}", self.__serverInfo ~= nil)
|
||||
|
||||
local userDecision = "Accept"
|
||||
if self.__userConfirmCallback ~= nil then
|
||||
userDecision = self.__userConfirmCallback(self.__instanceMap, combinedPatch, self.__serverInfo)
|
||||
end
|
||||
self.__confirmingPatch = nil
|
||||
|
||||
Log.trace("WebSocket patch decision: {}", userDecision)
|
||||
|
||||
if userDecision == "Abort" then
|
||||
self:__stopInternal("Aborted Rojo sync operation")
|
||||
return
|
||||
elseif userDecision == "Accept" then
|
||||
-- combinedPatch may have been updated with additional changes
|
||||
-- that arrived while we were waiting for confirmation
|
||||
self:__applyPatch(combinedPatch)
|
||||
end
|
||||
-- Note: "Reject" is not handled for ongoing patches, only for initial sync
|
||||
end)
|
||||
|
||||
self.__apiContext:setMessageCursor(messagesPacket.messageCursor)
|
||||
end
|
||||
|
||||
function ServeSession:start()
|
||||
self:__setStatus(Status.Connecting)
|
||||
self:setLoadingText("Connecting to server...")
|
||||
|
|
@ -202,25 +266,26 @@ function ServeSession:start()
|
|||
self.__syncSourceOnly = serverInfo.syncSourceOnly or false
|
||||
self.__changeBatcher:setSyncSourceOnly(self.__syncSourceOnly)
|
||||
|
||||
return self:__initialSync(serverInfo):andThen(function()
|
||||
self.__serverInfo = serverInfo
|
||||
return self:__computeInitialPatch(serverInfo):andThen(function(catchUpPatch)
|
||||
self:setLoadingText("Starting sync loop...")
|
||||
self:__setStatus(Status.Connected, serverInfo.projectName)
|
||||
self:__applyGameAndPlaceId(serverInfo)
|
||||
|
||||
return self.__apiContext:connectWebSocket({
|
||||
-- Connect WebSocket BEFORE showing confirmation so changes
|
||||
-- during confirmation can be merged into the patch
|
||||
-- Note: connectWebSocket returns a Promise that only resolves when
|
||||
-- the connection closes, so we don't chain .andThen() on it
|
||||
self.__apiContext:connectWebSocket({
|
||||
["messages"] = function(messagesPacket)
|
||||
if self.__status == Status.Disconnected then
|
||||
return
|
||||
end
|
||||
|
||||
Log.debug("Received {} messages from Rojo server", #messagesPacket.messages)
|
||||
|
||||
for _, message in messagesPacket.messages do
|
||||
self:__applyPatch(message)
|
||||
end
|
||||
self.__apiContext:setMessageCursor(messagesPacket.messageCursor)
|
||||
self:__onWebSocketMessage(messagesPacket)
|
||||
end,
|
||||
})
|
||||
}):catch(function(err)
|
||||
if self.__status ~= Status.Disconnected then
|
||||
self:__stopInternal(err)
|
||||
end
|
||||
end)
|
||||
|
||||
-- Now show confirmation and wait for user decision
|
||||
return self:__confirmAndApplyInitialPatch(catchUpPatch, serverInfo)
|
||||
end)
|
||||
end)
|
||||
:catch(function(err)
|
||||
|
|
@ -471,7 +536,7 @@ function ServeSession:__applyPatch(patch)
|
|||
end
|
||||
end
|
||||
|
||||
function ServeSession:__initialSync(serverInfo)
|
||||
function ServeSession:__computeInitialPatch(serverInfo)
|
||||
return self.__apiContext:read({ serverInfo.rootInstanceId }):andThen(function(readResponseBody)
|
||||
-- Tell the API Context that we're up-to-date with the version of
|
||||
-- the tree defined in this response.
|
||||
|
|
@ -509,62 +574,72 @@ function ServeSession:__initialSync(serverInfo)
|
|||
|
||||
Log.trace("Computed hydration patch: {:#?}", debugPatch(catchUpPatch))
|
||||
|
||||
local userDecision = "Accept"
|
||||
if self.__userConfirmCallback ~= nil then
|
||||
userDecision = self.__userConfirmCallback(self.__instanceMap, catchUpPatch, serverInfo)
|
||||
return catchUpPatch
|
||||
end)
|
||||
end
|
||||
|
||||
function ServeSession:__confirmAndApplyInitialPatch(catchUpPatch, serverInfo)
|
||||
local userDecision = "Accept"
|
||||
if self.__userConfirmCallback ~= nil then
|
||||
-- Set confirming patch so WebSocket messages that arrive during
|
||||
-- confirmation can be merged into this patch
|
||||
self.__confirmingPatch = catchUpPatch
|
||||
userDecision = self.__userConfirmCallback(self.__instanceMap, catchUpPatch, serverInfo)
|
||||
self.__confirmingPatch = nil
|
||||
end
|
||||
|
||||
if userDecision == "Abort" then
|
||||
return Promise.reject("Aborted Rojo sync operation")
|
||||
elseif userDecision == "Reject" then
|
||||
if not self.__twoWaySync then
|
||||
return Promise.reject("Cannot reject sync operation without two-way sync enabled")
|
||||
end
|
||||
-- The user wants their studio DOM to write back to their Rojo DOM
|
||||
-- so we will reverse the patch and send it back
|
||||
|
||||
if userDecision == "Abort" then
|
||||
return Promise.reject("Aborted Rojo sync operation")
|
||||
elseif userDecision == "Reject" then
|
||||
if not self.__twoWaySync then
|
||||
return Promise.reject("Cannot reject sync operation without two-way sync enabled")
|
||||
local inversePatch = PatchSet.newEmpty()
|
||||
|
||||
-- Send back the current properties
|
||||
for _, change in catchUpPatch.updated do
|
||||
local instance = self.__instanceMap.fromIds[change.id]
|
||||
if not instance then
|
||||
continue
|
||||
end
|
||||
-- The user wants their studio DOM to write back to their Rojo DOM
|
||||
-- so we will reverse the patch and send it back
|
||||
|
||||
local inversePatch = PatchSet.newEmpty()
|
||||
|
||||
-- Send back the current properties
|
||||
for _, change in catchUpPatch.updated do
|
||||
local instance = self.__instanceMap.fromIds[change.id]
|
||||
if not instance then
|
||||
local propertiesToSync = change.changedProperties
|
||||
if self.__syncSourceOnly then
|
||||
-- Filter to only Source property when server is in source-only mode
|
||||
if propertiesToSync.Source then
|
||||
propertiesToSync = { Source = propertiesToSync.Source }
|
||||
else
|
||||
continue
|
||||
end
|
||||
|
||||
local propertiesToSync = change.changedProperties
|
||||
if self.__syncSourceOnly then
|
||||
-- Filter to only Source property when server is in source-only mode
|
||||
if propertiesToSync.Source then
|
||||
propertiesToSync = { Source = propertiesToSync.Source }
|
||||
else
|
||||
continue
|
||||
end
|
||||
end
|
||||
|
||||
local update = encodePatchUpdate(instance, change.id, propertiesToSync)
|
||||
if update then
|
||||
table.insert(inversePatch.updated, update)
|
||||
end
|
||||
end
|
||||
-- Add the removed instances back to Rojo
|
||||
-- selene:allow(empty_if, unused_variable, empty_loop)
|
||||
for _, instance in catchUpPatch.removed do
|
||||
-- TODO: Generate ID for our instance and add it to inversePatch.added
|
||||
end
|
||||
-- Remove the additions we've rejected
|
||||
for id, _change in catchUpPatch.added do
|
||||
table.insert(inversePatch.removed, id)
|
||||
end
|
||||
|
||||
return self.__apiContext:write(inversePatch)
|
||||
elseif userDecision == "Accept" then
|
||||
self:__applyPatch(catchUpPatch)
|
||||
return Promise.resolve()
|
||||
else
|
||||
return Promise.reject("Invalid user decision: " .. userDecision)
|
||||
local update = encodePatchUpdate(instance, change.id, propertiesToSync)
|
||||
if update then
|
||||
table.insert(inversePatch.updated, update)
|
||||
end
|
||||
end
|
||||
end)
|
||||
-- Add the removed instances back to Rojo
|
||||
-- selene:allow(empty_if, unused_variable, empty_loop)
|
||||
for _, instance in catchUpPatch.removed do
|
||||
-- TODO: Generate ID for our instance and add it to inversePatch.added
|
||||
end
|
||||
-- Remove the additions we've rejected
|
||||
for id, _change in catchUpPatch.added do
|
||||
table.insert(inversePatch.removed, id)
|
||||
end
|
||||
|
||||
return self.__apiContext:write(inversePatch)
|
||||
elseif userDecision == "Accept" then
|
||||
self:__setStatus(Status.Connected, serverInfo.projectName)
|
||||
self:__applyGameAndPlaceId(serverInfo)
|
||||
self:__applyPatch(catchUpPatch)
|
||||
return Promise.resolve()
|
||||
else
|
||||
return Promise.reject("Invalid user decision: " .. userDecision)
|
||||
end
|
||||
end
|
||||
|
||||
function ServeSession:__stopInternal(err)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue