Enhance StdBackend with critical error handling and improve file watcher reliability

- Introduced `WatcherCriticalError` enum to categorize critical errors from the file watcher.
- Added a `CriticalErrorHandler` callback type for custom error handling in the watcher thread.
- Updated `StdBackend` to include a receiver for critical errors, allowing for better error management and logging.
- Refactored the watcher thread to handle errors gracefully instead of terminating the process, improving overall stability.
This commit is contained in:
Joe 2026-01-30 05:02:06 -06:00
parent 78aab0eca4
commit 41363473b2
27 changed files with 409 additions and 173 deletions

View file

@ -56,7 +56,8 @@ local function SelectionOption(props)
return Theme.with(function(theme)
local isSelected = props.isSelected
local bgColor = if isSelected
then (if props.optionType == "push" then Color3.fromHex("27AE60")
then (if props.optionType == "push"
then Color3.fromHex("27AE60")
elseif props.optionType == "pull" then Color3.fromHex("E74C3C")
else Color3.fromHex("7F8C8D"))
else theme.BorderedContainer.BackgroundColor

View file

@ -130,19 +130,27 @@ function TextButton:render()
zIndex = -1,
}),
Background = (style == "Solid" or style == "Warning" or style == "Primary" or style == "Success" or style == "Danger" or style == "Neutral") and e(SlicedImage, {
slice = Assets.Slices.RoundedBackground,
color = bindingUtil.mapLerp(
bindingEnabled,
buttonTheme.Enabled.BackgroundColor,
buttonTheme.Disabled.BackgroundColor
),
transparency = self.props.transparency,
Background = (
style == "Solid"
or style == "Warning"
or style == "Primary"
or style == "Success"
or style == "Danger"
or style == "Neutral"
)
and e(SlicedImage, {
slice = Assets.Slices.RoundedBackground,
color = bindingUtil.mapLerp(
bindingEnabled,
buttonTheme.Enabled.BackgroundColor,
buttonTheme.Disabled.BackgroundColor
),
transparency = self.props.transparency,
size = UDim2.new(1, 0, 1, 0),
size = UDim2.new(1, 0, 1, 0),
zIndex = -2,
}),
zIndex = -2,
}),
Children = Roact.createFragment(self.props[Roact.Children]),
})

View file

@ -125,13 +125,13 @@ function ConfirmingPage:render()
LayoutOrder = 4,
BackgroundTransparency = 1,
}, {
Abort = e(TextButton, {
text = "Abort",
style = "Bordered",
transparency = self.props.transparency,
layoutOrder = 1,
onClick = self.props.onAbort,
}),
Abort = e(TextButton, {
text = "Abort",
style = "Bordered",
transparency = self.props.transparency,
layoutOrder = 1,
onClick = self.props.onAbort,
}),
PullAll = if Settings:get("twoWaySync")
then e(TextButton, {
@ -274,8 +274,7 @@ function ConfirmingPage:render()
}),
}),
}),
})
})
if self.props.createPopup then
return e(StudioPluginGui, {

View file

@ -803,6 +803,23 @@ function App:startSession()
-- Track if this was the initial sync for this project
local isInitialSyncForProject = not self.knownProjects[serverInfo.projectName]
-- One-shot sync: disconnect immediately after confirmation
-- Skip the "Connected" UI state entirely to avoid flash
if Settings:get("oneShotSync") then
local resultType = if type(result) == "table" then result.type else result
if resultType == "Confirm" or resultType == "Accept" then
-- Use task.defer to disconnect after patch is applied (runs after current thread yields)
task.defer(function()
if self.serveSession then
Log.info("One-shot sync: disconnecting after sync")
self:endSession()
end
end)
end
-- Don't transition to Connected UI, just return result and let disconnect happen
return result
end
-- Reset UI state back to Connected after confirmation
-- This is needed for ongoing WebSocket patches where the session
-- is already connected and won't trigger a status change
@ -816,20 +833,6 @@ function App:startSession()
})
end
-- One-shot sync: disconnect after confirmation
if Settings:get("oneShotSync") then
local resultType = if type(result) == "table" then result.type else result
if resultType == "Confirm" or resultType == "Accept" then
-- Delay slightly to allow patch to be applied
task.delay(0.5, function()
if self.serveSession then
Log.info("One-shot sync: disconnecting after sync")
self:endSession()
end
end)
end
end
return result
end)

View file

@ -290,15 +290,17 @@ function ServeSession:start()
-- 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)
self:__onWebSocketMessage(messagesPacket)
end,
}):catch(function(err)
if self.__status ~= Status.Disconnected then
self:__stopInternal(err)
end
end)
self.__apiContext
:connectWebSocket({
["messages"] = function(messagesPacket)
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)
@ -649,7 +651,9 @@ function ServeSession:__confirmAndApplyInitialPatch(catchUpPatch, serverInfo)
-- Process removed items
for _, idOrInstance in catchUpPatch.removed do
local id = if type(idOrInstance) == "string" then idOrInstance else self.__instanceMap.fromInstances[idOrInstance]
local id = if type(idOrInstance) == "string"
then idOrInstance
else self.__instanceMap.fromInstances[idOrInstance]
local selection = selections[id]
if selection == "push" then
-- Apply Rojo removal to Studio