mirror of https://github.com/Ale32bit-CC/Node.lua
379 lines
9.0 KiB
Lua
379 lines
9.0 KiB
Lua
--[[
|
|
|
|
Node.lua by Ale32bit
|
|
https://github.com/Ale32bit-CC/Node.lua
|
|
|
|
MIT License
|
|
|
|
Copyright (c) 2019 Ale32bit
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in all
|
|
copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
SOFTWARE.
|
|
|
|
|
|
-- DOCUMENTATION --
|
|
|
|
To start:
|
|
|
|
local node = require("node")
|
|
|
|
Functions:
|
|
|
|
node.on( event, callback ): Event name (string), Callback (function)
|
|
Returns table with listener details and thread
|
|
|
|
node.removeListener( listener ): Listener (table)
|
|
Returns boolean if success
|
|
|
|
node.setInterval( callback, interval, [...] ): Callback (function), Interval in seconds (number), ...Arguments for callback (anything)
|
|
Returns table with interval details and thread
|
|
|
|
node.clearInterval( interval ): Interval (table)
|
|
Returns boolean if success
|
|
|
|
node.setTimeout( callback, timeout, [...] ): Callback (function), Timeout in seconds (number), ...Arguments for callback (anything)
|
|
Returns table with timeout details and thread
|
|
|
|
node.clearTimeout( timeout ): Timeout (table)
|
|
Returns boolean if success
|
|
|
|
node.spawn( function ): Function to execute in the coroutine manager (function)
|
|
Returns table containing details and methods
|
|
|
|
node.promise( executor, [options] ): Executor (function), Options (table): options.errorOnUncaught (boolean) use error() or just printError in case of uncaught reject
|
|
Returns table, 3 functions: bind(cb) on fulfill, catch(cb) on reject and finally(cb) after these functions. They all want callback functions.
|
|
|
|
node.init(): Start the event loop engine !! REQUIRED TO RUN !!
|
|
|
|
node.isRunning(): Check if event loop engine is running
|
|
Returns boolean
|
|
|
|
]]--
|
|
|
|
|
|
-- Variables --
|
|
|
|
local isRunning = false
|
|
local threads = {} -- Store all threads here
|
|
|
|
-- Utils --
|
|
|
|
local function assertType(v, exp, n, level)
|
|
n = n or 1
|
|
level = level or 3
|
|
if type(v) ~= exp then
|
|
error("bad argument #"..n.." (expected "..exp..", got "..type(v)..")", level)
|
|
end
|
|
end
|
|
|
|
-- Thread functions --
|
|
|
|
local function killThread(pid) -- Kill a thread
|
|
assertType(pid, "number", 1, 2)
|
|
if threads[pid] then
|
|
threads[pid].tokill = true
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function spawnThread(f) -- Spawn new thread
|
|
assertType(f, "function", 1, 2)
|
|
local pid = #threads + 1
|
|
local thread = {
|
|
tokill = false,
|
|
thread = coroutine.create(f),
|
|
filter = nil,
|
|
pid = pid,
|
|
}
|
|
thread = setmetatable(thread, {
|
|
__index = {
|
|
kill = function(self)
|
|
killThread(self.pid)
|
|
end,
|
|
status = function(self)
|
|
return coroutine.status(self.thread)
|
|
end,
|
|
},
|
|
|
|
__tostring = function(self)
|
|
return "Thread "..self.pid..": "..self:status()
|
|
end,
|
|
})
|
|
threads[pid] = thread
|
|
return thread, pid
|
|
end
|
|
|
|
-- Node functions --
|
|
|
|
-- Event Listener
|
|
local function on(event, callback)
|
|
assertType(event, "string", 1)
|
|
assertType(callback, "function", 2)
|
|
|
|
-- Create listener
|
|
local listener = {}
|
|
listener.event = event
|
|
listener.callback = callback
|
|
listener.run = true
|
|
listener.stopped = false
|
|
listener.thread = spawnThread(function()
|
|
while listener.run do
|
|
local ev = {os.pullEvent(listener.event)}
|
|
spawnThread(function() listener.callback(unpack(ev, 2)) end)
|
|
end
|
|
end)
|
|
|
|
return listener
|
|
end
|
|
|
|
|
|
-- Remove event listener
|
|
local function removeListener(listener)
|
|
assert(listener, "table")
|
|
if not listener.thread then
|
|
error("bad argument #1 (expected Event Listener)", 2)
|
|
end
|
|
if listener.stopped then
|
|
return false
|
|
end
|
|
listener.run = false
|
|
listener.stopped = true
|
|
killThread(listener.thread.pid)
|
|
return true
|
|
end
|
|
|
|
-- Create new interval
|
|
local function setInterval(callback, s, ...)
|
|
assertType(callback, "function", 1)
|
|
s = s or 0
|
|
assertType(s, "number", 2)
|
|
|
|
local interval = {}
|
|
interval.interval = s
|
|
interval.callback = callback
|
|
interval.args = {...}
|
|
interval.run = true
|
|
interval.stopped = false
|
|
interval.thread = spawnThread(function()
|
|
while interval.run do
|
|
local timer = os.startTimer( interval.interval )
|
|
repeat
|
|
local _, t = os.pullEvent("timer")
|
|
until t == timer
|
|
spawnThread(function()
|
|
interval.callback(unpack(interval.args))
|
|
end)
|
|
end
|
|
end)
|
|
|
|
return interval
|
|
end
|
|
|
|
-- Clear interval
|
|
local function clearInterval(interval)
|
|
assert(interval, "table")
|
|
if not interval.thread then
|
|
error("bad argument #1 (expected Interval)", 2)
|
|
end
|
|
if interval.stopped then
|
|
return false
|
|
end
|
|
|
|
interval.run = false
|
|
interval.stopped = true
|
|
killThread(interval.thread.pid)
|
|
return true
|
|
end
|
|
|
|
-- Create new timeout
|
|
local function setTimeout(callback, s, ...)
|
|
assertType(callback, "function", 1)
|
|
s = s or 0
|
|
assertType(s, "number", 2)
|
|
|
|
local timeout = {}
|
|
timeout.timeout = s
|
|
timeout.callback = callback
|
|
timeout.args = {...}
|
|
timeout.stopped = false
|
|
timeout.thread = spawnThread(function()
|
|
local timer = os.startTimer( timeout.timeout )
|
|
repeat
|
|
local _, t = os.pullEvent("timer")
|
|
until t == timer
|
|
timeout.callback(unpack(timeout.args))
|
|
end)
|
|
|
|
return timeout
|
|
end
|
|
|
|
-- Clear timeout
|
|
local function clearTimeout(timeout)
|
|
assert(timeout, "table")
|
|
if not timeout.thread then
|
|
error("bad argument #1 (expected Timeout)", 2)
|
|
end
|
|
if timeout.stopped then
|
|
return false
|
|
end
|
|
|
|
timeout.stopped = true
|
|
killThread(timeout.thread.pid)
|
|
return true
|
|
end
|
|
|
|
-- New promise
|
|
local function promise(executor, options)
|
|
assertType(executor, "function", 1)
|
|
options = options or {}
|
|
options.errorOnUncaught = options.errorOnUncaught or false
|
|
assertType(options, "table", 2)
|
|
|
|
local promise = {}
|
|
promise.options = options
|
|
promise.status = "pending"
|
|
promise.value = nil
|
|
|
|
promise.bind = function( callback )
|
|
assertType(callback, "function")
|
|
promise.__bind = callback
|
|
end
|
|
|
|
promise.catch = function( callback )
|
|
assertType(callback, "function")
|
|
promise.__catch = callback
|
|
end
|
|
|
|
promise.finally = function( callback )
|
|
assertType(callback, "function")
|
|
promise.__finally = callback
|
|
end
|
|
|
|
promise.__resolve = function( value )
|
|
promise.status = "resolved"
|
|
promise.value = value
|
|
killThread(promise.thread.pid)
|
|
if promise.__bind then
|
|
spawnThread(function()
|
|
promise.__bind(value)
|
|
end)
|
|
end
|
|
if promise.__finally then
|
|
spawnThread(promise.__finally)
|
|
end
|
|
return value
|
|
|
|
end
|
|
|
|
promise.__reject = function( reason )
|
|
promise.status = "rejected"
|
|
promise.value = reason
|
|
killThread(promise.thread.pid)
|
|
if promise.__catch then
|
|
spawnThread(function()
|
|
promise.__catch(reason)
|
|
end)
|
|
else
|
|
if promise.options.errorOnUncaught then
|
|
error("Uncaught (in promise) "..tostring(reason), 3)
|
|
else
|
|
printError("Uncaught (in promise) "..tostring(reason))
|
|
end
|
|
end
|
|
if promise.__finally then
|
|
spawnThread(promise.__finally)
|
|
end
|
|
return reason
|
|
end
|
|
|
|
promise.thread = spawnThread(function()
|
|
executor(promise.__resolve, promise.__reject)
|
|
end)
|
|
|
|
return promise
|
|
end
|
|
|
|
-- Start the event loop engine
|
|
local function init()
|
|
assert(not isRunning, "Event loop engine already running")
|
|
isRunning = true
|
|
|
|
os.queueEvent("node_init")
|
|
|
|
while (function() -- Execute loop if threads count is higher than 0
|
|
local count = 0
|
|
for k,v in pairs(threads) do
|
|
count = count+1
|
|
end
|
|
return count > 0
|
|
end)() do
|
|
local event = {coroutine.yield()}
|
|
for pid, thread in pairs(threads) do
|
|
if thread.tokill then
|
|
threads[pid] = nil -- Remove thread if killed (and don't resume it)
|
|
else
|
|
if thread.filter == nil or thread.filter == event[1] or event[1] == "terminate" then -- filter events
|
|
local ok, par = coroutine.resume( thread.thread, unpack(event))
|
|
|
|
|
|
if ok then -- If ok par should be the event os.pullEvent expects
|
|
threads[pid].filter = par
|
|
else -- else the thread crashed
|
|
isRunning = false
|
|
error(par, 0) -- terminate event loop engine because of error
|
|
break -- just in case
|
|
end
|
|
end
|
|
|
|
if coroutine.status(thread.thread) == "dead" then -- If thread died (ended with no errors) remove it from the threads table
|
|
threads[pid] = nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
isRunning = false
|
|
end
|
|
|
|
-- Returns status
|
|
local function status()
|
|
return isRunning
|
|
end
|
|
|
|
-- Export and set in _ENV --
|
|
|
|
local node = {
|
|
on = on,
|
|
removeListener = removeListener,
|
|
setInterval = setInterval,
|
|
clearInterval = clearInterval,
|
|
setTimeout = setTimeout,
|
|
clearTimeout = clearTimeout,
|
|
spawn = spawnThread,
|
|
promise = promise,
|
|
init = init,
|
|
status = status,
|
|
}
|
|
|
|
if _ENV then
|
|
_ENV.node = node
|
|
end
|
|
|
|
return node
|