@rbxts/replion 1.0.19 → 1.0.20

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 CHANGED
@@ -2,4 +2,4 @@
2
2
 
3
3
  ## Install
4
4
  Install with [wally](https://wally.run/):\
5
- `Replion = "shouxtech/replion@1.0.19"`
5
+ `Replion = "shouxtech/replion@1.0.20"`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rbxts/replion",
3
- "version": "1.0.19",
3
+ "version": "1.0.20",
4
4
  "description": "",
5
5
  "main": "src/init.lua",
6
6
  "scripts": {},
@@ -15,7 +15,7 @@ function BaseReplion.new(data: GenericDataTable)
15
15
 
16
16
  self.data = data;
17
17
 
18
- self._signals = {} :: { [string]: any };
18
+ self._signalRoot = {};
19
19
  self._allSignal = Signal.new();
20
20
 
21
21
  return self;
@@ -26,23 +26,78 @@ function BaseReplion:get(path: { string }?)
26
26
  return Shared.getNestedValue(self.data, path);
27
27
  end;
28
28
 
29
+ function BaseReplion:_pruneSignalTree(path: {string})
30
+ local stack = {self._signalRoot};
31
+ local current = self._signalRoot;
32
+
33
+ for _, key in path do
34
+ current = current[key];
35
+ if not current then return; end;
36
+ table.insert(stack, current);
37
+ end
38
+
39
+ for i = #path, 1, -1 do
40
+ local key = path[i];
41
+ local node = stack[i + 1];
42
+ local parent = stack[i];
43
+
44
+ local hasListeners = (node._refCount or 0) > 0;
45
+ local hasChildren = false;
46
+
47
+ if not hasListeners then
48
+ for k in node do
49
+ if k == '_signal' then continue; end;
50
+ if k == '_refCount' then continue; end;
51
+ hasChildren = true;
52
+ break;
53
+ end;
54
+ end;
55
+
56
+ if not hasListeners and not hasChildren then
57
+ parent[key] = nil;
58
+ else
59
+ break;
60
+ end;
61
+ end;
62
+ end;
63
+
29
64
  function BaseReplion:subscribe(path: { string }?, callback: Observer)
30
- if not path then
65
+ if (not path) or (#path == 0) then
31
66
  local connection = self._allSignal:Connect(callback);
32
67
  return function()
33
68
  connection:Disconnect()
34
69
  end;
35
70
  end;
36
71
 
37
- local signalKey = Shared.getSignalKey(path);
38
- if not self._signals[signalKey] then
39
- self._signals[signalKey] = Signal.new();
72
+ local current = self._signalRoot;
73
+ for _, key in path do
74
+ if not current[key] then current[key] = {}; end;
75
+ current = current[key];
40
76
  end;
41
77
 
42
- local connection = self._signals[signalKey]:Connect(callback);
43
-
78
+ if not current._signal then
79
+ current._signal = Signal.new();
80
+ current._refCount = 0;
81
+ end;
82
+
83
+ local connection = current._signal:Connect(callback);
84
+ current._refCount = (current._refCount or 0) + 1;
85
+
44
86
  return function()
45
87
  connection:Disconnect();
88
+
89
+ if current._refCount then
90
+ current._refCount -= 1;
91
+ end;
92
+
93
+ if current._refCount <= 0 then
94
+ if current._signal then
95
+ current._signal:Destroy();
96
+ current._signal = nil;
97
+ end;
98
+ current._refCount = nil;
99
+ self:_pruneSignalTree(path);
100
+ end
46
101
  end;
47
102
  end;
48
103
 
@@ -59,10 +114,20 @@ end;
59
114
 
60
115
  function BaseReplion:destroy()
61
116
  self._allSignal:Destroy();
62
- for _, signal in self._signals do
63
- signal:Destroy();
64
- end;
65
- self._signals = {};
117
+
118
+ local function destroyTree(node)
119
+ if node._signal then
120
+ node._signal:Destroy();
121
+ end;
122
+ for k, v in node do
123
+ if k == '_signal' then continue; end;
124
+ if k == '_refCount' then continue; end;
125
+ if typeof(v) ~= 'table' then continue; end;
126
+ destroyTree(v);
127
+ end;
128
+ end;
129
+ destroyTree(self._signalRoot)
130
+ table.clear(self._signalRoot);
66
131
  end;
67
132
 
68
133
  return BaseReplion;
package/src/Client.lua CHANGED
@@ -5,22 +5,8 @@ local BaseReplion = require(script.Parent.BaseReplion)
5
5
  local Shared = require(script.Parent.Shared);
6
6
  local Signal = require(isTypeScriptEnv and dependencies['sleitnick-signal'] or dependencies.Signal);
7
7
 
8
- type Deletion = { path: { string }, oldTable: any };
9
-
10
8
  local remote = script.Parent:WaitForChild(Shared.REMOTE_NAME) :: RemoteEvent;
11
9
 
12
- local function deepMergeAndApply(target: any, updates: any)
13
- for key, value in updates do
14
- if value == Shared.NIL_SENTINEL then
15
- target[key] = nil;
16
- elseif typeof(value) == 'table' and typeof(target[key]) == 'table' then
17
- deepMergeAndApply(target[key], value);
18
- else
19
- target[key] = value;
20
- end;
21
- end;
22
- end;
23
-
24
10
  local Client = {};
25
11
  Client.__index = Client;
26
12
 
@@ -70,99 +56,63 @@ function Client:observe(path: { string }?, callback: BaseReplion.Observer)
70
56
  return self._base:observe(path, callback);
71
57
  end;
72
58
 
73
- function Client:_collectDeletions(target: any, updates: any, path: { string }, out: { Deletion })
74
- for k, v in updates do
75
- local currentVal = target[k];
76
- local newPath = table.clone(path);
77
- table.insert(newPath, k);
78
-
79
- if v == Shared.NIL_SENTINEL or (typeof(currentVal) == 'table' and typeof(v) ~= 'table') then
80
- if typeof(currentVal) == 'table' then
81
- table.insert(out, { path = newPath, oldTable = currentVal });
82
- end;
83
- elseif typeof(v) == 'table' and typeof(currentVal) == 'table' then
84
- self:_collectDeletions(currentVal, v, newPath, out);
85
- end;
86
- end;
59
+ function Client:_fireRecursive(signalNode: any, newValue: any, oldValue: any)
60
+ if not signalNode then return; end;
61
+
62
+ if signalNode._signal then
63
+ signalNode._signal:Fire(newValue, oldValue);
64
+ end;
65
+
66
+ for key, childNode in signalNode do
67
+ if key == '_signal' then continue; end;
68
+ if key == '_refCount' then continue; end;
69
+
70
+ local newChild = if typeof(newValue) == 'table' then newValue[key] else nil;
71
+ local oldChild = if typeof(oldValue) == 'table' then oldValue[key] else nil;
72
+
73
+ if newChild ~= nil or oldChild ~= nil then
74
+ self:_fireRecursive(childNode, newChild, oldChild);
75
+ end;
76
+ end;
87
77
  end;
88
78
 
89
- function Client:_notifyDeepDeletion(path: { string }, oldTable: any)
90
- local signals = self._base._signals;
91
-
92
- for k, v in oldTable do
93
- local nextPath = table.clone(path);
94
- table.insert(nextPath, k);
95
-
96
- local signalKey = Shared.getSignalKey(nextPath);
97
- local signal = signals[signalKey];
98
- if signal then
99
- signal:Fire(nil, v);
100
- end;
101
-
102
- if typeof(v) == 'table' then
103
- self:_notifyDeepDeletion(nextPath, v);
104
- end;
105
- end;
106
- end;
107
-
108
- function Client:_notifyRecursiveUpdates(currentPath: { string }, updateTree: any)
109
- local signals = self._base._signals;
110
-
111
- for k, v in updateTree do
112
- local nextPath = table.clone(currentPath);
113
- table.insert(nextPath, k);
114
-
115
- local signalKey = Shared.getSignalKey(nextPath);
116
- local signal = signals[signalKey];
117
-
118
- if signal then
119
- local newValue = Shared.getNestedValue(self._base.data, nextPath);
120
- signal:Fire(newValue, newValue); -- Warning: Sends improper 'old' argument due to shallow copy optimization.
121
- end;
122
-
123
- if typeof(v) == 'table' then
124
- self:_notifyRecursiveUpdates(nextPath, v);
125
- end;
126
- end;
79
+ function Client:_applyUpdatesRecursive(target: any, updates: any, signalNode: any)
80
+ local anyChanged = false;
81
+
82
+ for key, updateVal in updates do
83
+ local currentVal = target[key];
84
+ local nextSignalNode = if signalNode then signalNode[key] else nil;
85
+
86
+ local finalVal = updateVal;
87
+ if finalVal == Shared.NIL_SENTINEL then finalVal = nil; end;
88
+
89
+ if typeof(finalVal) == 'table' and typeof(currentVal) == 'table' then
90
+ if self:_applyUpdatesRecursive(currentVal, finalVal, nextSignalNode) then
91
+ anyChanged = true;
92
+
93
+ if nextSignalNode and nextSignalNode._signal then
94
+ nextSignalNode._signal:Fire(currentVal, currentVal);
95
+ end;
96
+ end;
97
+ else
98
+ if currentVal ~= finalVal then
99
+ target[key] = finalVal;
100
+ anyChanged = true;
101
+
102
+ if nextSignalNode then
103
+ self:_fireRecursive(nextSignalNode, finalVal, currentVal);
104
+ end;
105
+ end;
106
+ end;
107
+ end;
108
+
109
+ return anyChanged;
127
110
  end;
128
111
 
129
112
  function Client:_applyUpdates(updates: BaseReplion.GenericDataTable)
130
- local deletions: { Deletion } = {};
131
- self:_collectDeletions(self._base.data, updates, {}, deletions);
132
-
133
- local oldData = table.clone(self._base.data); -- Note: Shallow copy instead of deep copy.
134
-
135
- deepMergeAndApply(self._base.data, updates);
136
-
137
- local anyChanged = false;
138
-
139
- for _, item in deletions do
140
- self:_notifyDeepDeletion(item.path, item.oldTable);
141
- end;
142
-
143
- for key, newValue in updates do
144
- local oldValue = oldData[key];
145
-
146
- -- If it's a table, we assume it changed if it's in the updates list.
147
- if typeof(newValue) == 'table' then
148
- anyChanged = true;
149
- local signal = self._base._signals[key];
150
- if signal then
151
- signal:Fire(self._base.data[key], self._base.data[key]); -- Warning: Sends improper 'old' argument due to shallow copy optimization.
152
- end;
153
- self:_notifyRecursiveUpdates({key}, newValue);
154
- elseif self._base.data[key] ~= oldValue then
155
- anyChanged = true;
156
- local signal = self._base._signals[key];
157
- if signal then
158
- signal:Fire(self._base.data[key], oldValue);
159
- end;
160
- end;
161
- end;
162
-
163
- if anyChanged then
164
- self._base._allSignal:Fire(self._base.data, updates);
165
- end;
113
+ local changed = self:_applyUpdatesRecursive(self._base.data, updates, self._base._signalRoot);
114
+ if not changed then return; end;
115
+ self._base._allSignal:Fire(self._base.data, updates);
166
116
  end;
167
117
 
168
118
  function Client:destroy()
package/src/Server.lua CHANGED
@@ -61,7 +61,6 @@ Server._globalReplions = {} :: { [string]: Replion };
61
61
  function Server._getPlayerReplion(player: Player, channel: string)
62
62
  local playerReplions = Server._activeReplions[player];
63
63
  if not playerReplions then return nil; end;
64
-
65
64
  return playerReplions[channel];
66
65
  end;
67
66
 
@@ -108,75 +107,56 @@ function Server:get(path: { string }?)
108
107
  return self._base:get(path);
109
108
  end;
110
109
 
111
- function Server:_notifyDeep(currentPath: { string }, newValue: any, oldValue: any)
112
- local signals = self._base._signals;
113
-
114
- if typeof(newValue) == 'table' then
115
- for k, v in newValue do
116
- local nextPath = table.clone(currentPath);
117
- table.insert(nextPath, k);
110
+ function Server:_notifyDeep(signalNode: any, newValue: any, oldValue: any)
111
+ if not signalNode then return; end;
118
112
 
119
- local nextOld = if typeof(oldValue) == 'table' then oldValue[k] else nil;
120
-
121
- local signalKey = Shared.getSignalKey(nextPath);
122
- local signal = signals[signalKey];
123
- if signal then
124
- signal:Fire(v, nextOld);
125
- end;
113
+ if signalNode._signal then
114
+ signalNode._signal:Fire(newValue, oldValue);
115
+ end;
126
116
 
127
- self:_notifyDeep(nextPath, v, nextOld);
128
- end;
129
- end;
117
+ for key, childNode in signalNode do
118
+ if key == '_signal' then continue; end;
119
+ if key == '_refCount' then continue; end;
130
120
 
131
- if typeof(oldValue) == 'table' then
132
- for k, v in oldValue do
133
- if typeof(newValue) == 'table' and newValue[k] ~= nil then continue; end;
121
+ local nextNew = if typeof(newValue) == 'table' then newValue[key] else nil;
122
+ local nextOld = if typeof(oldValue) == 'table' then oldValue[key] else nil;
134
123
 
135
- local nextPath = table.clone(currentPath);
136
- table.insert(nextPath, k);
137
-
138
- local signalKey = Shared.getSignalKey(nextPath);
139
- local signal = signals[signalKey];
140
- if signal then
141
- signal:Fire(nil, v);
142
- end;
143
-
144
- self:_notifyDeep(nextPath, nil, v);
145
- end;
146
- end;
124
+ if nextNew ~= nil or nextOld ~= nil then
125
+ self:_notifyDeep(childNode, nextNew, nextOld);
126
+ end;
127
+ end;
147
128
  end;
148
129
 
149
130
  function Server:set(path: { string }, value: any)
150
131
  if self._destroyed then return; end;
151
132
 
152
- local topKey = path[1];
153
-
154
133
  local oldValue = Shared.getNestedValue(self._base.data, path);
155
134
  local newValue = if typeof(value) == 'function' then value(oldValue) else value;
156
135
  if oldValue == newValue then return; end;
157
136
 
158
- local oldTop = self._base.data[topKey];
159
-
160
137
  setNestedValue(self._base.data, path, newValue);
161
138
  queueNestedUpdate(self._queuedUpdates, path, newValue);
162
139
  self:_scheduleUpdatesFlush();
163
140
 
164
141
  do
165
- local signals = self._base._signals;
166
- local topSignal = signals[topKey];
167
- if topSignal then
168
- topSignal:Fire(self._base.data[topKey], oldTop);
169
- end;
142
+ local rootKey = path[1];
143
+ local currentSignalNode = self._base._signalRoot;
170
144
 
171
- local signalKey = Shared.getSignalKey(path);
172
- if signalKey ~= topKey then
173
- local pathSignal = signals[signalKey];
174
- if pathSignal then
175
- pathSignal:Fire(newValue, oldValue)
176
- end;
177
- end;
145
+ local rootSignalNode = if currentSignalNode then currentSignalNode[rootKey] else nil;
146
+
147
+ for _, key in path do
148
+ if not currentSignalNode then break; end;
149
+ currentSignalNode = currentSignalNode[key];
150
+ end;
151
+
152
+ if rootSignalNode and rootSignalNode ~= currentSignalNode and rootSignalNode._signal then
153
+ local rootValue = self._base.data[rootKey];
154
+ rootSignalNode._signal:Fire(rootValue, rootValue);
155
+ end;
178
156
 
179
- self:_notifyDeep(path, newValue, oldValue);
157
+ if currentSignalNode then
158
+ self:_notifyDeep(currentSignalNode, newValue, oldValue);
159
+ end;
180
160
 
181
161
  local thisUpdate = {};
182
162
  setNestedValue(thisUpdate, path, newValue);
package/src/Shared.lua CHANGED
@@ -13,9 +13,4 @@ function Shared.getNestedValue(root: any, path: { string })
13
13
  return current;
14
14
  end;
15
15
 
16
- function Shared.getSignalKey(key: string | { string })
17
- if typeof(key) == 'string' then return key; end;
18
- return table.concat(key, '\0');
19
- end;
20
-
21
16
  return Shared;