Is this a good approach to invoking with remote event?

hi, I want complete control on what client sends to server while not yielding any threads forever. I created a fake remote function and just wondering if this is alright (maybe there is a better way) and if I missed anything. (I don’t know which category would have been better.)

basically client fires back to server to the open data index after server requests data

replicatedStorage.ClientRemotes.BetterInvoke.OnServerEvent:Connect(function(client: Player,time:string,response:any)
    if type(time) ~= "string" or betterFunctions.clientResponses[client][time] ~= "Open" then
        client:Kick("Attempted to manipulate data.")
        return
    end
    betterFunctions.clientResponses[client][time] = response --is this alright?
end)

the fake remote function handler:

functions.clientResponses = {}

functions.invokeClient = function(client:Player?,...)
	local RemoteFunction: RemoteEvent = ReplicatedStorage.ClientRemotes.BetterInvoke --My goal is to use BetterInvoke remote event as remote function. So I don't mess with yielding.
	if client and client:IsA("Player") then --switch to early return for less indent
		local start = os.clock()
		local playerData

		local args = {...} --I can't send it into pcall otherwise
		local _, Error = pcall(function()
            local maxIndex = 20
			local totalIndex = 0
			local function deepCopy(possibleTable:any?) --I don't want to change the data, so I create a copy. I forgot why tho.
				local clonedData
				if type(possibleTable) == "table" then
					clonedData = {}
					for k, v in pairs(possibleTable) do
						totalIndex += 1
						if totalIndex > maxIndex then --don't want clients to send big tables.
							if client then
								client:Kick()
							end
							return
						end
						if type(v) == "table" then
							v = deepCopy(v)
						end
						clonedData[k] = v
					end
				else
					clonedData = possibleTable
				end
				return clonedData
			end

			functions.clientResponses[client][tostring(start)] = "Open"

			RemoteFunction:FireClient(client,tostring(start),table.unpack(args))

			playerData = deepCopy(functions.clientResponses[client][tostring(start)])
			if playerData == "Open" then
				repeat
					print("waiting") --for test (usually prints 3 times which is same as remote functions)
					task.wait()
                    if not functions.clientResponses[client] then
                        return
                    end
					playerData = deepCopy(functions.clientResponses[client][tostring(start)])
				until playerData ~= "Open" or os.clock() - start > 2
			end
		end)

		if functions.clientResponses[client] then
			functions.clientResponses[client][tostring(start)] = nil
		end

		if Error then --mainly because player might just left the game
			warn(Error)
			return
		elseif playerData == "Open" then
			playerData = nil
			warn("Failed to receive information") --for test
		end

		return playerData --might exist, might not exist. Will be checked if it fits the required data type and if there is manipulations.
	end
end

An example of getting data from a different module;

func.latestMousePositions = {}

func.getMousePos = function(player:Player)
	if not player:IsA("Player") then
		return
	end
	local mousePos:Vector3? = betterFunctions.invokeClient(player,"Give Mouse Position")
	if mousePos and (typeof(mousePos) ~= "Vector3" or mousePos.X ~= mousePos.X or mousePos.Y ~= mousePos.Y or mousePos.Z ~= mousePos.Z) then
		player:Kick()
		return
	elseif not mousePos then
		if func.latestMousePositions[player] then
			return func.latestMousePositions[player]
		else
			return
		end
	else
		func.latestMousePositions[player] = mousePos
	end
	return mousePos --which will run a function if it exists - stop it if it does not.
end

Also yes, on player added - removing the player table instance gets added - removed.

Use guard clauses and newlines because this looks, not to be mean, unreadable

Instead of making all of this take inspiration from one of my old modules
It shouldn’t be used in production though, but you can see how it works and adapt it to your codebase that way. :+1:

Your approach for invoking remote events while maintaining control and avoiding thread yielding is valid. However, a few things to consider:

  1. Data Size: Be cautious with the data size limitation, as handling large data client-side can impact performance.
  2. Deep Copy: Your deep copy function is necessary for data integrity, but it can be optimized for better performance.
  3. Client Kicking: Kicking clients for data manipulation is a good security measure, but provide clear error messages.
  4. Polling: Polling for data isn’t very efficient. Consider asynchronous handling with promises or callbacks.
  5. Cleanup: Ensure your data cleanup logic is robust and doesn’t leave dangling data.
  6. Error Handling: Enhance error messages and server-side logging for debugging.
  7. Documentation: Add comments and documentation for clarity.

Your approach can work, but it’s complex and almost unreadable. Evaluate if the trade-offs align with your game’s requirements and explore other communication patterns if needed.

Sorry for late response,

I added kick message, tried to make it more clear. Also deepcopy is just 1 line, i don’t think i will use tables a lot (or big tables), but still client can manipulate so I set maxIndex to something like 5 (I can improve it later). Also I can make it more robust with setting default values like default timeLimit.

So what I understand is I just need to make it more clear to read? I mean I don’t see any way to stop polling, bindable event comes to my mind but they don’t have Event:Wait(), would be glad if you can tell me how can I stop it.

Also @HexadecimalLiker I checked the repo, i mean I was already invoking in task.spawn() like your invoke function but the client can still yield that thread forever while you can continue. That was the reason for this remote event invoking. Am I wrong that they can yield that thread forever?

Here is uh more clear(?) code;

functions.clientResponses = {}

functions.invokeClient = function(client:Player?,...)
	if not client or not client:IsA("Player") then
		return
	end
	local RemoteFunction: RemoteEvent = ReplicatedStorage.ClientRemotes.BetterInvoke --My goal is to use BetterInvoke remote event as remote function. So I don't mess with yielding.

	local start = os.clock()
	local playerData

	--can't send args into pcall, so I put args into a table and move that into pcall.
	local args = {...}; local _, Error = pcall(function()
		local maxIndex = 5
		local currentIndex = 0

		--I don't want to change the data, so I create a copy. Also for data to exist after data point deletion.

		local function deepCopy(possibleTable:any?)
			local clonedData
			if type(possibleTable) == "table" then --there is no functions for this yet so don't worry about it, see the else statement.
				clonedData = {}
				for key, value in pairs(possibleTable) do
					currentIndex += 1

					if currentIndex > maxIndex then --received data is bigger than max data size.
						if client then
							client:Kick("Tried sending data that exceeds the limits.")
						end
						return
					end

					if type(value) == "table" then --found a table index, will deep copy it aswell.
						value = deepCopy(value)
					end
					clonedData[key] = value
				end
			else
				clonedData = possibleTable
			end
			return clonedData
		end

		--init

		functions.clientResponses[client][tostring(start)] = "Open" --opens a data point

		RemoteFunction:FireClient(client,tostring(start),table.unpack(args)) --orders player to send back data

		--demands data in given time limit
		local timeLimit = 2
		repeat
			--print("waiting") --for test
			task.wait()
			if not functions.clientResponses[client] then
				return
			end
			playerData = deepCopy(functions.clientResponses[client][tostring(start)])
		until playerData ~= "Open" or os.clock() - start > timeLimit

	end)

	--data received or time limit passed, does some checks before returning data.

	if functions.clientResponses[client] then --client might left the game, which means data point erased already.
		functions.clientResponses[client][tostring(start)] = nil --close the open data point
	end

	if Error then --mainly because player might just left the game
		warn(Error)
		return
	elseif playerData == "Open" then --player failed to send data back, might be connection issue etc.
		playerData = nil --we don't want to send string values to server when client fails to send data
		--warn("Failed to receive information") --for test
	end

	return playerData --might exist, might not exist. Will be checked if it fits the required data type and if there is manipulations.
end