diff --git a/BatchHTTP.lua b/BatchHTTP.lua new file mode 100644 index 0000000..0ba8152 --- /dev/null +++ b/BatchHTTP.lua @@ -0,0 +1,141 @@ +-- ## CONFIG ## -- + +local SERVER_BASE_URL:string = "http://localhost:7000/" +local REQUESTS_PER_MINUTE:number = 500 -- roblox 500 requests per minute limit +local ALLOW_CLIENT = true; + +-- ## MODULE ## -- + +local exposed = {} + +local isServer = game:GetService("RunService"):IsServer() + +shared.BatchHttpModule = shared.BatchHttpModule or {} +shared.BatchHttpModule.queue = shared.BatchHttpModule.queue or {} +shared.BatchHttpModule.signal = shared.BatchHttpModule.signal or Instance.new("BindableEvent") +shared.BatchHttpModule.lastSent = shared.BatchHttpModule.lastSent or 0; +shared.BatchHttpModule.waitingForResponse = shared.BatchHttpModule.waitingForResponse or {} + +shared.BatchHttpModule.clientRemote = shared.BatchHttpModule.clientRemote or (function() + local funcName = "_BatchHTTP" + local funcLocation = workspace + if (isServer) then + local remoteFunction = Instance.new("RemoteFunction") + remoteFunction.Name = funcName + remoteFunction.Parent = funcLocation + remoteFunction.OnServerInvoke = function(plr,options) + if not ALLOW_CLIENT then return end + return exposed:RequestAsync(options) + end + return remoteFunction + else + return funcLocation:WaitForChild(funcName) + end +end)() + +local signal:BindableEvent = shared.BatchHttpModule.signal +local THREAD_INTERVAL:number = (60/REQUESTS_PER_MINUTE) + +local RealHttpService = game:GetService("HttpService") +local InstanceSecret = (RealHttpService:GenerateGUID(false).."--"..game.JobId) -- not only using jobid so that random people cant just spam the api to get a response + +function ProcessQueue() + --print("processing queue") + local requests = {} + local waiting = shared.BatchHttpModule.waitingForResponse + for _,queueObj in shared.BatchHttpModule.queue do + table.insert(requests, queueObj) + table.insert(shared.BatchHttpModule.waitingForResponse,queueObj.id) + end + + local body = {} + if (#requests ~= 0) then body.requests = requests end + if (#waiting ~= 0) then body.waiting = waiting end + + if not (body.waiting or body.requests) then return end + + shared.BatchHttpModule.lastSent = tick() + + local jsonBody = RealHttpService:JSONEncode(body) + shared.BatchHttpModule.queue = {} + local response = RealHttpService:PostAsync(SERVER_BASE_URL..InstanceSecret, jsonBody, Enum.HttpContentType.ApplicationJson, true, { + ["X-BatchHttp-Api-Key"] = "silly!" -- i know this exposes the key to client but its just temporary (permanent) + }) + local data = RealHttpService:JSONDecode(response) + + signal:Fire(data.responses or {}) +end + +function AddToQueue(queueObj) + table.insert(shared.BatchHttpModule.queue, queueObj) +end + +function DoRequest(request) + local id = RealHttpService:GenerateGUID(false); + print("[BatchHTTP] sending request",id) + AddToQueue({ + id = id, + request = request, + }) + local response = nil; + while not response do + local receivedData = signal.Event:Wait() + for _,obj in receivedData do + if obj.id == id then + response = obj + local tableIndex = table.find(shared.BatchHttpModule.waitingForResponse,id) + if tableIndex then + table.remove(shared.BatchHttpModule.waitingForResponse,tableIndex) + end + end + end + end + + print("[BatchHTTP] got response",id) + return response.data +end + +shared.BatchHttpModule.thread = shared.BatchHttpModule.thread or coroutine.wrap(function() + while true do + if (tick() - shared.BatchHttpModule.lastSent) > THREAD_INTERVAL then + task.spawn(ProcessQueue) + end + task.wait() + end +end)() + +exposed.HttpEnabled = RealHttpService.HttpEnabled + +exposed.GenerateGUID = RealHttpService.GenerateGUID +exposed.GetSecret = RealHttpService.GetSecret + +exposed.JSONDecode = RealHttpService.JSONDecode +exposed.JSONEncode = RealHttpService.JSONEncode +exposed.UrlEncode = RealHttpService.UrlEncode + +function exposed:RequestAsync(options) + if (isServer) then + return DoRequest(options) + else + return shared.BatchHttpModule.clientRemote:InvokeServer(options) + end +end + +function exposed:PostAsync(url, body, contentType, compress, headers) + return exposed:RequestAsync({ + Method = "post"; + Url = url; + Body = body; + Headers = headers; + }) +end + +function exposed:GetAsync(url) + return exposed:RequestAsync({ + Method = "get"; + Url = url; + }) +end + +return exposed; +