mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-04-25 15:16:07 +00:00
merge impl-v2: plugin
This commit is contained in:
@@ -1,4 +0,0 @@
|
|||||||
[*.lua]
|
|
||||||
indent_style = tab
|
|
||||||
trim_trailing_whitespace = true
|
|
||||||
insert_final_newline = true
|
|
||||||
@@ -39,6 +39,10 @@ stds.testez = {
|
|||||||
|
|
||||||
ignore = {
|
ignore = {
|
||||||
"212", -- unused arguments
|
"212", -- unused arguments
|
||||||
|
"421", -- shadowing local variable
|
||||||
|
"422", -- shadowing argument
|
||||||
|
"431", -- shadowing upvalue
|
||||||
|
"432", -- shadowing upvalue argument
|
||||||
}
|
}
|
||||||
|
|
||||||
std = "lua51+roblox"
|
std = "lua51+roblox"
|
||||||
|
|||||||
@@ -18,14 +18,14 @@
|
|||||||
"path": "modules/roact-rodux/lib",
|
"path": "modules/roact-rodux/lib",
|
||||||
"target": "ReplicatedStorage.Rojo.modules.RoactRodux"
|
"target": "ReplicatedStorage.Rojo.modules.RoactRodux"
|
||||||
},
|
},
|
||||||
"modules/promise": {
|
|
||||||
"path": "modules/promise/lib",
|
|
||||||
"target": "ReplicatedStorage.Rojo.modules.Promise"
|
|
||||||
},
|
|
||||||
"modules/testez": {
|
"modules/testez": {
|
||||||
"path": "modules/testez/lib",
|
"path": "modules/testez/lib",
|
||||||
"target": "ReplicatedStorage.TestEZ"
|
"target": "ReplicatedStorage.TestEZ"
|
||||||
},
|
},
|
||||||
|
"modules/promise": {
|
||||||
|
"path": "modules/promise/lib",
|
||||||
|
"target": "ReplicatedStorage.Rojo.modules.Promise"
|
||||||
|
},
|
||||||
"tests": {
|
"tests": {
|
||||||
"path": "tests",
|
"path": "tests",
|
||||||
"target": "TestService"
|
"target": "TestService"
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
local HttpService = game:GetService("HttpService")
|
|
||||||
|
|
||||||
local Promise = require(script.Parent.Parent.modules.Promise)
|
|
||||||
|
|
||||||
local Config = require(script.Parent.Config)
|
|
||||||
local Version = require(script.Parent.Version)
|
|
||||||
|
|
||||||
local Api = {}
|
|
||||||
Api.__index = Api
|
|
||||||
|
|
||||||
Api.Error = {
|
|
||||||
ServerIdMismatch = "ServerIdMismatch",
|
|
||||||
}
|
|
||||||
|
|
||||||
setmetatable(Api.Error, {
|
|
||||||
__index = function(_, key)
|
|
||||||
error("Invalid API.Error name " .. key, 2)
|
|
||||||
end
|
|
||||||
})
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Api.connect(Http) -> Promise<Api>
|
|
||||||
|
|
||||||
Create a new Api using the given HTTP implementation.
|
|
||||||
|
|
||||||
Attempting to invoke methods on an invalid conext will throw errors!
|
|
||||||
]]
|
|
||||||
function Api.connect(http)
|
|
||||||
local context = {
|
|
||||||
http = http,
|
|
||||||
serverId = nil,
|
|
||||||
currentTime = 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
setmetatable(context, Api)
|
|
||||||
|
|
||||||
return context:_start()
|
|
||||||
:andThen(function()
|
|
||||||
return context
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Api:_start()
|
|
||||||
return self.http:get("/")
|
|
||||||
:andThen(function(response)
|
|
||||||
response = response:json()
|
|
||||||
|
|
||||||
if response.protocolVersion ~= Config.protocolVersion then
|
|
||||||
local message = (
|
|
||||||
"Found a Rojo dev server, but it's using a different protocol version, and is incompatible." ..
|
|
||||||
"\nMake sure you have matching versions of both the Rojo plugin and server!" ..
|
|
||||||
"\n\nYour client is version %s, with protocol version %s. It expects server version %s." ..
|
|
||||||
"\nYour server is version %s, with protocol version %s." ..
|
|
||||||
"\n\nGo to https://github.com/LPGhatguy/rojo for more details."
|
|
||||||
):format(
|
|
||||||
Version.display(Config.version), Config.protocolVersion,
|
|
||||||
Config.expectedApiVersionString,
|
|
||||||
response.serverVersion, response.protocolVersion
|
|
||||||
)
|
|
||||||
|
|
||||||
return Promise.reject(message)
|
|
||||||
end
|
|
||||||
|
|
||||||
self.serverId = response.serverId
|
|
||||||
self.currentTime = response.currentTime
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Api:getInfo()
|
|
||||||
return self.http:get("/")
|
|
||||||
:andThen(function(response)
|
|
||||||
response = response:json()
|
|
||||||
|
|
||||||
if response.serverId ~= self.serverId then
|
|
||||||
return Promise.reject(Api.Error.ServerIdMismatch)
|
|
||||||
end
|
|
||||||
|
|
||||||
return response
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Api:read(paths)
|
|
||||||
local body = HttpService:JSONEncode(paths)
|
|
||||||
|
|
||||||
return self.http:post("/read", body)
|
|
||||||
:andThen(function(response)
|
|
||||||
response = response:json()
|
|
||||||
|
|
||||||
if response.serverId ~= self.serverId then
|
|
||||||
return Promise.reject(Api.Error.ServerIdMismatch)
|
|
||||||
end
|
|
||||||
|
|
||||||
return response.items
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Api:getChanges()
|
|
||||||
local url = ("/changes/%f"):format(self.currentTime)
|
|
||||||
|
|
||||||
return self.http:get(url)
|
|
||||||
:andThen(function(response)
|
|
||||||
response = response:json()
|
|
||||||
|
|
||||||
if response.serverId ~= self.serverId then
|
|
||||||
return Promise.reject(Api.Error.ServerIdMismatch)
|
|
||||||
end
|
|
||||||
|
|
||||||
self.currentTime = response.currentTime
|
|
||||||
|
|
||||||
return response.changes
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
return Api
|
|
||||||
119
plugin/src/ApiContext.lua
Normal file
119
plugin/src/ApiContext.lua
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
local Promise = require(script.Parent.Parent.modules.Promise)
|
||||||
|
|
||||||
|
local Config = require(script.Parent.Config)
|
||||||
|
local Version = require(script.Parent.Version)
|
||||||
|
local Http = require(script.Parent.Http)
|
||||||
|
local HttpError = require(script.Parent.HttpError)
|
||||||
|
|
||||||
|
local ApiContext = {}
|
||||||
|
ApiContext.__index = ApiContext
|
||||||
|
|
||||||
|
-- TODO: Audit cases of errors and create enum values for each of them.
|
||||||
|
ApiContext.Error = {
|
||||||
|
ServerIdMismatch = "ServerIdMismatch",
|
||||||
|
}
|
||||||
|
|
||||||
|
setmetatable(ApiContext.Error, {
|
||||||
|
__index = function(_, key)
|
||||||
|
error("Invalid API.Error name " .. key, 2)
|
||||||
|
end
|
||||||
|
})
|
||||||
|
|
||||||
|
function ApiContext.new(url, onMessage)
|
||||||
|
assert(type(url) == "string")
|
||||||
|
assert(type(onMessage) == "function")
|
||||||
|
|
||||||
|
local context = {
|
||||||
|
url = url,
|
||||||
|
onMessage = onMessage,
|
||||||
|
serverId = nil,
|
||||||
|
connected = false,
|
||||||
|
messageCursor = -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
setmetatable(context, ApiContext)
|
||||||
|
|
||||||
|
return context
|
||||||
|
end
|
||||||
|
|
||||||
|
function ApiContext:connect()
|
||||||
|
return Http.get(self.url)
|
||||||
|
:andThen(function(response)
|
||||||
|
local body = response:json()
|
||||||
|
|
||||||
|
if body.protocolVersion ~= Config.protocolVersion then
|
||||||
|
local message = (
|
||||||
|
"Found a Rojo dev server, but it's using a different protocol version, and is incompatible." ..
|
||||||
|
"\nMake sure you have matching versions of both the Rojo plugin and server!" ..
|
||||||
|
"\n\nYour client is version %s, with protocol version %s. It expects server version %s." ..
|
||||||
|
"\nYour server is version %s, with protocol version %s." ..
|
||||||
|
"\n\nGo to https://github.com/LPGhatguy/rojo for more details."
|
||||||
|
):format(
|
||||||
|
Version.display(Config.version), Config.protocolVersion,
|
||||||
|
Config.expectedApiContextVersionString,
|
||||||
|
body.serverVersion, body.protocolVersion
|
||||||
|
)
|
||||||
|
|
||||||
|
return Promise.reject(message)
|
||||||
|
end
|
||||||
|
|
||||||
|
self.serverId = body.serverId
|
||||||
|
self.connected = true
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ApiContext:readAll()
|
||||||
|
if not self.connected then
|
||||||
|
return Promise.reject()
|
||||||
|
end
|
||||||
|
|
||||||
|
return Http.get(self.url .. "/read_all")
|
||||||
|
:andThen(function(response)
|
||||||
|
local body = response:json()
|
||||||
|
|
||||||
|
if body.serverId ~= self.serverId then
|
||||||
|
return Promise.reject("server changed ID")
|
||||||
|
end
|
||||||
|
|
||||||
|
self.messageCursor = body.messageCursor
|
||||||
|
|
||||||
|
return body.instances
|
||||||
|
end, function(err)
|
||||||
|
self.connected = false
|
||||||
|
|
||||||
|
return Promise.reject(err)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ApiContext:retrieveMessages()
|
||||||
|
if not self.connected then
|
||||||
|
return Promise.reject()
|
||||||
|
end
|
||||||
|
|
||||||
|
return Http.get(self.url .. "/subscribe/" .. self.messageCursor)
|
||||||
|
:andThen(function(response)
|
||||||
|
local body = response:json()
|
||||||
|
|
||||||
|
if body.serverId ~= self.serverId then
|
||||||
|
return Promise.reject("server changed ID")
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, message in ipairs(body.messages) do
|
||||||
|
self.onMessage(message)
|
||||||
|
end
|
||||||
|
|
||||||
|
self.messageCursor = body.messageCursor
|
||||||
|
|
||||||
|
return self:retrieveMessages()
|
||||||
|
end, function(err)
|
||||||
|
if err.type == HttpError.Error.Timeout then
|
||||||
|
return self:retrieveMessages()
|
||||||
|
end
|
||||||
|
|
||||||
|
self.connected = false
|
||||||
|
|
||||||
|
return Promise.reject(err)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return ApiContext
|
||||||
@@ -1,12 +1,7 @@
|
|||||||
return {
|
return {
|
||||||
pollingRate = 0.2,
|
version = {0, 5, 0},
|
||||||
version = {0, 4, 11},
|
expectedServerVersionString = "0.5.x",
|
||||||
expectedServerVersionString = "0.4.x",
|
protocolVersion = 2,
|
||||||
protocolVersion = 1,
|
port = 34872,
|
||||||
icons = {
|
|
||||||
syncIn = "rbxassetid://1820320573",
|
|
||||||
togglePolling = "rbxassetid://1820320064",
|
|
||||||
testConnection = "rbxassetid://1820320989",
|
|
||||||
},
|
|
||||||
dev = false,
|
dev = false,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,27 +13,15 @@ local function dprint(...)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- TODO: Factor out into separate library, especially error handling
|
||||||
local Http = {}
|
local Http = {}
|
||||||
Http.__index = Http
|
|
||||||
|
|
||||||
function Http.new(baseUrl)
|
function Http.get(url)
|
||||||
assert(type(baseUrl) == "string", "Http.new needs a baseUrl!")
|
dprint("\nGET", url)
|
||||||
|
|
||||||
local http = {
|
|
||||||
baseUrl = baseUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
setmetatable(http, Http)
|
|
||||||
|
|
||||||
return http
|
|
||||||
end
|
|
||||||
|
|
||||||
function Http:get(endpoint)
|
|
||||||
dprint("\nGET", endpoint)
|
|
||||||
return Promise.new(function(resolve, reject)
|
return Promise.new(function(resolve, reject)
|
||||||
spawn(function()
|
spawn(function()
|
||||||
local ok, result = pcall(function()
|
local ok, result = pcall(function()
|
||||||
return HttpService:GetAsync(self.baseUrl .. endpoint, true)
|
return HttpService:GetAsync(url, true)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
if ok then
|
if ok then
|
||||||
@@ -46,13 +34,13 @@ function Http:get(endpoint)
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Http:post(endpoint, body)
|
function Http.post(url, body)
|
||||||
dprint("\nPOST", endpoint)
|
dprint("\nPOST", url)
|
||||||
dprint(body)
|
dprint(body)
|
||||||
return Promise.new(function(resolve, reject)
|
return Promise.new(function(resolve, reject)
|
||||||
spawn(function()
|
spawn(function()
|
||||||
local ok, result = pcall(function()
|
local ok, result = pcall(function()
|
||||||
return HttpService:PostAsync(self.baseUrl .. endpoint, body)
|
return HttpService:PostAsync(url, body)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
if ok then
|
if ok then
|
||||||
@@ -65,4 +53,12 @@ function Http:post(endpoint, body)
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Http.jsonEncode(object)
|
||||||
|
return HttpService:JSONEncode(object)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Http.jsonDecode(source)
|
||||||
|
return HttpService:JSONDecode(source)
|
||||||
|
end
|
||||||
|
|
||||||
return Http
|
return Http
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ HttpError.Error = {
|
|||||||
message = "Rojo plugin couldn't connect to the Rojo server.\n" ..
|
message = "Rojo plugin couldn't connect to the Rojo server.\n" ..
|
||||||
"Make sure the server is running -- use 'Rojo serve' to run it!",
|
"Make sure the server is running -- use 'Rojo serve' to run it!",
|
||||||
},
|
},
|
||||||
|
Timeout = {
|
||||||
|
message = "Rojo timed out during a request.",
|
||||||
|
},
|
||||||
Unknown = {
|
Unknown = {
|
||||||
message = "Rojo encountered an unknown error: {{message}}",
|
message = "Rojo encountered an unknown error: {{message}}",
|
||||||
},
|
},
|
||||||
@@ -44,7 +47,11 @@ function HttpError.fromErrorString(err)
|
|||||||
end
|
end
|
||||||
|
|
||||||
if err:find("^curl error") then
|
if err:find("^curl error") then
|
||||||
return HttpError.new(HttpError.Error.ConnectFailed)
|
if err:find("couldn't connect to server") then
|
||||||
|
return HttpError.new(HttpError.Error.ConnectFailed)
|
||||||
|
elseif err:find("timeout was reached") then
|
||||||
|
return HttpError.new(HttpError.Error.Timeout)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return HttpError.new(HttpError.Error.Unknown, err)
|
return HttpError.new(HttpError.Error.Unknown, err)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ if not plugin then
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local Plugin = require(script.Parent.Plugin)
|
local Session = require(script.Parent.Session)
|
||||||
local Config = require(script.Parent.Config)
|
local Config = require(script.Parent.Config)
|
||||||
local Version = require(script.Parent.Version)
|
local Version = require(script.Parent.Version)
|
||||||
|
|
||||||
@@ -39,40 +39,25 @@ local function checkUpgrade()
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function main()
|
local function main()
|
||||||
local pluginInstance = Plugin.new()
|
|
||||||
|
|
||||||
local displayedVersion = Config.dev and "DEV" or Version.display(Config.version)
|
local displayedVersion = Config.dev and "DEV" or Version.display(Config.version)
|
||||||
|
|
||||||
local toolbar = plugin:CreateToolbar("Rojo Plugin " .. displayedVersion)
|
local toolbar = plugin:CreateToolbar("Rojo " .. displayedVersion)
|
||||||
|
|
||||||
toolbar:CreateButton("Test Connection", "Connect to Rojo Server", Config.icons.testConnection)
|
local currentSession
|
||||||
|
|
||||||
|
-- TODO: More robust session tracking to handle errors
|
||||||
|
-- TODO: Icon!
|
||||||
|
toolbar:CreateButton("Connect", "Connect to Rojo Session", "")
|
||||||
.Click:Connect(function()
|
.Click:Connect(function()
|
||||||
checkUpgrade()
|
checkUpgrade()
|
||||||
|
|
||||||
pluginInstance:connect()
|
if currentSession ~= nil then
|
||||||
:catch(function(err)
|
warn("Rojo: A session is already running!")
|
||||||
warn(err)
|
return
|
||||||
end)
|
end
|
||||||
end)
|
|
||||||
|
|
||||||
toolbar:CreateButton("Sync In", "Sync into Roblox Studio", Config.icons.syncIn)
|
print("Rojo: Started session.")
|
||||||
.Click:Connect(function()
|
currentSession = Session.new()
|
||||||
checkUpgrade()
|
|
||||||
|
|
||||||
pluginInstance:syncIn()
|
|
||||||
:catch(function(err)
|
|
||||||
warn(err)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
toolbar:CreateButton("Toggle Polling", "Poll server for changes", Config.icons.togglePolling)
|
|
||||||
.Click:Connect(function()
|
|
||||||
checkUpgrade()
|
|
||||||
|
|
||||||
pluginInstance:togglePolling()
|
|
||||||
:catch(function(err)
|
|
||||||
warn(err)
|
|
||||||
end)
|
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,311 +0,0 @@
|
|||||||
local CoreGui = game:GetService("CoreGui")
|
|
||||||
|
|
||||||
local Promise = require(script.Parent.Parent.modules.Promise)
|
|
||||||
|
|
||||||
local Config = require(script.Parent.Config)
|
|
||||||
local Http = require(script.Parent.Http)
|
|
||||||
local Api = require(script.Parent.Api)
|
|
||||||
local Reconciler = require(script.Parent.Reconciler)
|
|
||||||
local Version = require(script.Parent.Version)
|
|
||||||
|
|
||||||
local MESSAGE_SERVER_CHANGED = "Rojo: The server has changed since the last request, reloading plugin..."
|
|
||||||
local MESSAGE_PLUGIN_CHANGED = "Rojo: Another instance of Rojo came online, unloading..."
|
|
||||||
|
|
||||||
local function collectMatch(source, pattern)
|
|
||||||
local result = {}
|
|
||||||
|
|
||||||
for match in source:gmatch(pattern) do
|
|
||||||
table.insert(result, match)
|
|
||||||
end
|
|
||||||
|
|
||||||
return result
|
|
||||||
end
|
|
||||||
|
|
||||||
local Plugin = {}
|
|
||||||
Plugin.__index = Plugin
|
|
||||||
|
|
||||||
function Plugin.new()
|
|
||||||
local address = "localhost"
|
|
||||||
local port = Config.dev and 8001 or 8000
|
|
||||||
|
|
||||||
local remote = ("http://%s:%d"):format(address, port)
|
|
||||||
|
|
||||||
local self = {
|
|
||||||
_http = Http.new(remote),
|
|
||||||
_reconciler = Reconciler.new(),
|
|
||||||
_api = nil,
|
|
||||||
_polling = false,
|
|
||||||
_syncInProgress = false,
|
|
||||||
}
|
|
||||||
|
|
||||||
setmetatable(self, Plugin)
|
|
||||||
|
|
||||||
do
|
|
||||||
local uiName = ("Rojo %s UI"):format(Version.display(Config.version))
|
|
||||||
|
|
||||||
if Config.dev then
|
|
||||||
uiName = "Rojo Dev UI"
|
|
||||||
end
|
|
||||||
|
|
||||||
-- If there's an existing Rojo UI, like from a Roblox plugin upgrade
|
|
||||||
-- that wasn't Rojo, make sure we clean it up.
|
|
||||||
local existingUi = CoreGui:FindFirstChild(uiName)
|
|
||||||
|
|
||||||
if existingUi ~= nil then
|
|
||||||
existingUi:Destroy()
|
|
||||||
end
|
|
||||||
|
|
||||||
local screenGui = Instance.new("ScreenGui")
|
|
||||||
screenGui.Name = uiName
|
|
||||||
screenGui.Parent = CoreGui
|
|
||||||
screenGui.DisplayOrder = -1
|
|
||||||
screenGui.Enabled = false
|
|
||||||
|
|
||||||
local label = Instance.new("TextLabel")
|
|
||||||
label.Font = Enum.Font.SourceSans
|
|
||||||
label.TextSize = 20
|
|
||||||
label.Text = "Rojo polling..."
|
|
||||||
label.BackgroundColor3 = Color3.fromRGB(31, 31, 31)
|
|
||||||
label.BackgroundTransparency = 0.5
|
|
||||||
label.BorderSizePixel = 0
|
|
||||||
label.TextColor3 = Color3.new(1, 1, 1)
|
|
||||||
label.Size = UDim2.new(0, 120, 0, 28)
|
|
||||||
label.Position = UDim2.new(0, 0, 0, 0)
|
|
||||||
label.Parent = screenGui
|
|
||||||
|
|
||||||
self._label = screenGui
|
|
||||||
|
|
||||||
-- If our UI was destroyed, we assume it was from another instance of
|
|
||||||
-- the Rojo plugin coming online.
|
|
||||||
--
|
|
||||||
-- Roblox doesn't notify plugins when they get unloaded, so this is the
|
|
||||||
-- best trigger we have right now unless we create a dedicated event
|
|
||||||
-- object.
|
|
||||||
screenGui.AncestryChanged:Connect(function(_, parent)
|
|
||||||
if parent == nil then
|
|
||||||
warn(MESSAGE_PLUGIN_CHANGED)
|
|
||||||
self:restart()
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Clears all state and issues a notice to the user that the plugin has
|
|
||||||
restarted.
|
|
||||||
]]
|
|
||||||
function Plugin:restart()
|
|
||||||
self:stopPolling()
|
|
||||||
|
|
||||||
self._reconciler:destruct()
|
|
||||||
self._reconciler = Reconciler.new()
|
|
||||||
|
|
||||||
self._api = nil
|
|
||||||
self._polling = false
|
|
||||||
self._syncInProgress = false
|
|
||||||
end
|
|
||||||
|
|
||||||
function Plugin:getApi()
|
|
||||||
if self._api == nil then
|
|
||||||
return Api.connect(self._http)
|
|
||||||
:andThen(function(api)
|
|
||||||
self._api = api
|
|
||||||
|
|
||||||
return api
|
|
||||||
end, function(err)
|
|
||||||
return Promise.reject(err)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
return Promise.resolve(self._api)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Plugin:connect()
|
|
||||||
print("Rojo: Testing connection...")
|
|
||||||
|
|
||||||
return self:getApi()
|
|
||||||
:andThen(function(api)
|
|
||||||
local ok, info = api:getInfo():await()
|
|
||||||
|
|
||||||
if not ok then
|
|
||||||
return Promise.reject(info)
|
|
||||||
end
|
|
||||||
|
|
||||||
print("Rojo: Server found!")
|
|
||||||
print("Rojo: Protocol version:", info.protocolVersion)
|
|
||||||
print("Rojo: Server version:", info.serverVersion)
|
|
||||||
end)
|
|
||||||
:catch(function(err)
|
|
||||||
if err == Api.Error.ServerIdMismatch then
|
|
||||||
warn(MESSAGE_SERVER_CHANGED)
|
|
||||||
self:restart()
|
|
||||||
return self:connect()
|
|
||||||
else
|
|
||||||
return Promise.reject(err)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Plugin:togglePolling()
|
|
||||||
if self._polling then
|
|
||||||
return self:stopPolling()
|
|
||||||
else
|
|
||||||
return self:startPolling()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Plugin:stopPolling()
|
|
||||||
if not self._polling then
|
|
||||||
return Promise.resolve(false)
|
|
||||||
end
|
|
||||||
|
|
||||||
print("Rojo: Stopped polling server for changes.")
|
|
||||||
|
|
||||||
self._polling = false
|
|
||||||
self._label.Enabled = false
|
|
||||||
|
|
||||||
return Promise.resolve(true)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Plugin:_pull(api, project, fileRoutes)
|
|
||||||
return api:read(fileRoutes)
|
|
||||||
:andThen(function(items)
|
|
||||||
for index = 1, #fileRoutes do
|
|
||||||
local fileRoute = fileRoutes[index]
|
|
||||||
local partitionName = fileRoute[1]
|
|
||||||
local partition = project.partitions[partitionName]
|
|
||||||
local item = items[index]
|
|
||||||
|
|
||||||
local partitionTargetRbxRoute = 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.
|
|
||||||
if item ~= nil and #fileRoute == 1 then
|
|
||||||
local objectName = partition.target:match("[^.]+$")
|
|
||||||
item.Name = objectName
|
|
||||||
end
|
|
||||||
|
|
||||||
local itemRbxRoute = {}
|
|
||||||
for _, piece in ipairs(partitionTargetRbxRoute) do
|
|
||||||
table.insert(itemRbxRoute, piece)
|
|
||||||
end
|
|
||||||
|
|
||||||
for i = 2, #fileRoute do
|
|
||||||
table.insert(itemRbxRoute, fileRoute[i])
|
|
||||||
end
|
|
||||||
|
|
||||||
self._reconciler:reconcileRoute(itemRbxRoute, item, fileRoute)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Plugin:startPolling()
|
|
||||||
if self._polling then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
print("Rojo: Starting to poll server for changes...")
|
|
||||||
|
|
||||||
self._polling = true
|
|
||||||
self._label.Enabled = true
|
|
||||||
|
|
||||||
return self:getApi()
|
|
||||||
:andThen(function(api)
|
|
||||||
local infoOk, info = api:getInfo():await()
|
|
||||||
|
|
||||||
if not infoOk then
|
|
||||||
return Promise.reject(info)
|
|
||||||
end
|
|
||||||
|
|
||||||
local syncOk, result = self:syncIn():await()
|
|
||||||
|
|
||||||
if not syncOk then
|
|
||||||
return Promise.reject(result)
|
|
||||||
end
|
|
||||||
|
|
||||||
while self._polling do
|
|
||||||
local changesOk, changes = api:getChanges():await()
|
|
||||||
|
|
||||||
if not changesOk then
|
|
||||||
return Promise.reject(changes)
|
|
||||||
end
|
|
||||||
|
|
||||||
if #changes > 0 then
|
|
||||||
local routes = {}
|
|
||||||
|
|
||||||
for _, change in ipairs(changes) do
|
|
||||||
table.insert(routes, change.route)
|
|
||||||
end
|
|
||||||
|
|
||||||
local pullOk, pullResult = self:_pull(api, info.project, routes):await()
|
|
||||||
|
|
||||||
if not pullOk then
|
|
||||||
return Promise.reject(pullResult)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
wait(Config.pollingRate)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
:catch(function(err)
|
|
||||||
if err == Api.Error.ServerIdMismatch then
|
|
||||||
warn(MESSAGE_SERVER_CHANGED)
|
|
||||||
self:restart()
|
|
||||||
return self:startPolling()
|
|
||||||
else
|
|
||||||
self:stopPolling()
|
|
||||||
return Promise.reject(err)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Plugin:syncIn()
|
|
||||||
if self._syncInProgress then
|
|
||||||
warn("Rojo: Can't sync right now, because a sync is already in progress.")
|
|
||||||
|
|
||||||
return Promise.resolve()
|
|
||||||
end
|
|
||||||
|
|
||||||
self._syncInProgress = true
|
|
||||||
print("Rojo: Syncing from server...")
|
|
||||||
|
|
||||||
return self:getApi()
|
|
||||||
:andThen(function(api)
|
|
||||||
local ok, info = api:getInfo():await()
|
|
||||||
|
|
||||||
if not ok then
|
|
||||||
return Promise.reject(info)
|
|
||||||
end
|
|
||||||
|
|
||||||
local fileRoutes = {}
|
|
||||||
|
|
||||||
for name in pairs(info.project.partitions) do
|
|
||||||
table.insert(fileRoutes, {name})
|
|
||||||
end
|
|
||||||
|
|
||||||
local pullSuccess, pullResult = self:_pull(api, info.project, fileRoutes):await()
|
|
||||||
|
|
||||||
self._syncInProgress = false
|
|
||||||
|
|
||||||
if not pullSuccess then
|
|
||||||
return Promise.reject(pullResult)
|
|
||||||
end
|
|
||||||
|
|
||||||
print("Rojo: Sync successful!")
|
|
||||||
end)
|
|
||||||
:catch(function(err)
|
|
||||||
self._syncInProgress = false
|
|
||||||
|
|
||||||
if err == Api.Error.ServerIdMismatch then
|
|
||||||
warn(MESSAGE_SERVER_CHANGED)
|
|
||||||
self:restart()
|
|
||||||
return self:syncIn()
|
|
||||||
else
|
|
||||||
return Promise.reject(err)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
return Plugin
|
|
||||||
@@ -1,246 +0,0 @@
|
|||||||
local RouteMap = require(script.Parent.RouteMap)
|
|
||||||
|
|
||||||
local function classEqual(a, b)
|
|
||||||
assert(typeof(a) == "string")
|
|
||||||
assert(typeof(b) == "string")
|
|
||||||
|
|
||||||
if a == "*" or b == "*" then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
return a == b
|
|
||||||
end
|
|
||||||
|
|
||||||
local function applyProperties(target, properties)
|
|
||||||
assert(typeof(target) == "Instance")
|
|
||||||
assert(typeof(properties) == "table")
|
|
||||||
|
|
||||||
for key, property in pairs(properties) do
|
|
||||||
-- TODO: Transform property value based on property.Type
|
|
||||||
-- Right now, we assume that 'value' is primitive!
|
|
||||||
target[key] = property.Value
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Attempt to parent `rbx` to `parent`, doing nothing if:
|
|
||||||
* parent is already `parent`
|
|
||||||
* Changing parent threw an error
|
|
||||||
]]
|
|
||||||
local function reparent(rbx, parent)
|
|
||||||
assert(typeof(rbx) == "Instance")
|
|
||||||
assert(typeof(parent) == "Instance")
|
|
||||||
|
|
||||||
if rbx.Parent == parent then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Setting `Parent` can fail if:
|
|
||||||
-- * The object has been destroyed
|
|
||||||
-- * The object is a service and cannot be reparented
|
|
||||||
pcall(function()
|
|
||||||
rbx.Parent = parent
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Attempts to match up Roblox instances and object specifiers for
|
|
||||||
reconciliation.
|
|
||||||
|
|
||||||
An object is considered a match if they have the same Name and ClassName.
|
|
||||||
|
|
||||||
primaryChildren and secondaryChildren can each be either a list of Roblox
|
|
||||||
instances or object specifiers. Since they share a common shape, switching
|
|
||||||
the two around isn't problematic!
|
|
||||||
|
|
||||||
visited is expected to be an empty table initially. It will be filled with
|
|
||||||
the set of children that have been visited so far.
|
|
||||||
]]
|
|
||||||
local function findNextChildPair(primaryChildren, secondaryChildren, visited)
|
|
||||||
for _, primaryChild in ipairs(primaryChildren) do
|
|
||||||
if not visited[primaryChild] then
|
|
||||||
visited[primaryChild] = true
|
|
||||||
|
|
||||||
for _, secondaryChild in ipairs(secondaryChildren) do
|
|
||||||
if classEqual(primaryChild.ClassName, secondaryChild.ClassName) and primaryChild.Name == secondaryChild.Name then
|
|
||||||
visited[secondaryChild] = true
|
|
||||||
|
|
||||||
return primaryChild, secondaryChild
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return primaryChild, nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local Reconciler = {}
|
|
||||||
Reconciler.__index = Reconciler
|
|
||||||
|
|
||||||
function Reconciler.new()
|
|
||||||
local reconciler = {
|
|
||||||
_routeMap = RouteMap.new(),
|
|
||||||
}
|
|
||||||
|
|
||||||
setmetatable(reconciler, Reconciler)
|
|
||||||
|
|
||||||
return reconciler
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
A semi-smart algorithm that attempts to apply the given item's children to
|
|
||||||
an existing Roblox object.
|
|
||||||
]]
|
|
||||||
function Reconciler:_reconcileChildren(rbx, item)
|
|
||||||
local visited = {}
|
|
||||||
local rbxChildren = rbx:GetChildren()
|
|
||||||
|
|
||||||
-- Reconcile any children that were added or updated
|
|
||||||
while true do
|
|
||||||
local itemChild, rbxChild = findNextChildPair(item.Children, rbxChildren, visited)
|
|
||||||
|
|
||||||
if itemChild == nil then
|
|
||||||
break
|
|
||||||
end
|
|
||||||
|
|
||||||
local newRbxChild = self:reconcile(rbxChild, itemChild)
|
|
||||||
|
|
||||||
if newRbxChild ~= nil then
|
|
||||||
newRbxChild.Parent = rbx
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Reconcile any children that were deleted
|
|
||||||
while true do
|
|
||||||
local rbxChild, itemChild = findNextChildPair(rbxChildren, item.Children, visited)
|
|
||||||
|
|
||||||
if rbxChild == nil then
|
|
||||||
break
|
|
||||||
end
|
|
||||||
|
|
||||||
local newRbxChild = self:reconcile(rbxChild, itemChild)
|
|
||||||
|
|
||||||
if newRbxChild ~= nil then
|
|
||||||
newRbxChild.Parent = rbx
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Construct a new Roblox object from the given item.
|
|
||||||
]]
|
|
||||||
function Reconciler:_reify(item)
|
|
||||||
local className = item.ClassName
|
|
||||||
|
|
||||||
-- "*" represents a match of any class. It reifies as a folder!
|
|
||||||
if className == "*" then
|
|
||||||
className = "Folder"
|
|
||||||
end
|
|
||||||
|
|
||||||
local rbx = Instance.new(className)
|
|
||||||
rbx.Name = item.Name
|
|
||||||
|
|
||||||
applyProperties(rbx, item.Properties)
|
|
||||||
|
|
||||||
for _, child in ipairs(item.Children) do
|
|
||||||
reparent(self:_reify(child), rbx)
|
|
||||||
end
|
|
||||||
|
|
||||||
if item.Route ~= nil then
|
|
||||||
self._routeMap:insert(item.Route, rbx)
|
|
||||||
end
|
|
||||||
|
|
||||||
return rbx
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Clears any state that the Reconciler has, stopping it completely.
|
|
||||||
]]
|
|
||||||
function Reconciler:destruct()
|
|
||||||
self._routeMap:destruct()
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Apply the changes represented by the given item to a Roblox object that's a
|
|
||||||
child of the given instance.
|
|
||||||
]]
|
|
||||||
function Reconciler:reconcile(rbx, item)
|
|
||||||
-- Item was deleted
|
|
||||||
if item == nil then
|
|
||||||
if rbx ~= nil then
|
|
||||||
self._routeMap:removeByRbx(rbx)
|
|
||||||
rbx:Destroy()
|
|
||||||
end
|
|
||||||
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Item was created!
|
|
||||||
if rbx == nil then
|
|
||||||
return self:_reify(item)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Item changed type!
|
|
||||||
if not classEqual(rbx.ClassName, item.ClassName) then
|
|
||||||
self._routeMap:removeByRbx(rbx)
|
|
||||||
rbx:Destroy()
|
|
||||||
|
|
||||||
return self:_reify(item)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- It's possible that the instance we're associating with this item hasn't
|
|
||||||
-- been inserted into the RouteMap yet.
|
|
||||||
if item.Route ~= nil then
|
|
||||||
self._routeMap:insert(item.Route, rbx)
|
|
||||||
end
|
|
||||||
|
|
||||||
applyProperties(rbx, item.Properties)
|
|
||||||
self:_reconcileChildren(rbx, item)
|
|
||||||
|
|
||||||
return rbx
|
|
||||||
end
|
|
||||||
|
|
||||||
function Reconciler:reconcileRoute(rbxRoute, item, fileRoute)
|
|
||||||
local parent
|
|
||||||
local rbx = game
|
|
||||||
|
|
||||||
for i = 1, #rbxRoute do
|
|
||||||
local piece = rbxRoute[i]
|
|
||||||
|
|
||||||
local child = rbx:FindFirstChild(piece)
|
|
||||||
|
|
||||||
-- We should get services instead of making folders here.
|
|
||||||
if rbx == game and child == nil then
|
|
||||||
local success
|
|
||||||
success, child = pcall(game.GetService, game, piece)
|
|
||||||
|
|
||||||
-- That isn't a valid service!
|
|
||||||
if not success then
|
|
||||||
child = nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- We don't want to create a folder if we're reaching our target item!
|
|
||||||
if child == nil and i ~= #rbxRoute then
|
|
||||||
child = Instance.new("Folder")
|
|
||||||
child.Parent = rbx
|
|
||||||
child.Name = piece
|
|
||||||
end
|
|
||||||
|
|
||||||
parent = rbx
|
|
||||||
rbx = child
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Let's check the route map!
|
|
||||||
if rbx == nil then
|
|
||||||
rbx = self._routeMap:get(fileRoute)
|
|
||||||
end
|
|
||||||
|
|
||||||
rbx = self:reconcile(rbx, item)
|
|
||||||
|
|
||||||
reparent(rbx, parent)
|
|
||||||
end
|
|
||||||
|
|
||||||
return Reconciler
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
--[[
|
|
||||||
A map from Route objects (given by the server) to Roblox instances (created
|
|
||||||
by the plugin).
|
|
||||||
]]
|
|
||||||
|
|
||||||
local function hashRoute(route)
|
|
||||||
return table.concat(route, "/")
|
|
||||||
end
|
|
||||||
|
|
||||||
local RouteMap = {}
|
|
||||||
RouteMap.__index = RouteMap
|
|
||||||
|
|
||||||
function RouteMap.new()
|
|
||||||
local self = {
|
|
||||||
_map = {},
|
|
||||||
_reverseMap = {},
|
|
||||||
_connectionsByRbx = {},
|
|
||||||
}
|
|
||||||
|
|
||||||
setmetatable(self, RouteMap)
|
|
||||||
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
function RouteMap:insert(route, rbx)
|
|
||||||
local hashed = hashRoute(route)
|
|
||||||
|
|
||||||
-- Make sure that each route and instance are only present in RouteMap once.
|
|
||||||
self:removeByRoute(route)
|
|
||||||
self:removeByRbx(rbx)
|
|
||||||
|
|
||||||
self._map[hashed] = rbx
|
|
||||||
self._reverseMap[rbx] = hashed
|
|
||||||
self._connectionsByRbx[rbx] = rbx.AncestryChanged:Connect(function(_, parent)
|
|
||||||
if parent == nil then
|
|
||||||
self:removeByRbx(rbx)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
function RouteMap:get(route)
|
|
||||||
return self._map[hashRoute(route)]
|
|
||||||
end
|
|
||||||
|
|
||||||
function RouteMap:removeByRoute(route)
|
|
||||||
local hashedRoute = hashRoute(route)
|
|
||||||
local rbx = self._map[hashedRoute]
|
|
||||||
|
|
||||||
if rbx ~= nil then
|
|
||||||
self:_removeInternal(rbx, hashedRoute)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function RouteMap:removeByRbx(rbx)
|
|
||||||
local hashedRoute = self._reverseMap[rbx]
|
|
||||||
|
|
||||||
if hashedRoute ~= nil then
|
|
||||||
self:_removeInternal(rbx, hashedRoute)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Correcly removes the given Roblox Instance/Route pair from the RouteMap.
|
|
||||||
]]
|
|
||||||
function RouteMap:_removeInternal(rbx, hashedRoute)
|
|
||||||
self._map[hashedRoute] = nil
|
|
||||||
self._reverseMap[rbx] = nil
|
|
||||||
self._connectionsByRbx[rbx]:Disconnect()
|
|
||||||
self._connectionsByRbx[rbx] = nil
|
|
||||||
|
|
||||||
self:_removeRbxDescendants(rbx)
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Ensure that there are no descendants of the given Roblox Instance still
|
|
||||||
present in the map, guaranteeing that it has been cleaned out.
|
|
||||||
]]
|
|
||||||
function RouteMap:_removeRbxDescendants(parentRbx)
|
|
||||||
for rbx in pairs(self._reverseMap) do
|
|
||||||
if rbx:IsDescendantOf(parentRbx) then
|
|
||||||
self:removeByRbx(rbx)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Remove all items from the map and disconnect all connections, cleaning up
|
|
||||||
the RouteMap.
|
|
||||||
]]
|
|
||||||
function RouteMap:destruct()
|
|
||||||
self._map = {}
|
|
||||||
self._reverseMap = {}
|
|
||||||
|
|
||||||
for _, connection in pairs(self._connectionsByRbx) do
|
|
||||||
connection:Disconnect()
|
|
||||||
end
|
|
||||||
|
|
||||||
self._connectionsByRbx = {}
|
|
||||||
end
|
|
||||||
|
|
||||||
function RouteMap:visualize()
|
|
||||||
-- Log all of our keys so that the visualization has a stable order.
|
|
||||||
local keys = {}
|
|
||||||
|
|
||||||
for key in pairs(self._map) do
|
|
||||||
table.insert(keys, key)
|
|
||||||
end
|
|
||||||
|
|
||||||
table.sort(keys)
|
|
||||||
|
|
||||||
local buffer = {}
|
|
||||||
for _, key in ipairs(keys) do
|
|
||||||
local visualized = ("- %s: %s"):format(
|
|
||||||
key,
|
|
||||||
self._map[key]:GetFullName()
|
|
||||||
)
|
|
||||||
table.insert(buffer, visualized)
|
|
||||||
end
|
|
||||||
|
|
||||||
return table.concat(buffer, "\n")
|
|
||||||
end
|
|
||||||
|
|
||||||
return RouteMap
|
|
||||||
76
plugin/src/Session.lua
Normal file
76
plugin/src/Session.lua
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
local Config = require(script.Parent.Config)
|
||||||
|
local ApiContext = require(script.Parent.ApiContext)
|
||||||
|
|
||||||
|
local REMOTE_URL = ("http://localhost:%d"):format(Config.port)
|
||||||
|
|
||||||
|
local Session = {}
|
||||||
|
Session.__index = Session
|
||||||
|
|
||||||
|
function Session.new()
|
||||||
|
local self = {}
|
||||||
|
|
||||||
|
setmetatable(self, Session)
|
||||||
|
|
||||||
|
-- TODO: Rewrite all instance tracking logic and implement a real reconciler
|
||||||
|
local created = {}
|
||||||
|
created["0"] = game:GetService("ReplicatedFirst")
|
||||||
|
|
||||||
|
local api
|
||||||
|
local function readAll()
|
||||||
|
print("Reading all...")
|
||||||
|
|
||||||
|
return api:readAll()
|
||||||
|
:andThen(function(instances)
|
||||||
|
local visited = {}
|
||||||
|
for id, instance in pairs(instances) do
|
||||||
|
visited[id] = true
|
||||||
|
if id ~= "0" then
|
||||||
|
local existing = created[id]
|
||||||
|
if existing ~= nil then
|
||||||
|
pcall(existing.Destroy, existing)
|
||||||
|
end
|
||||||
|
|
||||||
|
local real = Instance.new(instance.className)
|
||||||
|
real.Name = instance.name
|
||||||
|
|
||||||
|
for key, value in pairs(instance.properties) do
|
||||||
|
real[key] = value
|
||||||
|
end
|
||||||
|
|
||||||
|
created[id] = real
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for id, instance in pairs(instances) do
|
||||||
|
if id ~= "0" then
|
||||||
|
created[id].Parent = created[tostring(instance.parent)]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for id, object in pairs(created) do
|
||||||
|
if not visited[id] then
|
||||||
|
object:Destroy()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
api = ApiContext.new(REMOTE_URL, function(message)
|
||||||
|
if message.type == "InstanceChanged" then
|
||||||
|
print("Instance", message.id, "changed!")
|
||||||
|
readAll()
|
||||||
|
else
|
||||||
|
warn("Unknown message type " .. message.type)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
api:connect()
|
||||||
|
:andThen(readAll)
|
||||||
|
:andThen(function()
|
||||||
|
return api:retrieveMessages()
|
||||||
|
end)
|
||||||
|
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
return Session
|
||||||
Reference in New Issue
Block a user