-
Notifications
You must be signed in to change notification settings - Fork 182
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
When a patch fails, import the Instance directly using GetObjects
#786
base: master
Are you sure you want to change the base?
Changes from all commits
a67aec4
0017861
70b1899
ef90926
a5af0e8
7145d70
f2cdcae
24ce366
de3c15b
9c3d84e
c69ebbe
2abac70
e057711
04b3479
af7e372
b8829a0
e778863
190d458
47dbf1b
d77e5f2
e0c4380
d905679
08fd714
e721e6e
2c02328
6d6e0fe
6666e11
d7ace0f
94cf688
385340f
81a5c0f
424b1d0
a5321be
d21ffe1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
# stylua formatting | ||
0f8e1625d572a5fe0f7b5c08653ff92cc837d346 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
local Selection = game:GetService("Selection") | ||
|
||
local Rojo = script:FindFirstAncestor("Rojo") | ||
local invariant = require(script.Parent.Parent.invariant) | ||
|
||
local Log = require(Rojo.Packages.Log) | ||
|
||
local function fetchInstances(idList, instanceMap, apiContext) | ||
return apiContext:fetch(idList):andThen(function(body: { sessionId: string, path: string }) | ||
-- The endpoint `api/fetech/idlist` returns a table that contains | ||
-- `sessionId` and `path`. The value of `path` is the name of a | ||
-- file in the Content folder that may be loaded via `GetObjects`. | ||
local objects = game:GetObjects("rbxasset://" .. body.path) | ||
-- `ReferentMap` is a folder that contains nothing but | ||
-- ObjectValues which will be named after entries in `instanceMap` | ||
-- and have their `Value` property point towards a new Instance | ||
-- that it can be swapped out with. In turn, `reified` is a | ||
-- container for all of the objects pointed to by those instances. | ||
local map = objects[1]:FindFirstChild("ReferentMap") | ||
local reified = objects[1]:FindFirstChild("Reified") | ||
if map == nil then | ||
invariant("The fetch endpoint returned a malformed folder: missing ReferentMap") | ||
end | ||
if reified == nil then | ||
invariant("The fetch endpoint returned a malformed folder: missing Reified") | ||
end | ||
|
||
-- We want to preserve selection between replacements. | ||
local selected = Selection:Get() | ||
local selectedMap = {} | ||
for i, inst in selected do | ||
selectedMap[inst] = i | ||
end | ||
|
||
for _, entry in map:GetChildren() do | ||
if entry:IsA("ObjectValue") then | ||
local key, value = entry.Name, entry.Value | ||
if value == nil or not value:IsDescendantOf(reified) then | ||
invariant("ReferentMap contained entry {} that was parented to an outside source", key) | ||
else | ||
-- This could be a problem if Roblox ever supports | ||
-- parallel access to the DataModel but right now, | ||
-- there's no way this results in a data race. | ||
local oldInstance: Instance = instanceMap.fromIds[key] | ||
instanceMap:insert(key, value) | ||
Log.trace("Swapping Instance {} out", key) | ||
|
||
local oldParent = oldInstance.Parent | ||
local children = oldInstance:GetChildren() | ||
for _, child in children do | ||
child.Parent = value | ||
end | ||
value.Parent = oldParent | ||
if selectedMap[oldInstance] then | ||
-- Swapping section like this preserves order | ||
-- It might not matter, but it's also basically free | ||
-- So we may as well. | ||
selected[selectedMap[oldInstance]] = value | ||
end | ||
|
||
-- So long and thanks for all the fish :-) | ||
oldInstance:Destroy() | ||
end | ||
else | ||
invariant("ReferentMap entry `{}` was a `{}` and not an ObjectValue", entry.Name, entry.ClassName) | ||
end | ||
end | ||
|
||
Selection:Set(selected) | ||
end) | ||
end | ||
|
||
return fetchInstances |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,21 +2,28 @@ | |
This module defines the meat of the Rojo plugin and how it manages tracking | ||
and mutating the Roblox DOM. | ||
]] | ||
local ChangeHistoryService = game:GetService("ChangeHistoryService") | ||
|
||
local Packages = script.Parent.Parent.Packages | ||
local Log = require(Packages.Log) | ||
|
||
local PatchSet = require(script.Parent.PatchSet) | ||
|
||
local fetchInstances = require(script.fetchInstances) | ||
local applyPatch = require(script.applyPatch) | ||
local hydrate = require(script.hydrate) | ||
local diff = require(script.diff) | ||
|
||
local Reconciler = {} | ||
Reconciler.__index = Reconciler | ||
|
||
function Reconciler.new(instanceMap) | ||
function Reconciler.new(instanceMap, apiContext, fetchOnPatchFail: boolean) | ||
local self = { | ||
-- Tracks all of the instances known by the reconciler by ID. | ||
__instanceMap = instanceMap, | ||
-- An API context for sending requests back to the server | ||
__apiContext = apiContext, | ||
__fetchOnPatchFail = fetchOnPatchFail, | ||
__precommitCallbacks = {}, | ||
__postcommitCallbacks = {}, | ||
} | ||
|
@@ -64,8 +71,29 @@ function Reconciler:applyPatch(patch) | |
end | ||
end | ||
|
||
local patchTimestamp = DateTime.now():FormatLocalTime("LTS", "en-us") | ||
|
||
local unappliedPatch = applyPatch(self.__instanceMap, patch) | ||
|
||
if self.__fetchOnPatchFail then | ||
-- TODO Is it worth doing this for additions that fail? | ||
-- It seems unlikely that a valid Instance can't be made with `Instance.new` | ||
-- but can be made using GetObjects | ||
if PatchSet.hasUpdates(unappliedPatch) then | ||
local idList = table.create(#unappliedPatch.updated) | ||
for i, entry in unappliedPatch.updated do | ||
idList[i] = entry.id | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should also check here that the unapplied updates are for properties, otherwise we may try to fetch instances for updates (like metadata) that GetObjects won't help with |
||
end | ||
-- TODO this is destructive to any properties that are | ||
-- overwritten by the user but not known to Rojo. We can probably | ||
-- mitigate that by keeping tabs of any instances we need to swap. | ||
fetchInstances(idList, self.__instanceMap, self.__apiContext) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know if it makes sense to be performing HTTP requests in the reconciler. I suppose the entire fetch process could conceptually be seen as part of patch application, but I think I'd rather have the API call happen in ServeSession (maybe by wrapping Reconciler:applyPatch in a new function ServeSession:__applyPatch) to better follow the existing architecture, to make the control flow a little clearer, and to avoid passing an ApiContext plus a setting to the reconciler There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't love it, but I'll just be honest: this was exploratory surgery on the plugin. I didn't really know where to put it, so I went with the reconciler because it was at least sensible. |
||
table.clear(unappliedPatch.updated) | ||
end | ||
end | ||
|
||
ChangeHistoryService:SetWaypoint("Rojo: Patch " .. patchTimestamp) | ||
|
||
for _, callback in self.__postcommitCallbacks do | ||
local success, err = pcall(callback, patch, self.__instanceMap, unappliedPatch) | ||
if not success then | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure ReferentMap is necessary - it seems a little complex for what it's actually accomplishing
Because our serializers provide ordering guarantees for children, and GetObject insertion reflects this ordering, I think the server could construct a model file with children in the same order as the requested IDs, and then the client can match them back up via this ordering.
Alternatively, if it's uncomfortable to rely on child order this way, then the server could write multiple files, and respond with multiple paths (again, in the same order as the requested IDs), at the expense of multiple file system writes and multiple calls to GetObjects
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I went with ReferentMap because it was the first solution I thought of that avoided a bunch of FS writes and reads, not necessarily because I thought it was the best option. In practice it's not really all that complicated, it just sounds bad when you write it out. I can get behind wanting to simplify it as much as possible though, since it's unusual for Rojo.
There's the potential to write thousands of files to a file system and then load them in very quickly if we use multiple paths. I'm not inherently opposed to that, but I'd want to check what the performance of that was. It's not great as-is with one monolith model, I can imagine it getting really bad with a bunch of them.
I'm going to veto relying on child ordering just on principle. It makes me feel gross.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I overlooked the additional complication caused by instances with parent restrictions, so ordering wouldn't work anyway... it's probably fine