mirror of
https://github.com/CFC-Servers/cfc_network_promises.git
synced 2025-03-04 03:03:19 -05:00
Add files
This commit is contained in:
parent
47907079d0
commit
c01b2f7016
1
lua/autorun/client/cl_np_init.lua
Normal file
1
lua/autorun/client/cl_np_init.lua
Normal file
@ -0,0 +1 @@
|
||||
include( "network_promises/base.lua" )
|
15
lua/autorun/server/sv_np_init.lua
Normal file
15
lua/autorun/server/sv_np_init.lua
Normal file
@ -0,0 +1,15 @@
|
||||
function addFiles( dir )
|
||||
local files, dirs = file.Find( dir .. "/*", "LUA" )
|
||||
if not files then return end
|
||||
for k, v in pairs( files ) do
|
||||
if string.match( v, "^.+%.lua$" ) then
|
||||
AddCSLuaFile( dir .. "/" .. v )
|
||||
end
|
||||
end
|
||||
for k, v in pairs( dirs ) do
|
||||
addFiles( dir .. "/" .. v )
|
||||
end
|
||||
end
|
||||
addFiles( "network_promises" )
|
||||
|
||||
include( "network_promises/base.lua" )
|
27
lua/network_promises/base.lua
Normal file
27
lua/network_promises/base.lua
Normal file
@ -0,0 +1,27 @@
|
||||
local deferred = include( "network_promises/include/deferred.lua" )
|
||||
promise = deferred -- Create global promise variable for other scripts
|
||||
|
||||
networkPromise = {}
|
||||
NP = networkPromise -- shorthand name, networkPromise will get a bit cumbersome
|
||||
|
||||
include( "network_promises/http.lua" )
|
||||
include( "network_promises/net.lua" )
|
||||
|
||||
-- Remove this for example
|
||||
do return end
|
||||
|
||||
-- Example:
|
||||
if CLIENT then
|
||||
-- Send net message, print result or error
|
||||
NP.net.send( "getFactions" ):next( function( status, data )
|
||||
print( status )
|
||||
PrintTable( data )
|
||||
end, function( err )
|
||||
print( "Error: ", err )
|
||||
end )
|
||||
else
|
||||
-- Receive on getServerStatus, return the result of fetching the scripting url
|
||||
NP.net.receive( "getFactions", function( ply )
|
||||
return NP.http.fetch( "https://factions.cfcservers.org/dev/factions" )
|
||||
end )
|
||||
end
|
70
lua/network_promises/http.lua
Normal file
70
lua/network_promises/http.lua
Normal file
@ -0,0 +1,70 @@
|
||||
NP.http = {}
|
||||
|
||||
-- Returns a promise that resolves after t seconds
|
||||
local timeoutId = 0
|
||||
local function timeoutPromise( t )
|
||||
local d = promise.new()
|
||||
timer.Create( "timeout" .. timeoutId, t, 1, function()
|
||||
d:reject( "Timeout" )
|
||||
end )
|
||||
return d
|
||||
end
|
||||
|
||||
-- http.post as a promise, resolves whenever http finishes, or never if it doesn't (Looking at you, ISteamHTTP)
|
||||
-- url : post url
|
||||
-- data : post args as table
|
||||
-- resolves to function( statusCode, data, headers )
|
||||
function NP.http.postIndef( url, data )
|
||||
local d = promise.new() -- promise itself
|
||||
http.Post( url, data, function( body, len, headers, status )
|
||||
-- Check body is valid Json, if not, reject
|
||||
local data = util.JSONToTable( body )
|
||||
if not data then
|
||||
d:reject( "Invalid json response" )
|
||||
else
|
||||
d:resolve( status, data, headers )
|
||||
end
|
||||
end, function( err )
|
||||
d:reject( err )
|
||||
end )
|
||||
return d
|
||||
end
|
||||
|
||||
-- Same as above but for fetch
|
||||
function NP.http.fetchIndef( url )
|
||||
local d = promise.new()
|
||||
http.Fetch( url, function( body, len, headers, status )
|
||||
local data = util.JSONToTable( body )
|
||||
if not data then
|
||||
d:reject( "Invalid json response" )
|
||||
else
|
||||
d:resolve( status, data, headers )
|
||||
end
|
||||
end, function( err )
|
||||
d:reject( err )
|
||||
end )
|
||||
return d
|
||||
end
|
||||
|
||||
-- Post but with enforced timeout
|
||||
-- This promise is guaranteed to resolve/reject eventually
|
||||
-- url : post url
|
||||
-- data : post args as table
|
||||
-- timeout : optional timeout in seconds (def 5)
|
||||
-- resolves to function( statusCode, data, headers )
|
||||
function NP.http.post( url, data, timeout )
|
||||
timeout = timeout or 5
|
||||
return promise.first{
|
||||
NP.http.postIndef( url, data ),
|
||||
timeoutPromise( timeout )
|
||||
}
|
||||
end
|
||||
|
||||
-- Same as above but for fetch
|
||||
function NP.http.fetch( url, timeout )
|
||||
timeout = timeout or 5
|
||||
return promise.first{
|
||||
NP.http.fetchIndef( url ),
|
||||
timeoutPromise( timeout )
|
||||
}
|
||||
end
|
335
lua/network_promises/include/deferred.lua
Normal file
335
lua/network_promises/include/deferred.lua
Normal file
@ -0,0 +1,335 @@
|
||||
-- This lib was written by zserge, https://github.com/zserge/lua-promises
|
||||
|
||||
--- A+ promises in Lua.
|
||||
--- @module deferred
|
||||
|
||||
local M = {}
|
||||
|
||||
local deferred = {}
|
||||
deferred.__index = deferred
|
||||
|
||||
local PENDING = 0
|
||||
local RESOLVING = 1
|
||||
local REJECTING = 2
|
||||
local RESOLVED = 3
|
||||
local REJECTED = 4
|
||||
|
||||
local function finish(deferred, state)
|
||||
state = state or REJECTED
|
||||
for i, f in ipairs(deferred.queue) do
|
||||
if state == RESOLVED then
|
||||
f:resolve(deferred.value)
|
||||
else
|
||||
f:reject(deferred.value)
|
||||
end
|
||||
end
|
||||
deferred.state = state
|
||||
end
|
||||
|
||||
local function isfunction(f)
|
||||
if type(f) == 'table' then
|
||||
local mt = getmetatable(f)
|
||||
return mt ~= nil and type(mt.__call) == 'function'
|
||||
end
|
||||
return type(f) == 'function'
|
||||
end
|
||||
|
||||
local function promise(deferred, next, success, failure, nonpromisecb)
|
||||
if type(deferred) == 'table' and type(deferred.value) == 'table' and isfunction(next) then
|
||||
local called = false
|
||||
local ok, err = pcall(next, deferred.value, function(v)
|
||||
if called then return end
|
||||
called = true
|
||||
deferred.value = v
|
||||
success()
|
||||
end, function(v)
|
||||
if called then return end
|
||||
called = true
|
||||
deferred.value = v
|
||||
failure()
|
||||
end)
|
||||
if not ok and not called then
|
||||
deferred.value = err
|
||||
failure()
|
||||
end
|
||||
else
|
||||
nonpromisecb()
|
||||
end
|
||||
end
|
||||
|
||||
local function fire(deferred)
|
||||
local next
|
||||
if type(deferred.value) == 'table' then
|
||||
next = deferred.value.next
|
||||
end
|
||||
promise(deferred, next, function()
|
||||
deferred.state = RESOLVING
|
||||
fire(deferred)
|
||||
end, function()
|
||||
deferred.state = REJECTING
|
||||
fire(deferred)
|
||||
end, function()
|
||||
local ok
|
||||
local v
|
||||
if deferred.state == RESOLVING and isfunction(deferred.success) then
|
||||
ok, v = pcall(deferred.success, deferred.value)
|
||||
elseif deferred.state == REJECTING and isfunction(deferred.failure) then
|
||||
ok, v = pcall(deferred.failure, deferred.value)
|
||||
if ok then
|
||||
deferred.state = RESOLVING
|
||||
end
|
||||
end
|
||||
|
||||
if ok ~= nil then
|
||||
if ok then
|
||||
deferred.value = v
|
||||
else
|
||||
deferred.value = v
|
||||
return finish(deferred)
|
||||
end
|
||||
end
|
||||
|
||||
if deferred.value == deferred then
|
||||
deferred.value = pcall(error, 'resolving promise with itself')
|
||||
return finish(deferred)
|
||||
else
|
||||
promise(deferred, next, function()
|
||||
finish(deferred, RESOLVED)
|
||||
end, function(state)
|
||||
finish(deferred, state)
|
||||
end, function()
|
||||
finish(deferred, deferred.state == RESOLVING and RESOLVED)
|
||||
end)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local function resolve(deferred, state, value)
|
||||
if deferred.state == 0 then
|
||||
deferred.value = value
|
||||
deferred.state = state
|
||||
fire(deferred)
|
||||
end
|
||||
return deferred
|
||||
end
|
||||
|
||||
--
|
||||
-- PUBLIC API
|
||||
--
|
||||
function deferred:resolve(value)
|
||||
return resolve(self, RESOLVING, value)
|
||||
end
|
||||
|
||||
function deferred:reject(value)
|
||||
return resolve(self, REJECTING, value)
|
||||
end
|
||||
|
||||
--- Returns a new promise object.
|
||||
--- @treturn Promise New promise
|
||||
--- @usage
|
||||
--- local deferred = require('deferred')
|
||||
---
|
||||
--- --
|
||||
--- -- Converting callback-based API into promise-based is very straightforward:
|
||||
--- --
|
||||
--- -- 1) Create promise object
|
||||
--- -- 2) Start your asynchronous action
|
||||
--- -- 3) Resolve promise object whenever action is finished (only first resolution
|
||||
--- -- is accepted, others are ignored)
|
||||
--- -- 4) Reject promise object whenever action is failed (only first rejection is
|
||||
--- -- accepted, others are ignored)
|
||||
--- -- 5) Return promise object letting calling side to add a chain of callbacks to
|
||||
--- -- your asynchronous function
|
||||
---
|
||||
--- function read(f)
|
||||
--- local d = deferred.new()
|
||||
--- readasync(f, function(contents, err)
|
||||
--- if err == nil then
|
||||
--- d:resolve(contents)
|
||||
--- else
|
||||
--- d:reject(err)
|
||||
--- end
|
||||
--- end)
|
||||
--- return d
|
||||
--- end
|
||||
---
|
||||
--- -- You can now use read() like this:
|
||||
--- read('file.txt'):next(function(s)
|
||||
--- print('File.txt contents: ', s)
|
||||
--- end, function(err)
|
||||
--- print('Error', err)
|
||||
--- end)
|
||||
function M.new(options)
|
||||
if isfunction(options) then
|
||||
local d = M.new()
|
||||
local ok, err = pcall(options, d)
|
||||
if not ok then
|
||||
d:reject(err)
|
||||
end
|
||||
return d
|
||||
end
|
||||
options = options or {}
|
||||
local d
|
||||
d = {
|
||||
next = function(self, success, failure)
|
||||
local next = M.new({success = success, failure = failure, extend = options.extend})
|
||||
if d.state == RESOLVED then
|
||||
next:resolve(d.value)
|
||||
elseif d.state == REJECTED then
|
||||
next:reject(d.value)
|
||||
else
|
||||
table.insert(d.queue, next)
|
||||
end
|
||||
return next
|
||||
end,
|
||||
state = 0,
|
||||
queue = {},
|
||||
success = options.success,
|
||||
failure = options.failure,
|
||||
}
|
||||
d = setmetatable(d, deferred)
|
||||
if isfunction(options.extend) then
|
||||
options.extend(d)
|
||||
end
|
||||
return d
|
||||
end
|
||||
|
||||
--- Returns a new promise object that is resolved when all promises are resolved/rejected.
|
||||
--- @param args list of promise
|
||||
--- @treturn Promise New promise
|
||||
--- @usage
|
||||
--- deferred.all({
|
||||
--- http.get('http://example.com/first'),
|
||||
--- http.get('http://example.com/second'),
|
||||
--- http.get('http://example.com/third'),
|
||||
--- }):next(function(results)
|
||||
--- -- handle results here (all requests are finished and there has been
|
||||
--- -- no errors)
|
||||
--- end, function(results)
|
||||
--- -- handle errors here (all requests are finished and there has been
|
||||
--- -- at least one error)
|
||||
--- end)
|
||||
function M.all(args)
|
||||
local d = M.new()
|
||||
if #args == 0 then
|
||||
return d:resolve({})
|
||||
end
|
||||
local method = "resolve"
|
||||
local pending = #args
|
||||
local results = {}
|
||||
|
||||
local function synchronizer(i, resolved)
|
||||
return function(value)
|
||||
results[i] = value
|
||||
if not resolved then
|
||||
method = "reject"
|
||||
end
|
||||
pending = pending - 1
|
||||
if pending == 0 then
|
||||
d[method](d, results)
|
||||
end
|
||||
return value
|
||||
end
|
||||
end
|
||||
|
||||
for i = 1, pending do
|
||||
args[i]:next(synchronizer(i, true), synchronizer(i, false))
|
||||
end
|
||||
return d
|
||||
end
|
||||
|
||||
--- Returns a new promise object that is resolved with the values of sequential application of function fn to each element in the list. fn is expected to return promise object.
|
||||
--- @function map
|
||||
--- @param args list of promise
|
||||
--- @param fn promise used to resolve the list of promise
|
||||
--- @return a new promise
|
||||
--- @usage
|
||||
--- local items = {'a.txt', 'b.txt', 'c.txt'}
|
||||
--- -- Read 3 files, one by one
|
||||
--- deferred.map(items, read):next(function(files)
|
||||
--- -- here files is an array of file contents for each of the files
|
||||
--- end, function(err)
|
||||
--- -- handle reading error
|
||||
--- end)
|
||||
function M.map(args, fn)
|
||||
local d = M.new()
|
||||
local results = {}
|
||||
local function donext(i)
|
||||
if i > #args then
|
||||
d:resolve(results)
|
||||
else
|
||||
fn(args[i]):next(function(res)
|
||||
table.insert(results, res)
|
||||
donext(i+1)
|
||||
end, function(err)
|
||||
d:reject(err)
|
||||
end)
|
||||
end
|
||||
end
|
||||
donext(1)
|
||||
return d
|
||||
end
|
||||
|
||||
--- Returns a new promise object that is resolved as soon as the first of the promises gets resolved/rejected.
|
||||
--- @param args list of promise
|
||||
--- @treturn Promise New promise
|
||||
--- @usage
|
||||
--- -- returns a promise that gets rejected after a certain timeout
|
||||
--- function timeout(sec)
|
||||
--- local d = deferred.new()
|
||||
--- settimeout(function()
|
||||
--- d:reject('Timeout')
|
||||
--- end, sec)
|
||||
--- return d
|
||||
--- end
|
||||
---
|
||||
--- deferred.first({
|
||||
--- read(somefile), -- resolves promise with contents, or rejects with error
|
||||
--- timeout(5),
|
||||
--- }):next(function(result)
|
||||
--- -- file was read successfully...
|
||||
--- end, function(err)
|
||||
--- -- either timeout or I/O error...
|
||||
--- end)
|
||||
function M.first(args)
|
||||
local d = M.new()
|
||||
for _, v in ipairs(args) do
|
||||
v:next(function(res)
|
||||
d:resolve(res)
|
||||
end, function(err)
|
||||
d:reject(err)
|
||||
end)
|
||||
end
|
||||
return d
|
||||
end
|
||||
|
||||
--- A promise is an object that can store a value to be retrieved by a future object.
|
||||
--- @type Promise
|
||||
|
||||
--- Wait for the promise object.
|
||||
--- @function next
|
||||
--- @tparam function cb resolve callback (function(value) end)
|
||||
--- @tparam[opt] function errcb rejection callback (function(reject_value) end)
|
||||
--- @usage
|
||||
--- -- Reading two files sequentially:
|
||||
--- read('first.txt'):next(function(s)
|
||||
--- print('File file:', s)
|
||||
--- return read('second.txt')
|
||||
--- end):next(function(s)
|
||||
--- print('Second file:', s)
|
||||
--- end):next(nil, function(err)
|
||||
--- -- error while reading first or second file
|
||||
--- print('Error', err)
|
||||
--- end)
|
||||
|
||||
--- Resolve promise object with value.
|
||||
--- @function resolve
|
||||
--- @param value promise value
|
||||
--- @return resolved future result
|
||||
|
||||
--- Reject promise object with value.
|
||||
--- @function reject
|
||||
--- @param value promise value
|
||||
--- @return rejected future result
|
||||
|
||||
return M
|
70
lua/network_promises/net.lua
Normal file
70
lua/network_promises/net.lua
Normal file
@ -0,0 +1,70 @@
|
||||
NP.net = {}
|
||||
local netSendId = 0
|
||||
local netSends = {}
|
||||
|
||||
-- I don't know if we want this, could be convenient, remove if not.
|
||||
local function ensureNetworkString( str )
|
||||
local pooled = util.NetworkStringToID( str ) ~= 0
|
||||
if SERVER and not pooled then util.AddNetworkString( str ) end
|
||||
end
|
||||
|
||||
-- Send a net message with vararg data, requires receiver to use NP.net.receive
|
||||
-- name : net message name
|
||||
-- ... : netArg1, netArg2
|
||||
function NP.net.send( name, ... )
|
||||
ensureNetworkString( name )
|
||||
local d = promise.new()
|
||||
net.Start( name )
|
||||
net.WriteInt( netSendId, 16 )
|
||||
net.WriteTable{ ... }
|
||||
net.SendToServer()
|
||||
|
||||
-- Incase a message is sent multiple times before a reply
|
||||
-- Store it to a table so the correct response matches the correct promise
|
||||
netSends[netSendId] = d
|
||||
net.Receive( name, function( len, ply )
|
||||
local id = net.ReadInt( 16 )
|
||||
local success = net.ReadBool()
|
||||
local promise = netSends[id]
|
||||
if not promise then return end
|
||||
if success then
|
||||
promise:resolve( unpack( net.ReadTable() ) )
|
||||
else
|
||||
promise:reject( unpack( net.ReadTable() ) )
|
||||
end
|
||||
end )
|
||||
|
||||
return d
|
||||
end
|
||||
|
||||
-- net.Receive wrapper that removes need for net.ReadThis and net.ReadThat
|
||||
-- name : net message name
|
||||
-- func : function( player, netArg1, netArg2 )
|
||||
function NP.net.receive( name, func )
|
||||
ensureNetworkString( name )
|
||||
net.Receive( name, function( len, ply )
|
||||
local id = net.ReadInt( 16 )
|
||||
local data = net.ReadTable()
|
||||
local ret = { func( ply, unpack( data ) ) }
|
||||
|
||||
local function finish( status, args )
|
||||
net.Start( name )
|
||||
net.WriteInt( id, 16 )
|
||||
net.WriteBool( status )
|
||||
net.WriteTable( args )
|
||||
net.Send( ply )
|
||||
end
|
||||
if ret[1] and ret[1].next then
|
||||
ret[1]:next( function( ... )
|
||||
finish( true, { ... } )
|
||||
end,
|
||||
function( ... )
|
||||
finish( false, { ... } )
|
||||
end )
|
||||
else
|
||||
finish( true, ret )
|
||||
end
|
||||
|
||||
end )
|
||||
end
|
||||
|
Loading…
Reference in New Issue
Block a user