How to detect if function that was added to a table is a : or a . function using __newindex?

I tried to make my own networking module for my game, and it’s going well, automated remotefunction creation is working, but the problem is that the remotes are only created after the function is called, so sometimes it can get blocked due to locking remote creation, so how can i detect what type of function it is, using __newindex, so i can call that function the moment it’s created?

If you want to check the function type, this might be a bit tedious, but you can try looking either for the self parameter in the function’s parameter list or checking for colon notation.

local meta = {}
function meta:__newindex(key: any, value: (...any) -> (..any))
    --get the function code
    local source = debug.info(value, "s")

    --shorten it down to avoid nested functions
    source = string.sub(source, --[[however much you want to shorten it]])

    --search for the colon notation in its declaration
    local matchString = string.match(source, "function[%w_ ]+:[ %w_]+%([%w%a%d_, :]+%)")

    --search for the dot in the declaration
    local match2 = string.match(source, "function[%w_ ]+%.[ %w_]+%(self")

    if matchString or match2 then
        --do stuff
    end
end

This will only work for functions declared that way, and it’s really tedious and I wouldn’t recommend it. If you explain more about your issue, I’ll try to think of a better solution. If you need me to explain those string patterns, also just ask.

You can try also by setting a metatable on your Network module, the __newindex function identifies whether the added function is meant for the client or server and then instantiates the appropriate RemoteFunction

This can also be an example:

local Network = {}
setmetatable(Network, {
    __newindex = function(table, key, value)
        if type(value) == "function" then
            local remote = Instance.new("RemoteFunction")
            remote.Name = key
            remote.Parent = game:GetService("ReplicatedStorage")
            remote.OnServerInvoke = value
            rawset(table, key, function(...) return remote:InvokeClient(...) end)
        else
            rawset(table, key, value)
        end
    end
})

Where it can be used like this:

Server Script

-- ServerScript.lua
local Network = require(game.ReplicatedStorage.NetworkModule)

Network.SendMessageToClient = function(player, message)
    if not player then
        error("Player not provided!")
    end
    print("Received message from client:", message)
    return "Server received your message."
end

Local Script

-- LocalScript.lua
local Network = require(game.ReplicatedStorage.NetworkModule)

local success, response = pcall(function()
    return Network.SendMessageToClient(game.Players.LocalPlayer, "Hello Server!")
end)

if success then
    print(response) -- Outputs: "Server received your message."
else
    warn("Failed to send message:", response)
end

Basis: The __newindex Metamethod

while i was making the module, i was using the Knit documentation as a basis on how i should do it (as its one of the only network modules i know), so it got some pretty similar features, like, client functions would be created like this

function test.client:Get()
	return self.server:Get()
end

the remotes are automatically created when the .server is accessed, but as it’s stored inside a function, the .server value will only be acessed after being called, which can be after the remote creation window has ended, so i wanted my network module to instantly call any client function from the server side instantly after its creation, so that it creates the remotes instantly

btw these are how the metatables are structured so far

setmetatable(Client, {
		-- TODO: make proper function initialization
		__newindex = function(t, key, val)
			rawset(t, key, val)
		end,
	})
	setmetatable(Client.server, {
		__newindex = function() warn("cannot explicitly create a server value!") end,
		__index = function(_, key)
			warn("accessing "..key)
			
			local parentType = typeof(Service[key])
			local prefix = `{string.sub(string.upper(tostring(parentType)), 1, 3)}_`
			
			local remote : RemoteFunction = clientFetchFolder:FindFirstChild(prefix..key)
			if not remote then
				remote = Instance.new("RemoteFunction")
				remote.Name = prefix..key
				remote.Parent = clientFetchFolder
			end
			return runService:IsClient() and remote:InvokeServer() or Service[key]
		end,
	})

It would be like this

-- Tables to hold client and server functions
    self.client = {}
    self.server = {}
    
    -- Metatable for client to intercept function definitions
    setmetatable(self.client, {
        __newindex = function(tbl, key, value)
            if type(value) == "function" then
                -- Determine if it's a RemoteFunction or RemoteEvent based on naming convention or additional parameters
                -- For simplicity, let's assume functions ending with "Event" are RemoteEvents
                local isFunction = not key:find("Event$")
                
                -- Create Remote
                local remote = createRemote(key, isFunction)
                
                -- Define the client-side function
                tbl[key] = function(...)
                    if isFunction then
                        return remote:InvokeServer(...)
                    else
                        remote:FireServer(...)
                    end
                end
                
                -- Define the server-side handler
                if isFunction then
                    self.server[key] = function(player, ...)
                        return value(player, ...)
                    end
                    remote.OnServerInvoke = self.server[key]
                else
                    self.server[key] = function(player, ...)
                        value(player, ...)
                    end
                    remote.OnServerEvent:Connect(self.server[key])
                end
            else
                error("Only functions can be assigned to client.")
            end
        end,
    })
    
    -- Prevent explicit creation of server values
    setmetatable(self.server, {
        __newindex = function(tbl, key, value)
            error("Cannot explicitly create a server value! Define functions on 'client' instead.")
        end,
        __index = function(tbl, key)
            -- Access to server functions should be direct
            return self.server[key]
        end,
    })
    
    return self

