Echo - Player-data oriented, network efficient replication tool

About

Echo is a lightweight networking library which initial purpose of is to replicate player related data.
You could say we already got Replica by Loleris which I loved to use and partially inspired of, except it lacked a few features which is why I made this library.


Features

  • Data compression: Uses Packet library by Suphi which minimizes network bandwidth usage.
  • Replication .Events: Ability to detect replications on both server and client.
  • Caching: Received data gets stored which you can later retrieve making data manipulations 100x easier.
  • Requests batching: You should expect 0% data loss when it comes to the point, Echo halts replication requests on both client and server and fires in given order afterwards.

Documentation

CONSTRUCTOR

--Returns previously created replicator, or creates a new one.
New(Player : Player, InitialData : {}?) : Replicator?
--Returns a previously created replicator if exists.
FromPlayer(Player : Player) : Replicator?

EVENTS

SERVER
.ReplicatorAdded :: Signal.Signal <Player, Replicator>
.ReplicatorRemoved :: Signal.Signal <Player>
SERVER ECHO
--Returns the listener signal for :SendAll.
.SentAll:: Signal.Signal <any>
CLIENT
.Activated:: Signal.Signal <boolean>

METHODS

SERVER
--Replicates value of the given path. Can pass value manually as 2nd argument, otherwise will get it from the data table.
:Send(Path : {string | number}, Value : any?) : ()
--Returns a previously created listener signal for the path's value, or creates a new one.
:OnSent(Path : {string | number}) : Signal.Signal <any>
--Replicates the data table.
:SendAll() : ()
--Disconnects/clears all cached signals. Destroys the replicator entirely.
:Destroy() : ()
CLIENT
--Launches replications. If called first time will cache up the initial data.
:Start() : boolean
--Halts replications.
:Stop() : boolean
--Returns a boolean, true if client is listening for replications.
:IsActive() : boolean
--Returns the value of the path.
:Get(Path : {string | number}) : any
--Returns a previously created listener signal for the path's value, or creates a new one.
:Receive(Path : Path) : Signal.Signal <NewValue, OldValue>
--Disconnects and clears all receive signals.
:DisconnectAll() : ()

Basic Example

Server

local Echo = require(game.ServerScriptService.EchoServer)

game.Players.PlayerAdded:Connect(function(Player)
	
	local Data = {
		Coins = 300,
		Wins = 10,
		Food = {
			Apple = 3,
			Coconut = 1,
			Banana = 6
		}
	}
	
	local PlayerEcho = Echo.New(Player, Data)
	
	while true do
		Data.Food.Apple += 1
		PlayerEcho:Send({"Food", "Apple"}) -- Optionally pass any value you wish
		task.wait(1)
	end
end)

Client

local Echo = require(game.ReplicatedStorage.EchoClient)

Echo:Start()

Echo:Receive({'Food', 'Apple'}):Connect(function(NewValue : any, OldValue : any)
	print("NewValue: ", NewValue, "OldValue: ", OldValue)
end)

:package: Get on Creator Store
Looking for your feedback :star:

11 Likes

1.1

  • Fixed various errors if player had left mid initial setup.
  • Implemented soft clean up after player removed, would cause a critical bug if player’s echo wasn’t manually destroyed by the developer.

1.15

  • Made activation more reliable on server side.
  • Added new event .Activated on client side, fires upon using :Activate() or :Deactivate() and returns a boolean determining echo’s current “Active” status.

The event is useful in cases when the client receives initial data and developer needs a key from it but doesn’t know if the activation happened and/or was successful.

Example:

Client Script 1:

local Echo = require(game.ReplicatedStorage.EchoClient)

Echo:Start()

Client Script 2: :cross_mark:

local Coins = Echo:Get({'Coins'})
print(Coins) --> nil

Coins = Echo:Receive({'Coins'}):Wait() --> Will yield infinitely unless server manually replicates the value after echo activated

Client Script 2: :white_check_mark:

if not Echo:IsActive() then Echo.Activated:Wait() end

local Coins = Echo:Get({'Coins'}) or Echo:Receive({'Coins'}):Wait()

print(Coins) --> 500
1 Like

Sorry if it’s an obvious question, but is it possible to replicate the entire data table instead of calling :Send on every value? Bulk data modifications would seem rather tedious if that’s the only way to do it.

1 Like

Pretty cool! Might test it out later tonight.

Calling :Send separately for every value is supposed to minimize network usage because you don’t usually need to replicate the whole data table every time but if that’s your case then i’ll look into adding a method for such operation, probably an event on client aswell to detect the replication

You could try comparing it to the cached table and only updating the changes, something like

-- Pseudo
local cache = {}
local function Sync(Data)
	for i, v in cache do
		if Data[i] == v then continue end
		:Send(i, Data[i])
	end
end

Of course you would have to add a check for tables, optimizations and all that stuff

Yeah, that’s what i was gonna do, although idk if i should fire every key that has been changed in their corresponding :Receive signal or make a whole new receive signal for the new bulk replication method
What would be better?

1.2

  • Improved activation reliability yet again, now upon failing activation it will repeatedly try again until success (Infinite yield)

  • Replaced :Activate() and :Deactive() with :Start() and :Stop(). Although the older methods will still be valid and act as aliases, It is not recommended to use them anymore

  • Renamed :Destroy() client method to :DisconnectAll() as it fits the functionality more

  • Added :SendAll() server method (suggestion by @artembon). Upon receiving the data table on client and detecting any changes they get separately processed just like any :Send() call

  • Added .SentAll signal on server, pretty self explanatory, returns a deep clone of a replicated data table

  • Manually cleaning up replicators on server side is not needed anymore

  • Fixed edge cases when delayed requests would fire in incorrect order

How does this compare to blink ide

I guess you mean the network usage of Packet and Blink and how they perform compared to each other.

In terms of performance blink only has advantage in booleans, apart from that Packet and Blink do the same job as they (I assume) both use buffers. Packet is just more flexible and easier to use.

I dont know if Blink has that but with Packet you have the .any type which lets you to replicate any supported type but has overhead of 1 byte. So It’s obviously better to set up your scheme if you want to squeeze out even less network usage.