Performance rewrite part 1

removed acking, took way too long with client delay

added better debug messages
This commit is contained in:
Foereaper
2021-07-21 10:16:54 +02:00
parent b7a4c9947e
commit 98a08522c5
2 changed files with 107 additions and 191 deletions

View File

@ -2,15 +2,16 @@ local debug = false
local CMH = {}
local datacache = {}
local CSMHMsgPrefix = ""
local delim = {"", "", "", ""}
local pck = {REQ = 1, ACK = 2, DAT = 3, NAK = 4}
local delim = {"", "", "", "", ""}
local pck = {REQ = 1, DAT = 2}
-- HELPERS START
local function debugOut(msg)
local function debugOut(prefix, x, msg)
prefix = prefix or ""
msg = msg or ""
x = x or ""
if(debug == true) then
print("CMH Debug: "..msg)
print("["..date("%X", time()).."][CSMH]["..x.."]["..prefix.."]: "..msg)
end
end
@ -26,6 +27,7 @@ local function GenerateReqId()
end
local function ParseMessage(str)
str = str or ""
local output = {}
local valTemp = {}
local typeTemp = {}
@ -50,11 +52,11 @@ local function ParseMessage(str)
-- Convert value to correct type
for k, v in pairs(valTemp) do
local varType = typeTemp[k]
if(varType == 2) then -- Ints
if(varType == 3) then -- Ints
v = tonumber(v)
elseif(varType == 3) then -- Tables
elseif(varType == 4) then -- Tables
v = Smallfolk.loads(v, #v)
elseif(varType == 4) then -- Booleans
elseif(varType == 5) then -- Booleans
if(v == "true") then v = true else v = false end
end
table.insert(output, v)
@ -67,22 +69,22 @@ local function ParseMessage(str)
end
local function ProcessVariables(reqId, ...)
local arg = {...}
local splitLength = 200
local arg = {...}
local msg = ""
for _, v in pairs(arg) do
if(type(v) == "string") then
msg = msg .. delim[1]
elseif(type(v) == "number") then
msg = msg .. delim[2]
elseif(type(v) == "number") then
msg = msg .. delim[3]
elseif(type(v) == "table") then
-- use Smallfolk to convert table structure to string
v = Smallfolk.dumps(v)
msg = msg .. delim[3]
msg = msg .. delim[4]
elseif(type(v) == "boolean") then
v = tostring(v)
msg = msg .. delim[4]
msg = msg .. delim[5]
end
msg = msg .. v
end
@ -92,8 +94,8 @@ local function ProcessVariables(reqId, ...)
end
for i=1, msg:len(), splitLength do
datacache[reqId]["data"][#datacache[reqId]["data"]+1] = msg:sub(i,i+splitLength - 1)
datacache[reqId].count = datacache[reqId].count + 1
datacache[reqId]["data"][datacache[reqId].count] = msg:sub(i,i+splitLength - 1)
end
return datacache[reqId]
@ -103,36 +105,28 @@ end
-- Rx START
function CMH.OnReceive(self, event, prefix, _, Type, sender)
function CMH.OnReceive(self, event, header, data, Type, sender)
-- Ensure the sender and receiver is the same, the message is an addon message, and the message type is WHISPER
if event == "CHAT_MSG_ADDON" and sender == UnitName("player") and Type == "WHISPER" then
-- unpack and validate addon message structure
local pfx, source, pckId, data = prefix:match("(...)(%u)(%d%d)(.+)")
local pfx, source, pckId = header:match("(...)(%u)(%d%d)")
if not pfx or not source or not pckId then
return
end
-- Make sure we're only processing addon messages using our framework prefix character as welll as server messages
if(pfx == CSMHMsgPrefix and source == "S") then
debugOut("Received CSMH packet, processing data.")
-- Make sure we're only processing addon messages using our framework prefix character as well as client messages
if(pfx == delim[1] and source == "S") then
-- convert ID to number so we can compare with our packet list
pckId = tonumber(pckId)
if(pckId == pck.REQ) then
debugOut("REQ received, data: "..data)
debugOut("REQ", "Rx", "REQ received, data: "..data)
CMH.OnREQ(sender, data)
elseif(pckId == pck.ACK) then
debugOut("ACK received, data: "..data)
CMH.OnACK(sender, data)
elseif(pckId == pck.DAT) then
debugOut("DAT received, data: "..data)
debugOut("DAT", "Rx", "DAT received, data: "..data)
CMH.OnDAT(sender, data)
elseif(pckId == pck.NAK) then
debugOut("NAK received, data: "..data)
CMH.OnNAK(sender, data)
else
debugOut("Invalid packet ID, aborting")
debugOut("ERR", "Rx", "Invalid packet type, aborting")
return
end
end
@ -144,11 +138,11 @@ CMHFrame:RegisterEvent("CHAT_MSG_ADDON")
CMHFrame:SetScript("OnEvent", CMH.OnReceive)
function CMH.OnREQ(sender, data)
debugOut("Processing REQ data")
debugOut("REQ", "Rx", "Processing data..")
-- split header string into proper variables and ensure the string is the expected format
local functionId, linkCount, reqId, addon = data:match("(%d%d)(%d%d%d)(%w%w%w%w%w%w)(.+)");
if not functionId or not linkCount or not reqId or not addon then
debugOut("Malformed REQ data, aborting.")
debugOut("REQ", "Rx", "Malformed data, aborting.")
return
end
@ -157,62 +151,43 @@ function CMH.OnREQ(sender, data)
-- if the addon does not exist, abort
if not CMH[addon] then
CMH.SendNAK(reqId)
debugOut("Invalid addon, aborting")
debugOut("REQ", "Rx", "Invalid addon, aborting.")
return
end
-- if the functionId does not exist for said addon, abort
if not CMH[addon][functionId] then
CMH.SendNAK(reqId)
debugOut("Invalid addon function, aborting")
debugOut("REQ", "Rx", "Invalid addon function, aborting.")
return
end
-- the request cache already exists, this should not happen.
-- abort and send error to the client, as well as purge id from cache.
if(datacache[reqId]) then
CMH.SendNAK(reqId)
datacache[reqId] = nil
debugOut("Request cache already exists, aborting.")
debugOut("REQ", "Rx", "Cache already exists, aborting.")
return
end
-- Insert header info for request id and prepare temporary data storage
datacache[reqId] = {addon = addon, funcId = functionId, count = linkCount, data = {}}
-- send ACK to client notifying client that data is ready to be received
debugOut("REQ OK, sending ACK..")
CMH.SendACK(reqId)
end
function CMH.OnACK(sender, data)
local reqId = data:match("(%w%w%w%w%w%w)");
if not reqId then
return
end
-- We received ACK but no data is available in cache. This should never happen
if not datacache[reqId] then
debugOut("ACK received but no data available to transmit. Aborting.")
return
end
-- If data exists, we send it
debugOut("ACK validated, data exists. Sending..")
CMH.SendDAT(reqId)
debugOut("REQ", "Rx", "Header validated, cache created. Awaitng data..")
end
function CMH.OnDAT(sender, data)
debugOut("DAT", "Rx", "Validating data..")
-- Separate REQ ID from payload and verify
local reqId, payload = data:match("(%w%w%w%w%w%w)(.*)");
if not reqId and not payload then
local reqId = data:sub(1, 6)
local payload = data:sub(#reqId+1)
if not reqId then
debugOut("DAT", "Rx", "Request ID missing, aborting.")
return
end
-- If no REQ header info has been cached, abort
if not datacache[reqId] then
debugOut("Data received, but not expected. Aborting.")
debugOut("DAT", "Rx", "Cache does not exist, aborting.")
return
end
@ -222,28 +197,32 @@ function CMH.OnDAT(sender, data)
-- Some functions are trigger functions and expect no payload
-- Skip the rest of the functionality and call the expected function
if reqTable.count == 0 then
debugOut("DAT", "Rx", "Function expects no data, triggering function..")
-- Retrieve the function from global namespace and pass variables if it exists
local func = SMH[reqTable.addon][reqTable.funcId]
local func = CMH[reqTable.addon][reqTable.funcId]
if func then
debugOut(func)
_G[func](sender, {})
datacache[reqId] = nil
debugOut("DAT", "Rx", "Function "..func.." @ "..reqTable.addon.." executed, cache cleared.")
end
return
end
-- If the size of the cache is larger than expected, abort
if sizeOfDataCache+1 > reqTable.count then
debugOut("Received more data than expected. Aborting.")
debugOut("DAT", "Rx", "Received more data than expected. Aborting.")
return
end
-- Add payload to cache and update size variable
reqTable["data"][sizeOfDataCache+1] = payload
sizeOfDataCache = #reqTable.data
debugOut("DAT", "Rx", "Data part "..sizeOfDataCache.." of "..reqTable.count.." added to cache.")
-- If the last expected message has been received, process it
if(sizeOfDataCache == reqTable.count) then
debugOut("DAT", "Rx", "All expected data received, processing..")
-- Concatenate the cache and parse the full payload for function variables to return
local fullPayload = table.concat(reqTable.data);
local VarTable = ParseMessage(fullPayload)
@ -251,64 +230,42 @@ function CMH.OnDAT(sender, data)
-- Retrieve the function from global namespace and pass variables if it exists
local func = CMH[reqTable.addon][reqTable.funcId]
if func then
debugOut(func)
_G[func](sender, VarTable)
datacache[reqId] = nil
debugOut("DAT", "Rx", "Function "..func.." @ "..reqTable.addon.." executed, cache cleared.")
end
-- Delete the request session cache
datacache[reqId] = nil
end
end
function CMH.OnNAK(sender, data)
local reqId = data:match("(%w%w%w%w%w%w)");
if not reqId then
return
end
-- when we receive an error from the server, purge the local cache data
debugOut("Purging cache data with REQ ID: "..reqId)
datacache[reqId] = nil
end
-- Rx END
-- Tx START
function CMH.SendREQ(functionId, linkCount, reqId, addon)
debugOut("Sending REQ with ID: "..reqId)
local send = string.format("%01s%01s%02d%02d%03d%06s%0"..tostring(#addon).."s", CSMHMsgPrefix, "C", pck.REQ, functionId, linkCount, reqId, addon)
SendAddonMessage(send, "", "WHISPER", UnitName("player"))
end
function CMH.SendACK(reqId)
local send = string.format("%01s%01s%02d%06s", CSMHMsgPrefix, "C", pck.ACK, reqId)
SendAddonMessage(send, "", "WHISPER", UnitName("player"))
local header = string.format("%01s%01s%02d", delim[1], "C", pck.REQ)
local data = string.format("%02d%03d%06s%0"..tostring(#addon).."s", functionId, linkCount, reqId, addon)
SendAddonMessage(header, data, "WHISPER", UnitName("player"))
debugOut("REQ", "Tx", "Sent REQ with ID "..reqId..", sending DAT..")
end
function CMH.SendDAT(reqId)
-- Build data message header
local send = string.format("%01s%01s%02d%06s", CSMHMsgPrefix, "C", pck.DAT, reqId)
local header = string.format("%01s%01s%02d", delim[1], "C", pck.DAT)
-- iterate all items in the message data cache and send
-- functions can also be trigger functions without any data, only send header and no payload
if(#datacache[reqId]["data"] == 0) then
SendAddonMessage(send, "", "WHISPER", UnitName("player"))
SendAddonMessage(header, reqId, "WHISPER", UnitName("player"))
else
for _, v in pairs (datacache[reqId]["data"]) do
local payload = send..v
SendAddonMessage(payload, "", "WHISPER", UnitName("player"))
local payload = reqId..v
SendAddonMessage(header, payload, "WHISPER", UnitName("player"))
end
end
-- all items have been sent, cache can be purged
debugOut("All data sent, cleaning up cache.")
datacache[reqId] = nil
end
function CMH.SendNAK(reqId)
local send = string.format("%01s%01s%02d%06s", CSMHMsgPrefix, "C", pck.NAK, reqId)
SendAddonMessage(send, "", "WHISPER", UnitName("player"))
debugOut("DAT", "Tx", "Sent all DAT for ID "..reqId..", cache cleared, closing.")
end
-- Tx END
@ -332,6 +289,7 @@ function SendClientRequest(prefix, functionId, ...)
local varTable = ProcessVariables(reqId, ...)
CMH.SendREQ(functionId, varTable.count, reqId, prefix)
CMH.SendDAT(reqId)
end
--A API END

View File

@ -4,15 +4,16 @@ local debug = false
local SMH = {}
local datacache = {}
local CSMHMsgPrefix = ""
local delim = {"", "", "", ""}
local pck = {REQ = 1, ACK = 2, DAT = 3, NAK = 4}
local delim = {"", "", "", "", ""}
local pck = {REQ = 1, DAT = 2}
-- HELPERS START
local function debugOut(msg)
local function debugOut(prefix, x, msg)
prefix = prefix or ""
msg = msg or ""
x = x or ""
if(debug == true) then
print("SMH Debug: "..msg)
print("["..os.clock().."][CSMH]["..x.."]["..prefix.."]: "..msg)
end
end
@ -28,6 +29,7 @@ local function GenerateReqId()
end
local function ParseMessage(str)
str = str or ""
local output = {}
local valTemp = {}
local typeTemp = {}
@ -52,11 +54,11 @@ local function ParseMessage(str)
-- Convert value to correct type
for k, v in pairs(valTemp) do
local varType = typeTemp[k]
if(varType == 2) then -- Ints
if(varType == 3) then -- Ints
v = tonumber(v)
elseif(varType == 3) then -- Tables
elseif(varType == 4) then -- Tables
v = smallfolk.loads(v)
elseif(varType == 4) then -- Booleans
elseif(varType == 5) then -- Booleans
if(v == "true") then v = true else v = false end
end
table.insert(output, v)
@ -69,22 +71,22 @@ local function ParseMessage(str)
end
local function ProcessVariables(sender, reqId, ...)
local arg = {...}
local splitLength = 200
local arg = {...}
local msg = ""
for _, v in pairs(arg) do
if(type(v) == "string") then
msg = msg .. delim[1]
elseif(type(v) == "number") then
msg = msg .. delim[2]
elseif(type(v) == "number") then
msg = msg .. delim[3]
elseif(type(v) == "table") then
-- use Smallfolk to convert table structure to string
v = Smallfolk.dumps(v)
msg = msg .. delim[3]
msg = msg .. delim[4]
elseif(type(v) == "boolean") then
v = tostring(v)
msg = msg .. delim[4]
msg = msg .. delim[5]
end
msg = msg .. v
end
@ -96,8 +98,8 @@ local function ProcessVariables(sender, reqId, ...)
end
for i=1, msg:len(), splitLength do
datacache[sender:GetGUIDLow()][reqId]["data"][#datacache[sender:GetGUIDLow()][reqId]["data"]+1] = msg:sub(i,i+splitLength - 1)
datacache[sender:GetGUIDLow()][reqId].count = datacache[sender:GetGUIDLow()][reqId].count + 1
datacache[sender:GetGUIDLow()][reqId]["data"][datacache[sender:GetGUIDLow()][reqId].count] = msg:sub(i,i+splitLength - 1)
end
return datacache[sender:GetGUIDLow()][reqId]
@ -107,7 +109,7 @@ end
-- Rx START
function SMH.OnReceive(event, sender, _type, prefix, _, target)
function SMH.OnReceive(event, sender, _type, header, data, target)
-- Make sure the sender and receiver of the addon message is set and is the correct type.
-- Prevents error spam in the console
if not sender or not target or not sender.GetName or not target.GetName or type(sender) ~= "userdata" or type(target) ~= "userdata" then
@ -117,32 +119,24 @@ function SMH.OnReceive(event, sender, _type, prefix, _, target)
-- Ensure the sender and receiver is the same, and the message type is WHISPER
if sender:GetName() == target:GetName() and _type == 7 then
-- unpack and validate addon message structure
local pfx, source, pckId, data = prefix:match("(...)(%u)(%d%d)(.+)")
local pfx, source, pckId = header:match("(...)(%u)(%d%d)")
if not pfx or not source or not pckId then
return
end
-- Make sure we're only processing addon messages using our framework prefix character as well as client messages
if(pfx == CSMHMsgPrefix and source == "C") then
debugOut("Received CSMH packet, processing data.")
if(pfx == delim[1] and source == "C") then
-- convert ID to number so we can compare with our packet list
pckId = tonumber(pckId)
if(pckId == pck.REQ) then
debugOut("REQ received, data: "..data)
debugOut("REQ", "Rx", "REQ received, data: "..data)
SMH.OnREQ(sender, data)
elseif(pckId == pck.ACK) then
debugOut("ACK received, data: "..data)
SMH.OnACK(sender, data)
elseif(pckId == pck.DAT) then
debugOut("DAT received, data: "..data)
debugOut("DAT", "Rx", "DAT received, data: "..data)
SMH.OnDAT(sender, data)
elseif(pckId == pck.NAK) then
debugOut("NAK received, data: "..data)
SMH.OnNAK(sender, data)
else
debugOut("Invalid packet ID, aborting")
debugOut("ERR", "Rx", "Invalid packet type, aborting")
return
end
end
@ -152,11 +146,11 @@ end
RegisterServerEvent(30, SMH.OnReceive)
function SMH.OnREQ(sender, data)
debugOut("Processing REQ data")
debugOut("REQ", "Rx", "Processing data..")
-- split header string into proper variables and ensure the string is the expected format
local functionId, linkCount, reqId, addon = data:match("(%d%d)(%d%d%d)(%w%w%w%w%w%w)(.+)");
if not functionId or not linkCount or not reqId or not addon then
debugOut("Malformed REQ data, aborting.")
debugOut("REQ", "Rx", "Malformed data, aborting.")
return
end
@ -165,15 +159,13 @@ function SMH.OnREQ(sender, data)
-- if the addon does not exist, abort
if not SMH[addon] then
SMH.SendNAK(sender, reqId)
debugOut("Invalid addon, aborting")
debugOut("REQ", "Rx", "Invalid addon, aborting.")
return
end
-- if the functionId does not exist for said addon, abort
if not SMH[addon][functionId] then
SMH.SendNAK(sender, reqId)
debugOut("Invalid addon function, aborting")
debugOut("REQ", "Rx", "Invalid addon function, aborting.")
return
end
@ -183,47 +175,31 @@ function SMH.OnREQ(sender, data)
-- the request cache already exists, this should not happen.
-- abort and send error to the client, as well as purge id from cache.
if(datacache[sender:GetGUIDLow()][reqId]) then
SMH.SendNAK(sender, reqId)
datacache[sender:GetGUIDLow()][reqId] = nil
debugOut("Request cache already exists, aborting.")
debugOut("REQ", "Rx", "Cache already exists, aborting.")
return
end
-- Insert header info for request id and prepare temporary data storage
datacache[sender:GetGUIDLow()][reqId] = {addon = addon, funcId = functionId, count = linkCount, data = {}}
-- send ACK to client notifying client that data is ready to be received
debugOut("REQ OK, sending ACK..")
SMH.SendACK(sender, reqId)
debugOut("REQ", "Rx", "Header validated, cache created. Awaitng data..")
end
function SMH.OnACK(sender, data)
local reqId = data:match("(%w%w%w%w%w%w)");
if not reqId then
return
end
-- We received ACK but no data is available in cache. This should never happen
if not datacache[sender:GetGUIDLow()][reqId] then
debugOut("ACK received but no data available to transmit. Aborting.")
return
end
-- If data exists, we send it
debugOut("ACK validated, data exists. Sending..")
SMH.SendDAT(sender, reqId)
end
function SMH.OnDAT(sender, data)
debugOut("DAT", "Rx", "Validating data..")
-- Separate REQ ID from payload and verify
local reqId, payload = data:match("(%w%w%w%w%w%w)(.*)");
local reqId = data:sub(1, 6)
local payload = data:sub(#reqId+1)
if not reqId then
debugOut("DAT", "Rx", "Request ID missing, aborting.")
return
end
-- If no REQ header info has been cached, abort
if not datacache[sender:GetGUIDLow()][reqId] then
debugOut("Data received, but not expected. Aborting.")
debugOut("DAT", "Rx", "Cache does not exist, aborting.")
return
end
@ -233,28 +209,32 @@ function SMH.OnDAT(sender, data)
-- Some functions are trigger functions and expect no payload
-- Skip the rest of the functionality and call the expected function
if reqTable.count == 0 then
debugOut("DAT", "Rx", "Function expects no data, triggering function..")
-- Retrieve the function from global namespace and pass variables if it exists
local func = SMH[reqTable.addon][reqTable.funcId]
if func then
debugOut(func)
_G[func](sender, {})
datacache[sender:GetGUIDLow()][reqId] = nil
debugOut("DAT", "Rx", "Function "..func.." @ "..reqTable.addon.." executed, cache cleared.")
end
return
end
-- If the size of the cache is larger than expected, abort
if sizeOfDataCache+1 > reqTable.count then
debugOut("Received more data than expected. Aborting.")
debugOut("DAT", "Rx", "Received more data than expected. Aborting.")
return
end
-- Add payload to cache and update size variable
reqTable["data"][sizeOfDataCache+1] = payload
sizeOfDataCache = #reqTable.data
debugOut("DAT", "Rx", "Data part "..sizeOfDataCache.." of "..reqTable.count.." added to cache.")
-- If the last expected message has been received, process it
if(sizeOfDataCache == reqTable.count) then
debugOut("DAT", "Rx", "All expected data received, processing..")
-- Concatenate the cache and parse the full payload for function variables to return
local fullPayload = table.concat(reqTable.data);
local VarTable = ParseMessage(fullPayload)
@ -262,64 +242,42 @@ function SMH.OnDAT(sender, data)
-- Retrieve the function from global namespace and pass variables if it exists
local func = SMH[reqTable.addon][reqTable.funcId]
if func then
debugOut(func)
_G[func](sender, VarTable)
datacache[sender:GetGUIDLow()][reqId] = nil
debugOut("DAT", "Rx", "Function "..func.." @ "..reqTable.addon.." executed, cache cleared.")
end
-- Delete the request session cache
datacache[sender:GetGUIDLow()][reqId] = nil
end
end
function SMH.OnNAK(sender, data)
local reqId = data:match("(%w%w%w%w%w%w)");
if not reqId then
return
end
-- when we receive an error from the server, purge the local cache data
debugOut("Purging cache data with REQ ID: "..reqId)
datacache[sender:GetGUIDLow()][reqId] = nil
end
-- Rx END
-- Tx START
function SMH.SendREQ(sender, functionId, linkCount, reqId, addon)
debugOut("Sending REQ with ID: "..reqId)
local send = string.format("%01s%01s%02d%02d%03d%06s%0"..tostring(#addon).."s", CSMHMsgPrefix, "S", pck.REQ, functionId, linkCount, reqId, addon)
sender:SendAddonMessage(send, "", 7, sender)
end
function SMH.SendACK(sender, reqId)
local send = string.format("%01s%01s%02d%06s", CSMHMsgPrefix, "S", pck.ACK, reqId)
sender:SendAddonMessage(send, "", 7, sender)
local header = string.format("%01s%01s%02d", delim[1], "S", pck.REQ)
local data = string.format("%02d%03d%06s%0"..tostring(#addon).."s", functionId, linkCount, reqId, addon)
sender:SendAddonMessage(header, data, 7, sender)
debugOut("REQ", "Tx", "Sent REQ with ID "..reqId..", sending DAT..")
end
function SMH.SendDAT(sender, reqId)
-- Build data message header
local send = string.format("%01s%01s%02d%06s", CSMHMsgPrefix, "S", pck.DAT, reqId)
local header = string.format("%01s%01s%02d", delim[1], "S", pck.DAT)
-- iterate all items in the message data cache and send
-- functions can also be trigger functions without any data, only send header and no payload
if(#datacache[sender:GetGUIDLow()][reqId]["data"] == 0) then
sender:SendAddonMessage(send, "", 7, sender)
sender:SendAddonMessage(header, reqId, 7, sender)
else
for _, v in pairs (datacache[sender:GetGUIDLow()][reqId]["data"]) do
local payload = send..v
sender:SendAddonMessage(payload, "", 7, sender)
local payload = reqId..v
sender:SendAddonMessage(header, payload, 7, sender)
end
end
debugOut("All data sent, cleaning up cache.")
-- all items have been sent, cache can be purged
datacache[sender:GetGUIDLow()][reqId] = nil
end
function SMH.SendNAK(sender, reqId)
local send = string.format("%01s%01s%02d%06s", CSMHMsgPrefix, "S", pck.NAK, reqId)
sender:SendAddonMessage(send, "", 7, sender)
debugOut("DAT", "Tx", "Sent all DAT for ID "..reqId..", cache cleared, closing.")
end
-- Tx END
@ -344,8 +302,8 @@ end
function Player:SendServerResponse(prefix, functionId, ...)
local reqId = GenerateReqId()
local varTable = ProcessVariables(self, reqId, ...)
SMH.SendREQ(self, functionId, varTable.count, reqId, prefix)
SMH.SendDAT(self, reqId)
end
-- API END