Compare commits

...

9 Commits

Author SHA1 Message Date
Lucien Greathouse
6fc497f95e 0.4.3 2018-04-07 22:54:59 -07:00
Lucien Greathouse
52eea667a7 Make plugin connection much more robust, with better errors 2018-04-07 22:24:42 -07:00
Lucien Greathouse
c2f7e268ff Update changelog 2018-04-07 20:13:07 -07:00
Validark
31e5c558ab Open Http Properties upon HttpEnabled prompt (#58) 2018-04-07 20:10:55 -07:00
Lucien Greathouse
7a7ac9550d Update server dependencies 2018-04-04 23:09:02 -07:00
Lucien Greathouse
4d0fdf0dfd 0.4.2 2018-04-04 23:06:57 -07:00
Lucien Greathouse
b448e8007e Fix duplication 0.4.x duplication bug for good 2018-04-04 23:05:01 -07:00
Lucien Greathouse
bad0e67266 Remove extra spawn in server code 2018-04-04 23:02:46 -07:00
Lucien Greathouse
3dee3dd627 Fix README whitespace inconsistency 2018-04-04 00:14:23 -07:00
12 changed files with 196 additions and 99 deletions

View File

@@ -3,6 +3,15 @@
## Current Master ## Current Master
*No changes* *No changes*
## 0.4.3 (April 7, 2018)
* Plugin now automatically selects `HttpService` if it determines that HTTP isn't enabled ([#58](https://github.com/LPGhatguy/rojo/pull/58))
* Plugin now has much more robust handling and will wipe all state when the server changes.
* This should fix issues that would otherwise be solved by restarting Roblox Studio.
## 0.4.2 (April 4, 2018)
* Fixed final case of duplicated instance insertion, caused by reconciled instances not being inserted into `RouteMap`.
* The reconciler is still not a perfect solution, especially if script instances get moved around without being destroyed. I don't think this can be fixed before a big refactor.
## 0.4.1 (April 1, 2018) ## 0.4.1 (April 1, 2018)
* Merged plugin repository into main Rojo repository for easier tracking. * Merged plugin repository into main Rojo repository for easier tracking.
* Improved `RouteMap` object tracking; this should fix some cases of duplicated instances being synced into the tree. * Improved `RouteMap` object tracking; this should fix some cases of duplicated instances being synced into the tree.

View File

@@ -5,10 +5,10 @@
<div>&nbsp;</div> <div>&nbsp;</div>
<div align="center"> <div align="center">
<a href="https://travis-ci.org/LPGhatguy/rojo"> <a href="https://travis-ci.org/LPGhatguy/rojo">
<img src="https://api.travis-ci.org/LPGhatguy/rojo.svg?branch=master" alt="Travis-CI Build Status" /> <img src="https://api.travis-ci.org/LPGhatguy/rojo.svg?branch=master" alt="Travis-CI Build Status" />
</a> </a>
<img src="https://img.shields.io/badge/latest_version-0.4.1-brightgreen.svg" alt="Current server version" /> <img src="https://img.shields.io/badge/latest_version-0.4.3-brightgreen.svg" alt="Current server version" />
</div> </div>
<hr /> <hr />

View File

@@ -4,32 +4,43 @@ local Config = require(script.Parent.Config)
local Promise = require(script.Parent.Promise) local Promise = require(script.Parent.Promise)
local Version = require(script.Parent.Version) local Version = require(script.Parent.Version)
local Server = {} local Api = {}
Server.__index = Server Api.__index = Api
Api.Error = {
ServerIdMismatch = "ServerIdMismatch",
}
setmetatable(Api.Error, {
__index = function(_, key)
error("Invalid API.Error name " .. key, 2)
end
})
--[[ --[[
Create a new Server using the given HTTP implementation and replacer. Api.connect(Http) -> Promise<Api>
If the context becomes invalid, `replacer` will be invoked with a new Create a new Api using the given HTTP implementation.
context that should be suitable to replace this one.
Attempting to invoke methods on an invalid conext will throw errors! Attempting to invoke methods on an invalid conext will throw errors!
]] ]]
function Server.connect(http) function Api.connect(http)
local context = { local context = {
http = http, http = http,
serverId = nil, serverId = nil,
currentTime = 0, currentTime = 0,
} }
setmetatable(context, Server) setmetatable(context, Api)
return context:_start() return context:_start()
end end
function Server:_start() function Api:_start()
return self:getInfo() return self.http:get("/")
:andThen(function(response) :andThen(function(response)
response = response:json()
if response.protocolVersion ~= Config.protocolVersion then if response.protocolVersion ~= Config.protocolVersion then
local message = ( local message = (
"Found a Rojo dev server, but it's using a different protocol version, and is incompatible." .. "Found a Rojo dev server, but it's using a different protocol version, and is incompatible." ..
@@ -39,7 +50,7 @@ function Server:_start()
"\n\nGo to https://github.com/LPGhatguy/rojo for more details." "\n\nGo to https://github.com/LPGhatguy/rojo for more details."
):format( ):format(
Version.display(Config.version), Config.protocolVersion, Version.display(Config.version), Config.protocolVersion,
Config.expectedServerVersionString, Config.expectedApiVersionString,
response.serverVersion, response.protocolVersion response.serverVersion, response.protocolVersion
) )
@@ -53,37 +64,49 @@ function Server:_start()
end) end)
end end
function Server:getInfo() function Api:getInfo()
return self.http:get("/") return self.http:get("/")
:andThen(function(response) :andThen(function(response)
response = response:json() response = response:json()
if response.serverId ~= self.serverId then
return Promise.reject(Api.Error.ServerIdMismatch)
end
return response return response
end) end)
end end
function Server:read(paths) function Api:read(paths)
local body = HttpService:JSONEncode(paths) local body = HttpService:JSONEncode(paths)
return self.http:post("/read", body) return self.http:post("/read", body)
:andThen(function(response) :andThen(function(response)
response = response:json() response = response:json()
if response.serverId ~= self.serverId then
return Promise.reject(Api.Error.ServerIdMismatch)
end
return response.items return response.items
end) end)
end end
function Server:getChanges() function Api:getChanges()
local url = ("/changes/%f"):format(self.currentTime) local url = ("/changes/%f"):format(self.currentTime)
return self.http:get(url) return self.http:get(url)
:andThen(function(response) :andThen(function(response)
response = response:json() response = response:json()
if response.serverId ~= self.serverId then
return Promise.reject(Api.Error.ServerIdMismatch)
end
self.currentTime = response.currentTime self.currentTime = response.currentTime
return response.changes return response.changes
end) end)
end end
return Server return Api

View File

@@ -1,6 +1,6 @@
return { return {
pollingRate = 0.2, pollingRate = 0.2,
version = {0, 4, 1}, version = {0, 4, 3},
expectedServerVersionString = "0.4.x", expectedServerVersionString = "0.4.x",
protocolVersion = 1, protocolVersion = 1,
dev = false, dev = false,

View File

@@ -52,6 +52,9 @@ end
function HttpError:report() function HttpError:report()
warn(self.message) warn(self.message)
if self.type == HttpError.Error.HttpNotEnabled then
game:GetService("Selection"):Set{game:GetService("HttpService")}
end
end end
return HttpError return HttpError

View File

@@ -11,6 +11,11 @@ local Version = require(script.Parent.Version)
are, show them a reminder to make sure they check their server version. are, show them a reminder to make sure they check their server version.
]] ]]
local function checkUpgrade() local function checkUpgrade()
-- When developing Rojo, there's no use in doing version checks
if Config.dev then
return
end
local lastVersion = plugin:GetSetting("LastRojoVersion") local lastVersion = plugin:GetSetting("LastRojoVersion")
if lastVersion then if lastVersion then
@@ -30,10 +35,7 @@ local function checkUpgrade()
end end
end end
-- When developing Rojo, there's no use in storing that version number. plugin:SetSetting("LastRojoVersion", Config.version)
if not Config.dev then
plugin:SetSetting("LastRojoVersion", Config.version)
end
end end
local function main() local function main()
@@ -67,12 +69,10 @@ local function main()
.Click:Connect(function() .Click:Connect(function()
checkUpgrade() checkUpgrade()
spawn(function() pluginInstance:togglePolling()
pluginInstance:togglePolling() :catch(function(err)
:catch(function(err) warn(err)
warn(err) end)
end)
end)
end) end)
end end