Then you can use the module or integrate it as is
Server

-- ServerManager.lua
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local NetworkModule = require(ReplicatedStorage:WaitForChild("NetworkModule"))

local network = NetworkModule.new()

-- Define a RemoteFunction named "GetData"
function network.client:GetData(player, dataId)
    print(player.Name .. " requested data with ID:", dataId)
    -- Fetch and return data
    local data = "Data for ID: " .. tostring(dataId)
    return data
end

-- Define a RemoteEvent named "PlayerJoinedEvent"
function network.client:PlayerJoinedEvent(player, joinTime)
    print(player.Name .. " joined at:", joinTime)
    -- Handle player join event
end

print("Network remotes have been set up.")

Client

-- NetworkClient.lua
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local NetworkModule = require(ReplicatedStorage:WaitForChild("NetworkModule"))

local network = NetworkModule.new()

-- Function to request data from the server
local function requestData(dataId)
    local data = network.client:GetData(dataId)
    print("Received from server:", data)
end

-- Function to notify the server when the player joins
local function notifyPlayerJoined()
    local joinTime = os.time()
    network.client:PlayerJoinedEvent(joinTime)
    print("Notified server of player join at:", joinTime)
end

-- Example usage
requestData(42)
notifyPlayerJoined()

Full Module Example

-- NetworkModule.lua
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")

local NetworkModule = {}
NetworkModule.__index = NetworkModule

-- Create folders to store remotes
local RemoteFolder = Instance.new("Folder")
RemoteFolder.Name = "NetworkRemotes"
RemoteFolder.Parent = ReplicatedStorage

-- Utility function to create RemoteFunctions or RemoteEvents
local function createRemote(name, isFunction)
    if isFunction then
        local remote = Instance.new("RemoteFunction")
        remote.Name = name
        remote.Parent = RemoteFolder
        return remote
    else
        local remote = Instance.new("RemoteEvent")
        remote.Name = name
        remote.Parent = RemoteFolder
        return remote
    end
end

function NetworkModule.new()
    local self = setmetatable({}, NetworkModule)
    
    -- Tables to hold client and server functions
    self.client = {}
    self.server = {}
    
    -- Metatable for client to intercept function definitions
    setmetatable(self.client, {
        __newindex = function(tbl, key, value)
            if type(value) == "function" then
                -- Determine if it's a RemoteFunction or RemoteEvent based on naming convention or additional parameters
                -- For simplicity, let's assume functions ending with "Event" are RemoteEvents
                local isFunction = not key:find("Event$")
                
                -- Create Remote
                local remote = createRemote(key, isFunction)
                
                -- Define the client-side function
                tbl[key] = function(...)
                    if isFunction then
                        return remote:InvokeServer(...)
                    else
                        remote:FireServer(...)
                    end
                end
                
                -- Define the server-side handler
                if isFunction then
                    self.server[key] = function(player, ...)
                        return value(player, ...)
                    end
                    remote.OnServerInvoke = self.server[key]
                else
                    self.server[key] = function(player, ...)
                        value(player, ...)
                    end
                    remote.OnServerEvent:Connect(self.server[key])
                end
            else
                error("Only functions can be assigned to client.")
            end
        end,
    })
    
    -- Prevent explicit creation of server values
    setmetatable(self.server, {
        __newindex = function(tbl, key, value)
            error("Cannot explicitly create a server value! Define functions on 'client' instead.")
        end,
        __index = function(tbl, key)
            -- Access to server functions should be direct
            return self.server[key]
        end,
    })
    
    return self
end

return NetworkModule

i want to use it to detect if i should pass the table or not, aka mimic the colon behavior, like this

setmetatable(Service.client, {
		__newindex = function(t, key, val)
			if typeof(val) ~= "function" then warn("only functions can be created on the client table!") return end
			rawset(t, key, val)
			
			local remote = Instance.new("RemoteFunction")
			remote.Name = key
			remote.Parent = fetchStorage
			
			local isColon = true --placeholder
			remote.OnServerInvoke = function(...)
				if isColon then
					return val(t, ...)
				else
					return val(...)
				end
			end
		end,
	})