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