View File

@@ -1,6 +1,6 @@
local Config = require(script.Parent.Config) local Config = require(script.Parent.Config)
local Http = require(script.Parent.Http) local Http = require(script.Parent.Http)
local Server = require(script.Parent.Server) local Api = require(script.Parent.Api)
local Promise = require(script.Parent.Promise) local Promise = require(script.Parent.Promise)
local Reconciler = require(script.Parent.Reconciler) local Reconciler = require(script.Parent.Reconciler)
@@ -26,7 +26,7 @@ function Plugin.new()
local self = { local self = {
_http = Http.new(remote), _http = Http.new(remote),
_reconciler = Reconciler.new(), _reconciler = Reconciler.new(),
_server = nil, _api = nil,
_polling = false, _polling = false,
} }
@@ -57,37 +57,58 @@ function Plugin.new()
return self return self
end end
function Plugin:server() --[[
if not self._server then Clears all state and issues a notice to the user that the plugin has
self._server = Server.connect(self._http) restarted.
]]
function Plugin:restart()
warn("The server has changed since the last request, reloading plugin...")
self._reconciler:clear()
self._api = nil
self._polling = false
end
function Plugin:api()
if not self._api then
self._api = Api.connect(self._http)
:catch(function(err) :catch(function(err)
self._server = nil self._api = nil
return Promise.reject(err) return Promise.reject(err)
end) end)
end end
return self._server return self._api
end end
function Plugin:connect() function Plugin:connect()
print("Testing connection...") print("Testing connection...")
return self:server() return self:api()
:andThen(function(server) :andThen(function(api)
return server:getInfo() local ok, info = api:getInfo():await()
end)
:andThen(function(result) if not ok then
return Promise.reject(info)
end
print("Server found!") print("Server found!")
print("Protocol version:", result.protocolVersion) print("Protocol version:", info.protocolVersion)
print("Server version:", result.serverVersion) print("Server version:", info.serverVersion)
end)
:catch(function(err)
if err == Api.Error.ServerIdMismatch then
self:restart()
return self:connect()
else
return Promise.reject(err)
end
end) end)
end end
function Plugin:togglePolling() function Plugin:togglePolling()
if self._polling then if self._polling then
self:stopPolling() return self:stopPolling()
return Promise.resolve(nil)
else else
return self:startPolling() return self:startPolling()
end end
@@ -95,48 +116,51 @@ end
function Plugin:stopPolling() function Plugin:stopPolling()
if not self._polling then if not self._polling then
return return Promise.resolve(false)
end end
print("Stopped polling.") print("Stopped polling.")
self._polling = false self._polling = false
self._label.Enabled = false self._label.Enabled = false
return Promise.resolve(true)
end end
function Plugin:_pull(server, project, routes) function Plugin:_pull(api, project, routes)
local items = server:read(routes):await() return api:read(routes)
:andThen(function(items)
for index = 1, #routes do
local itemRoute = routes[index]
local partitionName = itemRoute[1]
local partition = project.partitions[partitionName]
local item = items[index]
for index = 1, #routes do local partitionRoute = collectMatch(partition.target, "[^.]+")
local itemRoute = routes[index]
local partitionName = itemRoute[1]
local partition = project.partitions[partitionName]
local item = items[index]
local partitionRoute = collectMatch(partition.target, "[^.]+") -- If the item route's length was 1, we need to rename the instance to
-- line up with the partition's root object name.
--
-- This is a HACK!
if #itemRoute == 1 then
if item then
local objectName = partition.target:match("[^.]+$")
item.Name = objectName
end
end
-- If the item route's length was 1, we need to rename the instance to local fullRoute = {}
-- line up with the partition's root object name. for _, piece in ipairs(partitionRoute) do
-- table.insert(fullRoute, piece)
-- This is a HACK! end
if #itemRoute == 1 then
if item then for i = 2, #itemRoute do
local objectName = partition.target:match("[^.]+$") table.insert(fullRoute, itemRoute[i])
item.Name = objectName end
self._reconciler:reconcileRoute(fullRoute, item, itemRoute)
end end
end end)
local fullRoute = {}
for _, piece in ipairs(partitionRoute) do
table.insert(fullRoute, piece)
end
for i = 2, #itemRoute do
table.insert(fullRoute, itemRoute[i])
end
self._reconciler:reconcileRoute(fullRoute, item, itemRoute)
end
end end
function Plugin:startPolling() function Plugin:startPolling()
@@ -149,14 +173,22 @@ function Plugin:startPolling()
self._polling = true self._polling = true
self._label.Enabled = true self._label.Enabled = true
return self:server() return self:api()
:andThen(function(server) :andThen(function(api)
self:syncIn():await() local syncOk, result = self:syncIn():await()
local project = server:getInfo():await().project if not syncOk then
return Promise.reject(result)
end
local infoOk, info = api:getInfo():await()
if not infoOk then
return Promise.reject(info)
end
while self._polling do while self._polling do
local changes = server:getChanges():await() local changes = api:getChanges():await()
if #changes > 0 then if #changes > 0 then
local routes = {} local routes = {}
@@ -165,34 +197,57 @@ function Plugin:startPolling()
table.insert(routes, change.route) table.insert(routes, change.route)
end end
self:_pull(server, project, routes) local pullOk, pullResult = self:_pull(api, info.project, routes):await()
if not pullOk then
return Promise.reject(pullResult)
end
end end
wait(Config.pollingRate) wait(Config.pollingRate)
end end
end) end)
:catch(function() :catch(function(err)
self:stopPolling() self:stopPolling()
if err == Api.Error.ServerIdMismatch then
self:restart()
return self:startPolling()
else
return Promise.reject(err)
end
end) end)
end end
function Plugin:syncIn() function Plugin:syncIn()
print("Syncing from server...") print("Syncing from server...")
return self:server() return self:api()
:andThen(function(server) :andThen(function(api)
local project = server:getInfo():await().project local ok, info = api:getInfo():await()
if not ok then
return Promise.reject(info)
end
local routes = {} local routes = {}
for name in pairs(project.partitions) do for name in pairs(info.project.partitions) do
table.insert(routes, {name}) table.insert(routes, {name})
end end
self:_pull(server, project, routes) self:_pull(api, info.project, routes)
print("Sync successful!") print("Sync successful!")
end) end)
:catch(function(err)
if err == Api.Error.ServerIdMismatch then
self:restart()
return self:syncIn()
else
return Promise.reject(err)
end
end)
end end
return Plugin return Plugin

