forked from rojo-rbx/rojo
Initial commit
This commit is contained in:
56
plugin/.luacheckrc
Normal file
56
plugin/.luacheckrc
Normal file
@@ -0,0 +1,56 @@
|
||||
stds.roblox = {
|
||||
read_globals = {
|
||||
game = {
|
||||
other_fields = true,
|
||||
},
|
||||
|
||||
-- Roblox globals
|
||||
"script",
|
||||
|
||||
-- Extra functions
|
||||
"tick", "warn", "spawn",
|
||||
"wait", "settings",
|
||||
|
||||
-- Types
|
||||
"Vector2", "Vector3",
|
||||
"Color3",
|
||||
"UDim", "UDim2",
|
||||
"Rect",
|
||||
"CFrame",
|
||||
"Enum",
|
||||
"Instance",
|
||||
}
|
||||
}
|
||||
|
||||
stds.plugin = {
|
||||
read_globals = {
|
||||
"plugin",
|
||||
}
|
||||
}
|
||||
|
||||
stds.testez = {
|
||||
read_globals = {
|
||||
"describe",
|
||||
"it", "itFOCUS", "itSKIP",
|
||||
"FOCUS", "SKIP", "HACK_NO_XPCALL",
|
||||
"expect",
|
||||
}
|
||||
}
|
||||
|
||||
ignore = {
|
||||
"212", -- unused arguments
|
||||
"421", -- shadowing local variable
|
||||
"422", -- shadowing argument
|
||||
"431", -- shadowing upvalue
|
||||
"432", -- shadowing upvalue argument
|
||||
}
|
||||
|
||||
std = "lua51+roblox"
|
||||
|
||||
files["**/*.server.lua"] = {
|
||||
std = "+plugin",
|
||||
}
|
||||
|
||||
files["**/*-spec.lua"] = {
|
||||
std = "+testez",
|
||||
}
|
||||
4
plugin/rbxfs.json
Normal file
4
plugin/rbxfs.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"rootDirectory": "src",
|
||||
"rootObject": "ReplicatedStorage.Rojo"
|
||||
}
|
||||
3
plugin/src/Config.lua
Normal file
3
plugin/src/Config.lua
Normal file
@@ -0,0 +1,3 @@
|
||||
return {
|
||||
pollingRate = 0.3,
|
||||
}
|
||||
54
plugin/src/Http.lua
Normal file
54
plugin/src/Http.lua
Normal file
@@ -0,0 +1,54 @@
|
||||
local HttpService = game:GetService("HttpService")
|
||||
|
||||
local Promise = require(script.Parent.Promise)
|
||||
local HttpError = require(script.Parent.HttpError)
|
||||
local HttpResponse = require(script.Parent.HttpResponse)
|
||||
|
||||
local Http = {}
|
||||
Http.__index = Http
|
||||
|
||||
function Http.new(baseUrl)
|
||||
assert(type(baseUrl) == "string", "Http.new needs a baseUrl!")
|
||||
|
||||
local http = {
|
||||
baseUrl = baseUrl
|
||||
}
|
||||
|
||||
setmetatable(http, Http)
|
||||
|
||||
return http
|
||||
end
|
||||
|
||||
function Http:get(endpoint)
|
||||
return Promise.new(function(resolve, reject)
|
||||
spawn(function()
|
||||
local ok, result = pcall(function()
|
||||
return HttpService:GetAsync(self.baseUrl .. endpoint, true)
|
||||
end)
|
||||
|
||||
if ok then
|
||||
resolve(HttpResponse.new(result))
|
||||
else
|
||||
reject(HttpError.fromErrorString(result))
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
function Http:post(endpoint, body)
|
||||
return Promise.new(function(resolve, reject)
|
||||
spawn(function()
|
||||
local ok, result = pcall(function()
|
||||
return HttpService:PostAsync(self.baseUrl .. endpoint, body)
|
||||
end)
|
||||
|
||||
if ok then
|
||||
resolve(HttpResponse.new(result))
|
||||
else
|
||||
reject(HttpError.fromErrorString(result))
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
return Http
|
||||
57
plugin/src/HttpError.lua
Normal file
57
plugin/src/HttpError.lua
Normal file
@@ -0,0 +1,57 @@
|
||||
local HttpError = {}
|
||||
HttpError.__index = HttpError
|
||||
|
||||
HttpError.Error = {
|
||||
HttpNotEnabled = {
|
||||
message = "Rojo requires HTTP access, which is not enabled.\n" ..
|
||||
"Check your game settings, located in the 'Home' tab of Studio.",
|
||||
},
|
||||
ConnectFailed = {
|
||||
message = "Rojo plugin couldn't connect to the Rojo server.\n" ..
|
||||
"Make sure the server is running -- use 'Rojo serve' to run it!",
|
||||
},
|
||||
Unknown = {
|
||||
message = "Rojo encountered an unknown error: {{message}}",
|
||||
},
|
||||
}
|
||||
|
||||
function HttpError.new(type, extraMessage)
|
||||
extraMessage = extraMessage or ""
|
||||
local message = type.message:gsub("{{message}}", extraMessage)
|
||||
|
||||
local err = {
|
||||
type = type,
|
||||
message = message,
|
||||
}
|
||||
|
||||
setmetatable(err, HttpError)
|
||||
|
||||
return err
|
||||
end
|
||||
|
||||
function HttpError:__tostring()
|
||||
return self.message
|
||||
end
|
||||
|
||||
--[[
|
||||
This method shouldn't have to exist. Ugh.
|
||||
]]
|
||||
function HttpError.fromErrorString(err)
|
||||
err = err:lower()
|
||||
|
||||
if err:find("^http requests are not enabled") then
|
||||
return HttpError.new(HttpError.Error.HttpNotEnabled)
|
||||
end
|
||||
|
||||
if err:find("^curl error") then
|
||||
return HttpError.new(HttpError.Error.ConnectFailed)
|
||||
end
|
||||
|
||||
return HttpError.new(HttpError.Error.Unknown, err)
|
||||
end
|
||||
|
||||
function HttpError:report()
|
||||
warn(self.message)
|
||||
end
|
||||
|
||||
return HttpError
|
||||
20
plugin/src/HttpResponse.lua
Normal file
20
plugin/src/HttpResponse.lua
Normal file
@@ -0,0 +1,20 @@
|
||||
local HttpService = game:GetService("HttpService")
|
||||
|
||||
local HttpResponse = {}
|
||||
HttpResponse.__index = HttpResponse
|
||||
|
||||
function HttpResponse.new(body)
|
||||
local response = {
|
||||
body = body,
|
||||
}
|
||||
|
||||
setmetatable(response, HttpResponse)
|
||||
|
||||
return response
|
||||
end
|
||||
|
||||
function HttpResponse:json()
|
||||
return HttpService:JSONDecode(self.body)
|
||||
end
|
||||
|
||||
return HttpResponse
|
||||
30
plugin/src/Main.server.lua
Normal file
30
plugin/src/Main.server.lua
Normal file
@@ -0,0 +1,30 @@
|
||||
if not plugin then
|
||||
return
|
||||
end
|
||||
|
||||
local Plugin = require(script.Parent.Plugin)
|
||||
|
||||
local function main()
|
||||
local pluginInstance = Plugin.new()
|
||||
|
||||
local toolbar = plugin:CreateToolbar("Rojo Plugin 0.1.0")
|
||||
|
||||
toolbar:CreateButton("Test Connection", "Connect to Rojo Server", "")
|
||||
.Click:Connect(function()
|
||||
pluginInstance:connect()
|
||||
end)
|
||||
|
||||
toolbar:CreateButton("Sync In", "Sync into Roblox Studio", "")
|
||||
.Click:Connect(function()
|
||||
pluginInstance:syncIn()
|
||||
end)
|
||||
|
||||
toolbar:CreateButton("Toggle Polling", "Poll server for changes", "")
|
||||
.Click:Connect(function()
|
||||
spawn(function()
|
||||
pluginInstance:togglePolling()
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
main()
|
||||
256
plugin/src/Plugin.lua
Normal file
256
plugin/src/Plugin.lua
Normal file
@@ -0,0 +1,256 @@
|
||||
local Config = require(script.Parent.Config)
|
||||
local Http = require(script.Parent.Http)
|
||||
local Server = require(script.Parent.Server)
|
||||
local Promise = require(script.Parent.Promise)
|
||||
|
||||
local function collectMatch(source, pattern)
|
||||
local result = {}
|
||||
|
||||
for match in source:gmatch(pattern) do
|
||||
table.insert(result, match)
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
local function fileToName(filename)
|
||||
if filename:find("%.server%.lua$") then
|
||||
return filename:match("^(.-)%.server%.lua$"), "Script"
|
||||
elseif filename:find("%.client%.lua$") then
|
||||
return filename:match("^(.-)%.client%.lua$"), "LocalScript"
|
||||
elseif filename:find("%.lua") then
|
||||
return filename:match("^(.-)%.lua$"), "ModuleScript"
|
||||
else
|
||||
return filename, "StringValue"
|
||||
end
|
||||
end
|
||||
|
||||
local function nameToInstance(filename, contents)
|
||||
local name, className = fileToName(filename)
|
||||
|
||||
local instance = Instance.new(className)
|
||||
instance.Name = name
|
||||
|
||||
if className:find("Script$") then
|
||||
instance.Source = contents
|
||||
else
|
||||
instance.Value = contents
|
||||
end
|
||||
|
||||
return instance
|
||||
end
|
||||
|
||||
local function make(item, name)
|
||||
if item.type == "dir" then
|
||||
local instance = Instance.new("Folder")
|
||||
instance.Name = name
|
||||
|
||||
for childName, child in pairs(item.children) do
|
||||
make(child, childName).Parent = instance
|
||||
end
|
||||
|
||||
return instance
|
||||
elseif item.type == "file" then
|
||||
return nameToInstance(name, item.contents)
|
||||
else
|
||||
error("not implemented")
|
||||
end
|
||||
end
|
||||
|
||||
local function write(parent, route, item)
|
||||
local location = parent
|
||||
|
||||
for index = 1, #route - 1 do
|
||||
local piece = route[index]
|
||||
local newLocation = location:FindFirstChild(piece)
|
||||
|
||||
if not newLocation then
|
||||
newLocation = Instance.new("Folder")
|
||||
newLocation.Name = piece
|
||||
newLocation.Parent = location
|
||||
end
|
||||
|
||||
location = newLocation
|
||||
end
|
||||
|
||||
local fileName = route[#route]
|
||||
local name = fileToName(fileName)
|
||||
|
||||
local existing = location:FindFirstChild(name)
|
||||
|
||||
local new
|
||||
if item then
|
||||
new = make(item, fileName)
|
||||
end
|
||||
|
||||
if existing then
|
||||
existing:Destroy()
|
||||
end
|
||||
|
||||
if new then
|
||||
new.Parent = location
|
||||
end
|
||||
end
|
||||
|
||||
local Plugin = {}
|
||||
Plugin.__index = Plugin
|
||||
|
||||
function Plugin.new()
|
||||
local address = "localhost"
|
||||
local port = 8081
|
||||
|
||||
local remote = ("http://%s:%d"):format(address, port)
|
||||
|
||||
local foop = {
|
||||
_http = Http.new(remote),
|
||||
_server = nil,
|
||||
_polling = false,
|
||||
}
|
||||
|
||||
setmetatable(foop, Plugin)
|
||||
|
||||
do
|
||||
local screenGui = Instance.new("ScreenGui")
|
||||
screenGui.Name = "Rojo UI"
|
||||
screenGui.Parent = game.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
|
||||
|
||||
foop._label = screenGui
|
||||
end
|
||||
|
||||
return foop
|
||||
end
|
||||
|
||||
function Plugin:server()
|
||||
if not self._server then
|
||||
self._server = Server.connect(self._http)
|
||||
:catch(function(err)
|
||||
self._server = nil
|
||||
return Promise.reject(err)
|
||||
end)
|
||||
end
|
||||
|
||||
return self._server
|
||||
end
|
||||
|
||||
function Plugin:connect()
|
||||
print("Testing connection...")
|
||||
|
||||
self:server()
|
||||
:andThen(function(server)
|
||||
return server:getInfo()
|
||||
end)
|
||||
:andThen(function(result)
|
||||
print("Server found!")
|
||||
print("Protocol version:", result.protocolVersion)
|
||||
print("Server version:", result.serverVersion)
|
||||
end)
|
||||
end
|
||||
|
||||
function Plugin:togglePolling()
|
||||
if self._polling then
|
||||
self:stopPolling()
|
||||
else
|
||||
self:startPolling()
|
||||
end
|
||||
end
|
||||
|
||||
function Plugin:stopPolling()
|
||||
if not self._polling then
|
||||
return
|
||||
end
|
||||
|
||||
print("Stopping polling...")
|
||||
|
||||
self._polling = false
|
||||
self._label.Enabled = false
|
||||
end
|
||||
|
||||
function Plugin:startPolling()
|
||||
if self._polling then
|
||||
return
|
||||
end
|
||||
|
||||
print("Starting to poll...")
|
||||
|
||||
self._polling = true
|
||||
self._label.Enabled = true
|
||||
|
||||
return self:server()
|
||||
:andThen(function(server)
|
||||
self:syncIn():await()
|
||||
|
||||
local project = server:getInfo():await().project
|
||||
|
||||
while self._polling do
|
||||
local changes = server:getChanges():await()
|
||||
|
||||
local routes = {}
|
||||
|
||||
for _, change in ipairs(changes) do
|
||||
table.insert(routes, change.route)
|
||||
end
|
||||
|
||||
local items = server:read(routes):await()
|
||||
|
||||
for index = 1, #routes do
|
||||
local partitionName = routes[index][1]
|
||||
local partition = project.partitions[partitionName]
|
||||
local data = items[index]
|
||||
|
||||
local fullRoute = collectMatch(partition.target, "[^.]+")
|
||||
|
||||
write(game, fullRoute, data)
|
||||
end
|
||||
|
||||
wait(Config.pollingRate)
|
||||
end
|
||||
end)
|
||||
:catch(function()
|
||||
self:stopPolling()
|
||||
end)
|
||||
end
|
||||
|
||||
function Plugin:syncIn()
|
||||
print("Syncing from server...")
|
||||
|
||||
return self:server()
|
||||
:andThen(function(server)
|
||||
local project = server:getInfo():await().project
|
||||
|
||||
local readRoutes = {}
|
||||
|
||||
for name in pairs(project.partitions) do
|
||||
table.insert(readRoutes, {name})
|
||||
end
|
||||
|
||||
local items = server:read(readRoutes):await()
|
||||
|
||||
for index = 1, #readRoutes do
|
||||
local partitionName = readRoutes[index][1]
|
||||
local partition = project.partitions[partitionName]
|
||||
local data = items[index]
|
||||
|
||||
local fullRoute = collectMatch(partition.target, "[^.]+")
|
||||
|
||||
write(game, fullRoute, data)
|
||||
end
|
||||
|
||||
print("Sync successful!")
|
||||
end)
|
||||
end
|
||||
|
||||
return Plugin
|
||||
70
plugin/src/Promise-spec.lua
Normal file
70
plugin/src/Promise-spec.lua
Normal file
@@ -0,0 +1,70 @@
|
||||
return function()
|
||||
local Promise = require(script.Parent.Promise)
|
||||
|
||||
describe("Promise.new", function()
|
||||
it("should instantiate with a callback", function()
|
||||
local promise = Promise.new(function() end)
|
||||
|
||||
expect(promise).to.be.ok()
|
||||
end)
|
||||
|
||||
it("should invoke the given callback with resolve and reject", function()
|
||||
local callCount = 0
|
||||
local resolveArg
|
||||
local rejectArg
|
||||
|
||||
local promise = Promise.new(function(resolve, reject)
|
||||
callCount = callCount + 1
|
||||
resolveArg = resolve
|
||||
rejectArg = reject
|
||||
end)
|
||||
|
||||
expect(promise).to.be.ok()
|
||||
|
||||
expect(callCount).to.equal(1)
|
||||
expect(resolveArg).to.be.a("function")
|
||||
expect(rejectArg).to.be.a("function")
|
||||
expect(promise._status).to.equal(Promise.Status.Started)
|
||||
end)
|
||||
|
||||
it("should resolve promises on resolve()", function()
|
||||
local callCount = 0
|
||||
|
||||
local promise = Promise.new(function(resolve)
|
||||
callCount = callCount + 1
|
||||
resolve()
|
||||
end)
|
||||
|
||||
expect(promise).to.be.ok()
|
||||
expect(callCount).to.equal(1)
|
||||
expect(promise._status).to.equal(Promise.Status.Resolved)
|
||||
end)
|
||||
|
||||
it("should reject promises on reject()", function()
|
||||
local callCount = 0
|
||||
|
||||
local promise = Promise.new(function(resolve, reject)
|
||||
callCount = callCount + 1
|
||||
reject()
|
||||
end)
|
||||
|
||||
expect(promise).to.be.ok()
|
||||
expect(callCount).to.equal(1)
|
||||
expect(promise._status).to.equal(Promise.Status.Rejected)
|
||||
end)
|
||||
|
||||
it("should reject on error in callback", function()
|
||||
local callCount = 0
|
||||
|
||||
local promise = Promise.new(function()
|
||||
callCount = callCount + 1
|
||||
error("hahah")
|
||||
end)
|
||||
|
||||
expect(promise).to.be.ok()
|
||||
expect(callCount).to.equal(1)
|
||||
expect(promise._status).to.equal(Promise.Status.Rejected)
|
||||
expect(promise._value[1]:find("hahah")).to.be.ok()
|
||||
end)
|
||||
end)
|
||||
end
|
||||
290
plugin/src/Promise.lua
Normal file
290
plugin/src/Promise.lua
Normal file
@@ -0,0 +1,290 @@
|
||||
--[[
|
||||
An implementation of Promises similar to Promise/A+.
|
||||
]]
|
||||
|
||||
local PROMISE_DEBUG = true
|
||||
|
||||
-- If promise debugging is on, use a version of pcall that warns on failure.
|
||||
-- This is useful for finding errors that happen within Promise itself.
|
||||
local wpcall
|
||||
if PROMISE_DEBUG then
|
||||
wpcall = function(f, ...)
|
||||
local result = { pcall(f, ...) }
|
||||
|
||||
if not result[1] then
|
||||
warn(result[2])
|
||||
end
|
||||
|
||||
return unpack(result)
|
||||
end
|
||||
else
|
||||
wpcall = pcall
|
||||
end
|
||||
|
||||
--[[
|
||||
Creates a function that invokes a callback with correct error handling and
|
||||
resolution mechanisms.
|
||||
]]
|
||||
local function createAdvancer(callback, resolve, reject)
|
||||
return function(...)
|
||||
local result = { wpcall(callback, ...) }
|
||||
local ok = table.remove(result, 1)
|
||||
|
||||
if ok then
|
||||
resolve(unpack(result))
|
||||
else
|
||||
reject(unpack(result))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function isEmpty(t)
|
||||
return next(t) == nil
|
||||
end
|
||||
|
||||
local Promise = {}
|
||||
Promise.__index = Promise
|
||||
|
||||
Promise.Status = {
|
||||
Started = "Started",
|
||||
Resolved = "Resolved",
|
||||
Rejected = "Rejected",
|
||||
}
|
||||
|
||||
--[[
|
||||
Constructs a new Promise with the given initializing callback.
|
||||
|
||||
This is generally only called when directly wrapping a non-promise API into
|
||||
a promise-based version.
|
||||
|
||||
The callback will receive 'resolve' and 'reject' methods, used to start
|
||||
invoking the promise chain.
|
||||
|
||||
For example:
|
||||
|
||||
local function get(url)
|
||||
return Promise.new(function(resolve, reject)
|
||||
spawn(function()
|
||||
resolve(HttpService:GetAsync(url))
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
get("https://google.com")
|
||||
:andThen(function(stuff)
|
||||
print("Got some stuff!", stuff)
|
||||
end)
|
||||
]]
|
||||
function Promise.new(callback)
|
||||
local promise = {
|
||||
-- Used to locate where a promise was created
|
||||
_source = debug.traceback(),
|
||||
|
||||
-- A tag to identify us as a promise
|
||||
_type = "Promise",
|
||||
|
||||
_status = Promise.Status.Started,
|
||||
|
||||
-- A table containing a list of all results, whether success or failure.
|
||||
-- Only valid if _status is set to something besides Started
|
||||
_value = nil,
|
||||
|
||||
-- Queues representing functions we should invoke when we update!
|
||||
_queuedResolve = {},
|
||||
_queuedReject = {},
|
||||
}
|
||||
|
||||
setmetatable(promise, Promise)
|
||||
|
||||
local function resolve(...)
|
||||
promise:_resolve(...)
|
||||
end
|
||||
|
||||
local function reject(...)
|
||||
promise:_reject(...)
|
||||
end
|
||||
|
||||
local ok, err = wpcall(callback, resolve, reject)
|
||||
|
||||
if not ok and promise._status == Promise.Status.Started then
|
||||
reject(err)
|
||||
end
|
||||
|
||||
return promise
|
||||
end
|
||||
|
||||
--[[
|
||||
Create a promise that represents the immediately resolved value.
|
||||
]]
|
||||
function Promise.resolve(value)
|
||||
return Promise.new(function(resolve)
|
||||
resolve(value)
|
||||
end)
|
||||
end
|
||||
|
||||
--[[
|
||||
Create a promise that represents the immediately rejected value.
|
||||
]]
|
||||
function Promise.reject(value)
|
||||
return Promise.new(function(_, reject)
|
||||
reject(value)
|
||||
end)
|
||||
end
|
||||
|
||||
--[[
|
||||
Returns a new promise that:
|
||||
* is resolved when all input promises resolve
|
||||
* is rejected if ANY input promises reject
|
||||
]]
|
||||
function Promise.all(...)
|
||||
error("unimplemented", 2)
|
||||
end
|
||||
|
||||
--[[
|
||||
Is the given object a Promise instance?
|
||||
]]
|
||||
function Promise.is(object)
|
||||
if type(object) ~= "table" then
|
||||
return false
|
||||
end
|
||||
|
||||
return object._type == "Promise"
|
||||
end
|
||||
|
||||
--[[
|
||||
Creates a new promise that receives the result of this promise.
|
||||
|
||||
The given callbacks are invoked depending on that result.
|
||||
]]
|
||||
function Promise:andThen(successHandler, failureHandler)
|
||||
-- Create a new promise to follow this part of the chain
|
||||
return Promise.new(function(resolve, reject)
|
||||
-- Our default callbacks just pass values onto the next promise.
|
||||
-- This lets success and failure cascade correctly!
|
||||
|
||||
local successCallback = resolve
|
||||
if successHandler then
|
||||
successCallback = createAdvancer(successHandler, resolve, reject)
|
||||
end
|
||||
|
||||
local failureCallback = reject
|
||||
if failureHandler then
|
||||
failureCallback = createAdvancer(failureHandler, resolve, reject)
|
||||
end
|
||||
|
||||
if self._status == Promise.Status.Started then
|
||||
-- If we haven't resolved yet, put ourselves into the queue
|
||||
table.insert(self._queuedResolve, successCallback)
|
||||
table.insert(self._queuedReject, failureCallback)
|
||||
elseif self._status == Promise.Status.Resolved then
|
||||
-- This promise has already resolved! Trigger success immediately.
|
||||
successCallback(unpack(self._value))
|
||||
elseif self._status == Promise.Status.Rejected then
|
||||
-- This promise died a terrible death! Trigger failure immediately.
|
||||
failureCallback(unpack(self._value))
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--[[
|
||||
Used to catch any errors that may have occurred in the promise.
|
||||
]]
|
||||
function Promise:catch(failureCallback)
|
||||
return self:andThen(nil, failureCallback)
|
||||
end
|
||||
|
||||
--[[
|
||||
Yield until the promise is completed.
|
||||
|
||||
This matches the execution model of normal Roblox functions.
|
||||
]]
|
||||
function Promise:await()
|
||||
if self._status == Promise.Status.Started then
|
||||
local result
|
||||
local bindable = Instance.new("BindableEvent")
|
||||
|
||||
self:andThen(function(...)
|
||||
result = {...}
|
||||
bindable:Fire(true)
|
||||
end, function(...)
|
||||
result = {...}
|
||||
bindable:Fire(false)
|
||||
end)
|
||||
|
||||
local ok = bindable.Event:Wait()
|
||||
bindable:Destroy()
|
||||
|
||||
if not ok then
|
||||
error(tostring(result[1]), 2)
|
||||
end
|
||||
|
||||
return unpack(result)
|
||||
elseif self._status == Promise.Status.Resolved then
|
||||
return unpack(self._value)
|
||||
elseif self._status == Promise.Status.Rejected then
|
||||
error(tostring(self._value[1]), 2)
|
||||
end
|
||||
end
|
||||
|
||||
function Promise:_resolve(...)
|
||||
if self._status ~= Promise.Status.Started then
|
||||
return
|
||||
end
|
||||
|
||||
-- If the resolved value was a Promise, we chain onto it!
|
||||
if Promise.is((...)) then
|
||||
-- Without this warning, arguments sometimes mysteriously disappear
|
||||
if select("#", ...) > 1 then
|
||||
local message = ("When returning a Promise from andThen, extra arguments are discarded! See:\n\n%s"):format(
|
||||
self._source
|
||||
)
|
||||
warn(message)
|
||||
end
|
||||
|
||||
(...):andThen(function(...)
|
||||
self:_resolve(...)
|
||||
end, function(...)
|
||||
self:_reject(...)
|
||||
end)
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
self._status = Promise.Status.Resolved
|
||||
self._value = {...}
|
||||
|
||||
-- We assume that these callbacks will not throw errors.
|
||||
for _, callback in ipairs(self._queuedResolve) do
|
||||
callback(...)
|
||||
end
|
||||
end
|
||||
|
||||
function Promise:_reject(...)
|
||||
if self._status ~= Promise.Status.Started then
|
||||
return
|
||||
end
|
||||
|
||||
self._status = Promise.Status.Rejected
|
||||
self._value = {...}
|
||||
|
||||
-- If there are any rejection handlers, call those!
|
||||
if not isEmpty(self._queuedReject) then
|
||||
-- We assume that these callbacks will not throw errors.
|
||||
for _, callback in ipairs(self._queuedReject) do
|
||||
callback(...)
|
||||
end
|
||||
else
|
||||
-- At this point, no one was able to observe the error.
|
||||
-- An error handler might still be attached if the error occurred
|
||||
-- synchronously. We'll wait one tick, and if there are still no
|
||||
-- observers, then we should put a message in the console.
|
||||
|
||||
local message = ("Unhandled promise rejection:\n\n%s\n\n%s"):format(
|
||||
tostring((...)),
|
||||
self._source
|
||||
)
|
||||
warn(message)
|
||||
end
|
||||
end
|
||||
|
||||
return Promise
|
||||
69
plugin/src/Server.lua
Normal file
69
plugin/src/Server.lua
Normal file
@@ -0,0 +1,69 @@
|
||||
local HttpService = game:GetService("HttpService")
|
||||
|
||||
local Server = {}
|
||||
Server.__index = Server
|
||||
|
||||
--[[
|
||||
Create a new Server using the given HTTP implementation and replacer.
|
||||
|
||||
If the context becomes invalid, `replacer` will be invoked with a new
|
||||
context that should be suitable to replace this one.
|
||||
|
||||
Attempting to invoke methods on an invalid conext will throw errors!
|
||||
]]
|
||||
function Server.connect(http)
|
||||
local context = {
|
||||
http = http,
|
||||
serverId = nil,
|
||||
currentTime = 0,
|
||||
}
|
||||
|
||||
setmetatable(context, Server)
|
||||
|
||||
return context:_start()
|
||||
end
|
||||
|
||||
function Server:_start()
|
||||
return self:getInfo()
|
||||
:andThen(function(response)
|
||||
self.serverId = response.serverId
|
||||
self.currentTime = response.currentTime
|
||||
|
||||
return self
|
||||
end)
|
||||
end
|
||||
|
||||
function Server:getInfo()
|
||||
return self.http:get("/")
|
||||
:andThen(function(response)
|
||||
response = response:json()
|
||||
|
||||
return response
|
||||
end)
|
||||
end
|
||||
|
||||
function Server:read(paths)
|
||||
local body = HttpService:JSONEncode(paths)
|
||||
|
||||
return self.http:post("/read", body)
|
||||
:andThen(function(response)
|
||||
response = response:json()
|
||||
|
||||
return response.items
|
||||
end)
|
||||
end
|
||||
|
||||
function Server:getChanges()
|
||||
local url = ("/changes/%f"):format(self.currentTime)
|
||||
|
||||
return self.http:get(url)
|
||||
:andThen(function(response)
|
||||
response = response:json()
|
||||
|
||||
self.currentTime = response.currentTime
|
||||
|
||||
return response.changes
|
||||
end)
|
||||
end
|
||||
|
||||
return Server
|
||||
Reference in New Issue
Block a user