This commit is contained in:
Alessandro 2019-02-16 23:25:56 +01:00 committed by GitHub
parent 8416afe79c
commit ea61a45658
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 249 additions and 189 deletions

438
node.lua
View File

@ -1,305 +1,362 @@
-- Node.lua --[[
-- By Ale32bit
-- https://git.ale32bit.me/Ale32bit/Node.lua Node.lua by Ale32bit
https://github.com/Ale32bit-CC/Node.lua
-- MIT License MIT License
-- Copyright (c) 2019 Alessandro "Ale32bit"
-- Full license Copyright (c) 2019 Ale32bit
-- https://git.ale32bit.me/Ale32bit/Node.lua/src/branch/master/LICENSE
-- utils 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:
local function genHex() The above copyright notice and this permission notice shall be included in all
local rand = math.floor(math.random() * math.floor(16^8)) 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.
return rand
-- 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 end
-- coroutine manager -- Thread functions --
local procs = {} 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 spawn(func) local function spawnThread(f) -- Spawn new thread
local id = #procs + 1 assertType(f, "function", 1, 2)
procs[id] = { local pid = #threads + 1
kill = false, local thread = {
thread = coroutine.create(func), tokill = false,
thread = coroutine.create(f),
filter = nil, filter = nil,
pid = pid,
} }
return id thread = setmetatable(thread, {
end __index = {
kill = function(self)
local function kill(pid) killThread(self.pid)
procs[pid].kill = true end,
end status = function(self)
return coroutine.status(self.thread)
-- Node functions end,
},
local function on(event, func)
assert(type(event) == "string", "bad argument #1 (expected string, got ".. type(func) ..")") __tostring = function(self)
assert(type(func) == "function", "bad argument #2 (expected function, got ".. type(func) ..")") return "Thread "..self.pid..": "..self:status()
local listener = {}
listener.event = event
listener.func = func
listener.id = genHex()
listener.run = true
listener.stopped = false
listener.pid = spawn(function()
while listener.run do
local ev = {os.pullEvent(event)}
spawn(function() listener.func(unpack(ev, 2)) end)
end
end)
listener = setmetatable(listener, {
__tostring = function()
return string.format("Listener 0x%x [%s] (%s)", listener.id, event, string.match(tostring(func),"%w+$"))
end, 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 return listener
end end
-- Remove event listener
local function removeListener(listener) local function removeListener(listener)
assert(type(listener) == "table", "bad argument #1 (expected listener, got ".. type(listener) ..")") assert(listener, "table")
local id = listener.id if not listener.thread then
assert(id, "bad argument #1 (expected listener, got ".. type(listener) ..")") error("bad argument #1 (expected Event Listener)", 2)
end
if listener.stopped then if listener.stopped then
return false return false
end end
listener.run = false listener.run = false
listener.stopped = true listener.stopped = true
kill(listener.pid) killThread(listener.thread.pid)
return true return true
end end
local function setInterval(func, s, ...) -- Create new interval
assert(type(func) == "function", "bad argument #1 (expected function, got ".. type(func) ..")") local function setInterval(callback, s, ...)
assertType(callback, "function", 1)
s = s or 0 s = s or 0
assert(type(s) == "number", "bad argument #2 (expected number, got ".. type(s) ..")") assertType(s, "number", 2)
local interval = {} local interval = {}
interval.interval = s interval.interval = s
interval.func = func interval.callback = callback
interval.args = {...} interval.args = {...}
interval.id = genHex()
interval.run = true interval.run = true
interval.stopped = false interval.stopped = false
interval.pid = spawn(function() interval.thread = spawnThread(function()
while interval.run do while interval.run do
sleep(interval.interval) local timer = os.startTimer( interval.interval )
spawn(function() repeat
interval.func(unpack(interval.args)) local _, t = os.pullEvent("timer")
until t == timer
spawnThread(function()
interval.callback(unpack(interval.args))
end) end)
end end
end) end)
interval = setmetatable(interval, {
__tostring = function()
return string.format("Interval 0x%x [%s]s (%s)", interval.id, s, string.match(tostring(func),"%w+$"))
end,
})
return interval return interval
end end
-- Clear interval
local function clearInterval(interval) local function clearInterval(interval)
assert(type(interval) == "table", "bad argument #1 (expected interval, got ".. type(interval) ..")") assert(interval, "table")
local id = interval.id if not interval.thread then
assert(id, "bad argument #1 (expected interval, got ".. type(interval) ..")") error("bad argument #1 (expected Interval)", 2)
end
if interval.stopped then if interval.stopped then
return false return false
end end
interval.run = false interval.run = false
interval.stopped = true interval.stopped = true
kill(interval.pid) killThread(interval.thread.pid)
return true return true
end end
local function setTimeout(func, s, ...) -- Create new timeout
assert(type(func) == "function", "bad argument #1 (expected function, got ".. type(func) ..")") local function setTimeout(callback, s, ...)
assertType(callback, "function", 1)
s = s or 0 s = s or 0
assert(type(s) == "number", "bad argument #2 (expected number, got ".. type(s) ..")") assertType(s, "number", 2)
local interval = {}
interval.timeout = s local timeout = {}
interval.func = func timeout.timeout = s
interval.args = {...} timeout.callback = callback
interval.id = genHex() timeout.args = {...}
interval.stopped = false timeout.stopped = false
interval.pid = spawn(function() timeout.thread = spawnThread(function()
sleep(interval.timeout) local timer = os.startTimer( timeout.timeout )
spawn(function() repeat
interval.func(unpack(interval.args)) local _, t = os.pullEvent("timer")
interval.stopped = true until t == timer
end) timeout.callback(unpack(timeout.args))
end) end)
interval = setmetatable(interval, { return timeout
__tostring = function()
return string.format("Timeout 0x%x [%s]s (%s)", interval.id, s, string.match(tostring(func),"%w+$"))
end,
})
return interval
end end
local function clearTimeout(interval) -- Clear timeout
assert(type(interval) == "table", "bad argument #1 (expected timeout, got ".. type(interval) ..")") local function clearTimeout(timeout)
local id = interval.id assert(timeout, "table")
assert(id, "bad argument #1 (expected timeout, got ".. type(interval) ..")") if not timeout.thread then
error("bad argument #1 (expected Timeout)", 2)
if interval.stopped then end
if timeout.stopped then
return false return false
end end
interval.stopped = true timeout.stopped = true
kill(interval.pid) killThread(timeout.thread.pid)
return true return true
end end
local function spawnFunction(func) -- New promise
assert(type(func) == "function", "bad argument #1 (expected function, got ".. type(func) ..")") local function promise(executor, options)
local pid = spawn(func) assertType(executor, "function", 1)
local thread = {} options = options or {}
thread.pid = pid options.errorOnUncaught = options.errorOnUncaught or false
thread.kill = function() assertType(options, "table", 2)
return kill(pid)
end
thread.status = function()
return coroutine.status(procs[pid].thread)
end
local tostr = string.format("Thread 0x%s [%s] (%s)", string.match(tostring(procs[pid].thread),"%w+$"), thread.pid, string.match(tostring(func),"%w+$"))
thread = setmetatable(thread, {
__tostring = function()
return tostr
end,
})
return thread
end
local function promise(func, errorOnUncaught) -- errorOnUncaught is temporary
assert(type(func) == "function", "bad argument #1 (expected function, got ".. type(func) ..")")
local promise = {} local promise = {}
promise.id = genHex() promise.options = options
promise.status = "pending" -- "resolved", "rejected" promise.status = "pending"
promise.value = nil promise.value = nil
promise.bind = function(func) promise.bind = function( callback )
assert(type(func) == "function", "bad argument (expected function, got ".. type(func) ..")") assertType(callback, "function")
promise.__then = func promise.__bind = callback
end end
promise.catch = function(func) promise.catch = function( callback )
assert(type(func) == "function", "bad argument (expected function, got ".. type(func) ..")") assertType(callback, "function")
promise.__catch = func promise.__catch = callback
end end
promise.finally = function(func) promise.finally = function( callback )
assert(type(func) == "function", "bad argument (expected function, got ".. type(func) ..")") assertType(callback, "function")
promise.__finally = func promise.__finally = callback
end end
local resolve = function(...) promise.__resolve = function( value )
promise.value = {...}
promise.status = "resolved" promise.status = "resolved"
if promise.__then then promise.value = value
spawn(function() killThread(promise.thread.pid)
promise.__then(unpack(promise.value)) if promise.__bind then
spawnThread(function()
promise.__bind(value)
end) end)
end end
if promise.__finally then if promise.__finally then
spawn(promise.__finally) spawnThread(promise.__finally)
end end
kill(promise.pid) return value
end end
local reject = function(...) promise.__reject = function( reason )
promise.value = {...}
promise.status = "rejected" promise.status = "rejected"
promise.value = reason
killThread(promise.thread.pid)
if promise.__catch then if promise.__catch then
spawn(function() spawnThread(function()
promise.__catch(unpack(promise.value)) promise.__catch(reason)
end) end)
else else
if errorOnUncaught then if promise.options.errorOnUncaught then
local val = {} error("Uncaught (in promise) "..tostring(reason), 3)
for k, v in ipairs(promise.value) do
val[k] = tostring(v)
end
error("Uncaught (in promise) "..table.concat(promise.value, " "), 2)
else else
printError("Uncaught (in promise)", unpack(promise.value)) printError("Uncaught (in promise) "..tostring(reason))
end end
end end
if promise.__finally then if promise.__finally then
spawn(promise.__finally) spawnThread(promise.__finally)
end end
kill(promise.pid) return reason
end end
promise.pid = spawn(function() promise.thread = spawnThread(function()
func(resolve, reject) executor(promise.__resolve, promise.__reject)
end) end)
promise = setmetatable(promise, {
__tostring = function()
return string.format("Promise 0x%x [%s] (%s)", promise.id, promise.status, tostring(promise.value) or "nil")
end,
})
return promise return promise
end end
local isRunning = false -- Start the event loop engine
local function init() local function init()
if isRunning then assert(not isRunning, "Event loop engine already running")
error("Node Event Loop already running", 2)
end
isRunning = true isRunning = true
while (function()
local c = 0 os.queueEvent("node_init")
for k,v in pairs(procs) do
c = c+1 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 end
return c > 0 return count > 0
end)() do end)() do
local event = {coroutine.yield()} local event = {coroutine.yield()}
for pid, proc in pairs(procs) do for pid, thread in pairs(threads) do
if proc.kill then -- remove process if killed if thread.tokill then
procs[pid] = nil threads[pid] = nil -- Remove thread if killed (and don't resume it)
else else
if proc.filter == nil or proc.filter == event[1] or event[1] == "terminate" then if thread.filter == nil or thread.filter == event[1] or event[1] == "terminate" then -- filter events
local ok, par = coroutine.resume( proc.thread, unpack(event)) local ok, par = coroutine.resume( thread.thread, unpack(event))
if not ok then
if ok then -- If ok par should be the event os.pullEvent expects
threads[pid].filter = par
else -- else the thread crashed
isRunning = false isRunning = false
error(par, 0) error(par, 0) -- terminate event loop engine because of error
break break -- just in case
else
procs[pid].filter = par
end end
end end
if coroutine.status(proc.thread) == "dead" then
procs[pid] = nil 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
end end
end end
isRunning = false isRunning = false
end end
-- Returns status
local function status() local function status()
return isRunning return isRunning
end end
-- Export and set in _ENV --
local node = { local node = {
on = on, on = on,
@ -308,11 +365,14 @@ local node = {
clearInterval = clearInterval, clearInterval = clearInterval,
setTimeout = setTimeout, setTimeout = setTimeout,
clearTimeout = clearTimeout, clearTimeout = clearTimeout,
spawn = spawnFunction, spawn = spawnThread,
promise = promise, promise = promise,
init = init, init = init,
status = status,
} }
_ENV.node = node if _ENV then
_ENV.node = node
end
return node return node