View File

@@ -221,15 +221,11 @@ function Promise:await()
local ok = bindable.Event:Wait() local ok = bindable.Event:Wait()
bindable:Destroy() bindable:Destroy()
if not ok then return ok, unpack(result)
error(tostring(result[1]), 2)
end
return unpack(result)
elseif self._status == Promise.Status.Resolved then elseif self._status == Promise.Status.Resolved then
return unpack(self._value) return true, unpack(self._value)
elseif self._status == Promise.Status.Rejected then elseif self._status == Promise.Status.Rejected then
error(tostring(self._value[1]), 2) return false, unpack(self._value)
end end
end end

View File

@@ -124,6 +124,13 @@ function Reconciler:_reify(item)
return rbx return rbx
end end
--[[
Clears any state that the Reconciler has, effectively restarting it.
]]
function Reconciler:clear()
self._routeMap:clear()
end
--[[ --[[
Apply the changes represented by the given item to a Roblox object that's a Apply the changes represented by the given item to a Roblox object that's a
child of the given instance. child of the given instance.
@@ -161,6 +168,10 @@ function Reconciler:reconcile(rbx, item)
-- Use a dumb algorithm for reconciling children -- Use a dumb algorithm for reconciling children
self:_reconcileChildren(rbx, item) self:_reconcileChildren(rbx, item)
if item.Route then
self._routeMap:insert(item.Route, rbx)
end
return rbx return rbx
end end

View File

@@ -71,8 +71,8 @@ function RouteMap:clear()
self._map = {} self._map = {}
self._reverseMap = {} self._reverseMap = {}
for object in pairs(self._connectionsByRbx) do for _, connection in pairs(self._connectionsByRbx) do
object:Disconnect() connection:Disconnect()
end end
self._connectionsByRbx = {} self._connectionsByRbx = {}

2
server/Cargo.lock generated
View File

@@ -597,7 +597,7 @@ dependencies = [
[[package]] [[package]]
name = "rojo" name = "rojo"
version = "0.4.1" version = "0.4.3"
dependencies = [ dependencies = [
"clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "rojo" name = "rojo"
version = "0.4.1" version = "0.4.3"
authors = ["Lucien Greathouse <me@lpghatguy.com>"] authors = ["Lucien Greathouse <me@lpghatguy.com>"]
description = "A tool to create robust Roblox projects" description = "A tool to create robust Roblox projects"
license = "MIT" license = "MIT"