Invoking the server for data from the client

SNIPPET TITLE: Invoking the server for data from the client, or informing the server for an action and waiting for a response as to whether that invocation was acceptable.

WHAT DOES IT DO?:
Do you, the client, want access to data that the server holds without using networked properties? Or maybe you want to check if it's acceptable for the client to do something? If so, this solves that.

BASE SNIPPET
Broadcast module:

-- error codes
local ERRORCODES = {
	[1] = 'No response from server after packet [%s]';
	[2] = 'Unresolved broadcast error after packet [%s]';
}

-- resolver
local resolver = { }
function resolver.new()
	return setmetatable({ }, {
		__index = resolver;
	})
end

function resolver:reconcile(data)
	if data.res and not data.res.error then
		if self._handler then
			self._handler(data)
		end
	else
		if self._catch then
			if data.res.error then
				data.res.error = ERRORCODES[data.res.error]:format(data.id)
			end
			self._catch(data)
		else
			warn(ERRORCODES[2]:format(data.id))
		end
	end
end
function resolver:andThen(foo)
	self._handler = foo
	
	return self
end
function resolver:catch(foo)
	self._catch = foo
	
	return self
end

-- broadcaster
local broadcast = { }
function broadcast.new()
	local this = { }
	this.invocations = { }
	
	-- private
	local random = RandomStream.new()

	local function send(event, data)
		Task.Spawn(function ()
			Events.BroadcastToServer(event, data)
		end)
	end
	
	local function invoke(event, data)
		data.id = this:getUUID()
		
		local resolve = resolver.new()		
		this.invocations[data.id] = Events.Connect(data.id, function (data)
			local connection = this.invocations[data.id]
			this.invocations[data.id] = nil

			coroutine.wrap(function (...)
				resolve:reconcile(...)
			end)(data)		
			
			return connection:Disconnect()
		end)
				
		send(event, data)
		
		return resolve
	end
	
	local function construct()
		
		return this
	end
	
	-- public
	function this:getUUID()
		-- modified from: https://gist.github.com/jrus/3197011
		random:Mutate()
		
		local template = 'xxxxxxxx-xxxx-4xxx-yxxx'
		return string.gsub(template, '[xy]', function (c)
			local v = (c == 'x') and random:GetInteger(0, 0xf) or random:GetInteger(8, 0xb)
			return string.format('%x', v)
		end)
	end
	
	function this:invokeServer(event, data)
		return invoke(event, data)
	end
	
	function this:fireServer(event, data)
		return send(event, data)
	end
	
	return construct()
end

return broadcast

Examples:
Server code example:

local handlers = {
	-- you should handle these in another module, but for easy reading...
	specialEvent = function (player, data)
		if not somethingUnacceptable then
			return 'specialEvent!'
		end
	end;
}

Events.ConnectForPlayer('registerAction', function (player, data)
	local res
	if data.class then
		if handlers[data.class] then
			res = handlers[data.class](data)
		end
	end
	
	return Events.BroadcastToPlayer(player, data.id, {
		id   = data.id;
		res  = res or {
			error = 1
		}
	})
end)

Client code example:

local broadcast = require('AssetIdForBroadcastModule').new()
local player    = Game:GetLocalPlayer()

-- globals
local KEYS = {
	LMB   = 'ability_primary';
	SPACE = 'ability_extra_17';
}

-- main
player.bindingPressedEvent:Connect(function (player, key)
	if key == KEYS.LMB then
		broadcast:invokeServer(
			'registerAction',
			{
				class = 'specialEvent';
			}
		):andThen(function (data)
			print('From server:', data.res)
			-- do something relating to the 'specialEvent'
			
		end):catch(function (data)
			print(data.res.error)
		end)
	end
end)

ADDITIONAL INFO:
Admittedly, some of this is purely for syntactical sugar (i.e. :andThen and :catch probably didn't need to work like that but it looked nicer that way - waiting on someone to port a promise library).

I haven't handled rate limits and/or ability to queue in this but it could easily be added / I'll update it at some point.

1 Like