@rbxts/replion 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # Replion
2
+
3
+ ## Install
4
+ Install with [wally](https://wally.run/):\
5
+ `Replion = "shouxtech/replion@1.0.0"`
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@rbxts/replion",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "src/init.lua",
6
+ "scripts": {},
7
+ "keywords": [],
8
+ "author": "CriShoux",
9
+ "license": "ISC",
10
+ "type": "commonjs",
11
+ "types": "src/index.d.ts",
12
+ "files": [
13
+ "src"
14
+ ],
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "devDependencies": {
19
+ "@rbxts/compiler-types": "^3.0.0-types.0",
20
+ "@rbxts/types": "^1.0.896",
21
+ "roblox-ts": "^3.0.0",
22
+ "typescript": "^5.9.3"
23
+ },
24
+ "dependencies": {
25
+ "@rbxts/sift": "^0.0.11",
26
+ "@rbxts/sleitnick-signal": "^1.0.8"
27
+ }
28
+ }
package/src/Client.lua ADDED
@@ -0,0 +1,146 @@
1
+ --!strict
2
+ local Shared = require(script.Parent.Shared);
3
+ local Signal = require(dependencies:FindFirstChild('Signal') or dependencies['sleitnick-signal']);
4
+
5
+ type Observer = (newValue: any, oldValue: any) -> ();
6
+
7
+ local remote = script.Parent:WaitForChild(Shared.REMOTE_NAME) :: RemoteEvent;
8
+
9
+ local function deepMergeAndApply(target: any, updates: any)
10
+ for key, value in updates do
11
+ if value == Shared.NIL_SENTINEL then
12
+ target[key] = nil;
13
+ elseif typeof(value) == 'table' and typeof(target[key]) == 'table' then
14
+ deepMergeAndApply(target[key], value);
15
+ else
16
+ target[key] = value;
17
+ end;
18
+ end;
19
+ end;
20
+
21
+ local Client = {};
22
+ Client.__index = Client;
23
+
24
+ type Replion = typeof(setmetatable({}, Client));
25
+
26
+ Client._activeReplions = {} :: { [string]: Replion };
27
+ Client._replionInitializedSignals = {} :: { [string]: Signal.Signal<Replion> };
28
+
29
+ function Client.waitForReplion(channel: string)
30
+ local existing = Client._activeReplions[channel];
31
+ if existing then return existing; end;
32
+
33
+ local initSignal = Client._replionInitializedSignals[channel];
34
+ if initSignal then return initSignal:Wait(); end;
35
+
36
+ initSignal = Signal.new();
37
+ Client._replionInitializedSignals[channel] = initSignal;
38
+
39
+ remote:FireServer('I', channel);
40
+
41
+ local replion = initSignal:Wait();
42
+
43
+ Client._replionInitializedSignals[channel] = nil;
44
+
45
+ return replion;
46
+ end;
47
+
48
+ function Client.new(channel: string, data: Shared.GenericDataTable)
49
+ local self = setmetatable({}, Client);
50
+
51
+ self.channel = channel;
52
+ self.data = data;
53
+ self._signals = {} :: { [string]: any };
54
+ self._allSignal = Signal.new();
55
+
56
+ return self;
57
+ end;
58
+
59
+ function Client:get(key: (string | {string})?)
60
+ if not key then return self.data; end;
61
+ if typeof(key) == 'table' then
62
+ return Shared.getNestedValue(self.data, key);
63
+ end;
64
+
65
+ return self.data[key];
66
+ end;
67
+
68
+ function Client:observe(key: string?, callback: Observer)
69
+ if not key then
70
+ task.spawn(callback, self.data, nil);
71
+
72
+ return self._allSignal:Connect(callback);
73
+ end;
74
+
75
+ if not self._signals[key] then
76
+ self._signals[key] = Signal.new();
77
+ end;
78
+
79
+ task.spawn(callback, self.data[key], nil);
80
+
81
+ return self._signals[key]:Connect(callback);
82
+ end;
83
+
84
+ function Client:_applyUpdates(updates: Shared.GenericDataTable)
85
+ local oldData = table.clone(self.data); -- Note: Shallow copy instead of deep copy.
86
+
87
+ deepMergeAndApply(self.data, updates);
88
+
89
+ local anyChanged = false;
90
+
91
+ for key, newValue in updates do
92
+ local oldValue = oldData[key];
93
+
94
+ -- If it's a table, we assume it changed if it's in the update list.
95
+ if typeof(newValue) == 'table' then
96
+ anyChanged = true
97
+ local signal = self._signals[key];
98
+ if signal then
99
+ signal:Fire(self.data[key], self.data[key]); -- Note: Sends improper 'old' argument due to shallow copy optimization.
100
+ end;
101
+ elseif self.data[key] ~= oldValue then
102
+ anyChanged = true;
103
+ local signal = self._signals[key];
104
+ if signal then
105
+ signal:Fire(self.data[key], oldValue);
106
+ end;
107
+ end;
108
+ end;
109
+
110
+ if anyChanged then
111
+ self._allSignal:Fire(self.data, updates);
112
+ end;
113
+ end;
114
+
115
+ function Client:destroy()
116
+ self._allSignal:Destroy();
117
+ for _, signal in self._signals do
118
+ signal:Destroy();
119
+ end;
120
+ self._signals = {};
121
+ end;
122
+
123
+ remote.OnClientEvent:Connect(function(type, channel, payload)
124
+ if type == 'I' then
125
+ local newReplion = Client.new(channel, payload);
126
+ Client._activeReplions[channel] = newReplion;
127
+
128
+ local initializedSignal = Client._replionInitializedSignals[channel];
129
+ if not initializedSignal then return; end;
130
+
131
+ initializedSignal:Fire(newReplion);
132
+ elseif type == 'U' then
133
+ local replion = Client._activeReplions[channel];
134
+ if not replion then return; end;
135
+
136
+ replion:_applyUpdates(payload);
137
+ elseif type == 'D' then
138
+ local replion = Client._activeReplions[channel]
139
+ if not replion then return; end;
140
+
141
+ replion:destroy();
142
+ Client._activeReplions[channel] = nil;
143
+ end;
144
+ end);
145
+
146
+ return Client;
package/src/Server.lua ADDED
@@ -0,0 +1,195 @@
1
+ --!strict
2
+ local Players = game:GetService('Players');
3
+
4
+ local dependencies = script.Parent.Parent;
5
+ local Shared = require(script.Parent.Shared);
6
+ local Sift = require(dependencies:FindFirstChild('Sift') or dependencies.sift.out);
7
+
8
+ type ServerConfig = {
9
+ channel: string;
10
+ replicateTo: Player?;
11
+ data: Shared.GenericDataTable;
12
+ };
13
+
14
+ local remote = script.Parent:FindFirstChild(Shared.REMOTE_NAME) :: RemoteEvent;
15
+ if not remote then
16
+ remote = Instance.new('RemoteEvent');
17
+ remote.Name = Shared.REMOTE_NAME;
18
+ remote.Parent = script.Parent;
19
+ end;
20
+
21
+ local function setNestedValue(root: any, path: {string}, value: any)
22
+ local current = root;
23
+ for i = 1, #path - 1 do
24
+ local key = path[i];
25
+ if typeof(current[key]) ~= 'table' then
26
+ current[key] = {};
27
+ end;
28
+ current = current[key];
29
+ end;
30
+
31
+ local lastKey = path[#path];
32
+ local oldValue = current[lastKey];
33
+ current[lastKey] = value;
34
+ return oldValue;
35
+ end;
36
+
37
+ local function queueNestedUpdate(queue: any, path: {string}, value: any)
38
+ local current = queue;
39
+ for i = 1, #path - 1 do
40
+ local key = path[i];
41
+ if typeof(current[key]) ~= 'table' then
42
+ current[key] = {};
43
+ end;
44
+ current = current[key];
45
+ end;
46
+
47
+ current[path[#path]] = if value == nil then Shared.NIL_SENTINEL else value;
48
+ end;
49
+
50
+ local Server = {};
51
+ Server.__index = Server;
52
+
53
+ type Replion = typeof(setmetatable({}, Server));
54
+
55
+ Server._activeReplions = {} :: { [Player]: { [string]: Replion } };
56
+ Server._globalReplions = {} :: { [string]: Replion };
57
+
58
+ -- This method is private because this it isn't able to give correct types for the found Replion, and that's unideal for public use.
59
+ function Server._getPlayerReplion(player: Player, channel: string)
60
+ local playerReplions = Server._activeReplions[player];
61
+ if not playerReplions then return nil; end;
62
+
63
+ return playerReplions[channel];
64
+ end;
65
+
66
+ -- This method is private because this it isn't able to give correct types for the found Replion, and that's unideal for public use.
67
+ function Server._getGlobalReplion(channel: string)
68
+ return Server._globalReplions[channel];
69
+ end;
70
+
71
+ function Server.new(config: ServerConfig)
72
+ local self = setmetatable({}, Server);
73
+
74
+ self.player = config.replicateTo;
75
+ self.channel = config.channel;
76
+ self.data = table.clone(config.data);
77
+
78
+ self._queuedUpdates = {} :: Shared.GenericDataTable;
79
+ self._isQueued = false;
80
+ self._destroyed = false;
81
+
82
+ if self.player then
83
+ local playerReplions = Server._activeReplions[self.player];
84
+ if not playerReplions then
85
+ playerReplions = {};
86
+ Server._activeReplions[self.player] = playerReplions;
87
+ end;
88
+ playerReplions[self.channel] = self;
89
+ else
90
+ Server._globalReplions[self.channel] = self;
91
+ end;
92
+
93
+ return self;
94
+ end;
95
+
96
+ function Server:set(key: string | {string}, value: any)
97
+ if self._destroyed then return; end;
98
+
99
+ local path = if typeof(key) == 'table' then key else {key};
100
+ local oldValue = Shared.getNestedValue(self.data, path);
101
+
102
+ local newValue = if typeof(value) == 'function' then value(oldValue) else value;
103
+
104
+ if oldValue == newValue then return; end;
105
+
106
+ setNestedValue(self.data, path, newValue);
107
+ queueNestedUpdate(self._queuedUpdates, path, newValue);
108
+ self:_scheduleUpdatesFlush();
109
+ end;
110
+
111
+ function Server:get(key: (string | {string})?)
112
+ if not key then return self.data; end;
113
+ if typeof(key) == 'table' then
114
+ return Shared.getNestedValue(self.data, key);
115
+ end;
116
+
117
+ return self.data[key];
118
+ end;
119
+
120
+ function Server:destroy()
121
+ self._destroyed = true;
122
+ self.data = {};
123
+ self._queuedUpdates = {};
124
+
125
+ if self.player then
126
+ local playerReplions = self._activeReplions[self.player];
127
+ if not playerReplions then return; end;
128
+
129
+ playerReplions[self.channel] = nil;
130
+ remote:FireClient(self.player, 'D', self.channel);
131
+ else
132
+ self._globalReplions[self.channel] = nil;
133
+ remote:FireAllClients('D', self.channel);
134
+ end;
135
+ end;
136
+
137
+ function Server:_scheduleUpdatesFlush()
138
+ if self._isQueued then return; end;
139
+ self._isQueued = true;
140
+
141
+ task.defer(function()
142
+ if self._destroyed then return; end;
143
+ self:_flushUpdates();
144
+ end);
145
+ end;
146
+
147
+ function Server:_flushUpdates()
148
+ self._isQueued = false;
149
+
150
+ local updates = self._queuedUpdates;
151
+
152
+ local nUpdates = Sift.Dictionary.count(updates);
153
+ if nUpdates == 0 then return; end;
154
+
155
+ if self.player then
156
+ remote:FireClient(self.player, 'U', self.channel, updates);
157
+ else
158
+ remote:FireAllClients('U', self.channel, updates);
159
+ end;
160
+ self._queuedUpdates = {};
161
+ end;
162
+
163
+ function Server:_sendInitial(player: Player?)
164
+ local receiver = player or self.player;
165
+ if not receiver then return; end;
166
+
167
+ remote:FireClient(receiver, 'I', self.channel, self.data);
168
+ end;
169
+
170
+ remote.OnServerEvent:Connect(function(player, action, ...)
171
+ if action == 'I' then
172
+ local channel = ...;
173
+ if not channel then return; end;
174
+
175
+ local replion = Server._getPlayerReplion(player, channel);
176
+ if not replion then
177
+ replion = Server._getGlobalReplion(channel);
178
+ if not replion then return; end;
179
+ end;
180
+
181
+ replion:_sendInitial(player);
182
+ end;
183
+ end);
184
+
185
+ Players.PlayerRemoving:Connect(function(player)
186
+ local playerReplions = Server._activeReplions[player];
187
+ if not playerReplions then return; end;
188
+
189
+ for _, replion in playerReplions do
190
+ replion:destroy();
191
+ end;
192
+ Server._activeReplions[player] = nil;
193
+ end);
194
+
195
+ return Server;
package/src/Shared.lua ADDED
@@ -0,0 +1,18 @@
1
+ --!strict
2
+ export type GenericDataTable = { [string]: any };
3
+
4
+ local Shared = {};
5
+
6
+ Shared.REMOTE_NAME = 'ReplionNetwork';
7
+ Shared.NIL_SENTINEL = '\0\0__NIL__\0\0';
8
+
9
+ function Shared.getNestedValue(root: any, path: {string})
10
+ local current = root;
11
+ for _, key in path do
12
+ if typeof(current) ~= 'table' then return nil; end;
13
+ current = current[key];
14
+ end;
15
+ return current;
16
+ end;
17
+
18
+ return Shared;
package/src/index.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ interface State<T> {
2
+ get(): T;
3
+ set(newValue: T): void;
4
+ }
5
+
6
+ declare namespace Observers {
7
+ function state<T>(initialValue: T): State<T>;
8
+ function state<T>(): State<T | undefined>;
9
+
10
+ function observeAttribute<T extends AttributeValue>(instance: Instance, attribute: string, callback: (value: T | undefined) => (() => void) | void): () => void;
11
+ function observeState<T>(state: State<T>, callback: (value: T) => void): () => void;
12
+ function observePlayers(callback: (player: Player) => (() => void) | void): () => void;
13
+ function observeCharacters(callback: (char: Model, player: Player) => (() => void) | void): () => void;
14
+ function observeAttribute<T extends AttributeValue>(instance: Instance, attribute: string, callback: (value: T | undefined) => (() => void) | void): () => void;
15
+ function observeChildren(instance: Instance, callback: (child: Instance) => (() => void) | void): () => void;
16
+ function observeTag<T extends Instance>(tag: string, callback: (instance: T) => (() => void) | void, ancestors?: Instance[]): () => void;
17
+ function observeProperty<P extends Instance, K extends InstancePropertyNames<P>>(instance: P, property: K, callback: (value: P[K]) => (() => void) | void): () => void;
18
+ }
19
+
20
+ export = Observers;
package/src/init.lua ADDED
@@ -0,0 +1,12 @@
1
+ --!strict
2
+ local RunService = game:GetService('RunService');
3
+
4
+ local Replion = {};
5
+
6
+ if RunService:IsServer() then
7
+ Replion.Server = require(script.Server);
8
+ else
9
+ Replion.Client = require(script.Client);
10
+ end;
11
+
12
+ return Replion;