dominus-cli 2.0.0 → 2.1.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/CHANGELOG.md +175 -0
- package/README.md +164 -248
- package/dist/install-plugin.js +30 -0
- package/dist/install-plugin.js.map +1 -0
- package/dist/mcp.js +1477 -4739
- package/dist/mcp.js.map +1 -1
- package/docs/DOMINUS_2_PLAN.md +75 -0
- package/package.json +24 -25
- package/plugin/Dominus.rbxm +0 -0
- package/plugin/src/CommandRouter.lua +94 -0
- package/plugin/src/Explorer.lua +530 -373
- package/plugin/src/InstanceRegistry.lua +122 -0
- package/plugin/src/Mutation.lua +135 -0
- package/plugin/src/Properties.lua +859 -785
- package/plugin/src/Protocol.lua +30 -30
- package/plugin/src/Reflection.lua +91 -59
- package/plugin/src/TestRunner.lua +69 -141
- package/plugin/src/UIBuilder.lua +857 -727
- package/plugin/src/ValueCodec.lua +230 -0
- package/plugin/src/Watcher.lua +85 -85
- package/plugin/src/WsClient.lua +153 -81
- package/plugin/src/init.server.lua +175 -362
- package/dist/index.js +0 -7998
- package/dist/index.js.map +0 -1
- package/plugin/src/Executor.lua +0 -117
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
local InstanceRegistry = require(script.Parent.InstanceRegistry)
|
|
2
|
+
|
|
3
|
+
local ValueCodec = {}
|
|
4
|
+
|
|
5
|
+
local function number(value, label)
|
|
6
|
+
if type(value) ~= "number" then
|
|
7
|
+
error(label .. " must be a number")
|
|
8
|
+
end
|
|
9
|
+
return value
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
local function component(value, names, index, default)
|
|
13
|
+
for _, name in names do
|
|
14
|
+
if value[name] ~= nil then
|
|
15
|
+
return number(value[name], name)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
if value[index] ~= nil then
|
|
19
|
+
return number(value[index], tostring(index))
|
|
20
|
+
end
|
|
21
|
+
return default
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
local function decodeColor3(value)
|
|
25
|
+
if type(value) == "string" then
|
|
26
|
+
if value:sub(1, 1) == "#" then
|
|
27
|
+
return Color3.fromHex(value)
|
|
28
|
+
end
|
|
29
|
+
local r, g, b = value:match("rgb%s*%((%d+)%D+(%d+)%D+(%d+)%)")
|
|
30
|
+
if r then
|
|
31
|
+
return Color3.fromRGB(tonumber(r), tonumber(g), tonumber(b))
|
|
32
|
+
end
|
|
33
|
+
error("Color3 strings must be #RRGGBB or rgb(r,g,b)")
|
|
34
|
+
end
|
|
35
|
+
if type(value) ~= "table" then
|
|
36
|
+
error("Color3 must be a string or array")
|
|
37
|
+
end
|
|
38
|
+
local r = component(value, { "R", "r", "red" }, 1, 0)
|
|
39
|
+
local g = component(value, { "G", "g", "green" }, 2, 0)
|
|
40
|
+
local b = component(value, { "B", "b", "blue" }, 3, 0)
|
|
41
|
+
if r > 1 or g > 1 or b > 1 then
|
|
42
|
+
return Color3.fromRGB(r, g, b)
|
|
43
|
+
end
|
|
44
|
+
return Color3.new(r, g, b)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
local function decodeEnum(current, value)
|
|
48
|
+
if typeof(value) == "EnumItem" then
|
|
49
|
+
return value
|
|
50
|
+
end
|
|
51
|
+
if type(value) ~= "string" then
|
|
52
|
+
error("Enum value must be a string")
|
|
53
|
+
end
|
|
54
|
+
local enumName = tostring(current.EnumType):match("Enum%.(.+)")
|
|
55
|
+
local itemName = value:match("Enum%.[^.]+%.(.+)") or value
|
|
56
|
+
if not enumName or not Enum[enumName] or not Enum[enumName][itemName] then
|
|
57
|
+
error("Invalid enum value " .. value)
|
|
58
|
+
end
|
|
59
|
+
return Enum[enumName][itemName]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
function ValueCodec.decodeForCurrent(current, value)
|
|
63
|
+
local expectedType = typeof(current)
|
|
64
|
+
if expectedType == "UDim2" then
|
|
65
|
+
assert(type(value) == "table", "UDim2 must be an array or object")
|
|
66
|
+
return UDim2.new(
|
|
67
|
+
component(value, { "XScale", "xScale" }, 1, 0),
|
|
68
|
+
component(value, { "XOffset", "xOffset" }, 2, 0),
|
|
69
|
+
component(value, { "YScale", "yScale" }, 3, 0),
|
|
70
|
+
component(value, { "YOffset", "yOffset" }, 4, 0)
|
|
71
|
+
)
|
|
72
|
+
elseif expectedType == "UDim" then
|
|
73
|
+
assert(type(value) == "table", "UDim must be an array or object")
|
|
74
|
+
return UDim.new(component(value, { "Scale", "scale" }, 1, 0), component(value, { "Offset", "offset" }, 2, 0))
|
|
75
|
+
elseif expectedType == "Vector2" then
|
|
76
|
+
assert(type(value) == "table", "Vector2 must be an array or object")
|
|
77
|
+
return Vector2.new(component(value, { "X", "x" }, 1, 0), component(value, { "Y", "y" }, 2, 0))
|
|
78
|
+
elseif expectedType == "Vector3" then
|
|
79
|
+
assert(type(value) == "table", "Vector3 must be an array or object")
|
|
80
|
+
return Vector3.new(
|
|
81
|
+
component(value, { "X", "x" }, 1, 0),
|
|
82
|
+
component(value, { "Y", "y" }, 2, 0),
|
|
83
|
+
component(value, { "Z", "z" }, 3, 0)
|
|
84
|
+
)
|
|
85
|
+
elseif expectedType == "Color3" then
|
|
86
|
+
return decodeColor3(value)
|
|
87
|
+
elseif expectedType == "BrickColor" then
|
|
88
|
+
return BrickColor.new(value)
|
|
89
|
+
elseif expectedType == "EnumItem" then
|
|
90
|
+
return decodeEnum(current, value)
|
|
91
|
+
elseif expectedType == "Rect" then
|
|
92
|
+
assert(type(value) == "table", "Rect must be a four-number array")
|
|
93
|
+
return Rect.new(number(value[1], "1"), number(value[2], "2"), number(value[3], "3"), number(value[4], "4"))
|
|
94
|
+
elseif expectedType == "NumberRange" then
|
|
95
|
+
if type(value) == "number" then
|
|
96
|
+
return NumberRange.new(value)
|
|
97
|
+
end
|
|
98
|
+
assert(type(value) == "table", "NumberRange must be a number or two-number array")
|
|
99
|
+
return NumberRange.new(number(value[1], "1"), number(value[2] or value[1], "2"))
|
|
100
|
+
elseif expectedType == "CFrame" then
|
|
101
|
+
assert(type(value) == "table", "CFrame must be an array")
|
|
102
|
+
if #value == 3 then
|
|
103
|
+
return CFrame.new(value[1], value[2], value[3])
|
|
104
|
+
elseif #value == 12 then
|
|
105
|
+
return CFrame.new(table.unpack(value))
|
|
106
|
+
end
|
|
107
|
+
error("CFrame requires 3 or 12 numbers")
|
|
108
|
+
elseif expectedType == "NumberSequence" then
|
|
109
|
+
if type(value) == "number" then
|
|
110
|
+
return NumberSequence.new(value)
|
|
111
|
+
end
|
|
112
|
+
assert(type(value) == "table", "NumberSequence must be a number or keypoint array")
|
|
113
|
+
local keypoints = {}
|
|
114
|
+
for _, keypoint in value do
|
|
115
|
+
table.insert(keypoints, NumberSequenceKeypoint.new(keypoint[1], keypoint[2], keypoint[3] or 0))
|
|
116
|
+
end
|
|
117
|
+
return NumberSequence.new(keypoints)
|
|
118
|
+
elseif expectedType == "ColorSequence" then
|
|
119
|
+
if type(value) == "string" then
|
|
120
|
+
return ColorSequence.new(decodeColor3(value))
|
|
121
|
+
end
|
|
122
|
+
assert(type(value) == "table", "ColorSequence must be a color or keypoint array")
|
|
123
|
+
local keypoints = {}
|
|
124
|
+
for _, keypoint in value do
|
|
125
|
+
table.insert(keypoints, ColorSequenceKeypoint.new(keypoint[1], decodeColor3(keypoint[2])))
|
|
126
|
+
end
|
|
127
|
+
return ColorSequence.new(keypoints)
|
|
128
|
+
elseif expectedType == "Font" then
|
|
129
|
+
assert(type(value) == "table", "Font must be an object")
|
|
130
|
+
local weight = Enum.FontWeight[value.Weight or value.weight or "Regular"]
|
|
131
|
+
local style = Enum.FontStyle[value.Style or value.style or "Normal"]
|
|
132
|
+
assert(weight and style, "Invalid Font weight or style")
|
|
133
|
+
return Font.new(value.Family or value.family, weight, style)
|
|
134
|
+
elseif expectedType == "Instance" then
|
|
135
|
+
local resolved, err = InstanceRegistry.resolve(value)
|
|
136
|
+
if not resolved then
|
|
137
|
+
error(err)
|
|
138
|
+
end
|
|
139
|
+
return resolved
|
|
140
|
+
elseif expectedType == "number" then
|
|
141
|
+
return number(value, "Property")
|
|
142
|
+
elseif expectedType == "boolean" then
|
|
143
|
+
assert(type(value) == "boolean", "Property must be a boolean")
|
|
144
|
+
return value
|
|
145
|
+
elseif expectedType == "string" then
|
|
146
|
+
assert(type(value) == "string", "Property must be a string")
|
|
147
|
+
return value
|
|
148
|
+
end
|
|
149
|
+
return value
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
function ValueCodec.encode(value)
|
|
153
|
+
local valueType = typeof(value)
|
|
154
|
+
if valueType == "UDim2" then
|
|
155
|
+
return { value.X.Scale, value.X.Offset, value.Y.Scale, value.Y.Offset }
|
|
156
|
+
end
|
|
157
|
+
if valueType == "UDim" then
|
|
158
|
+
return { value.Scale, value.Offset }
|
|
159
|
+
end
|
|
160
|
+
if valueType == "Vector2" then
|
|
161
|
+
return { value.X, value.Y }
|
|
162
|
+
end
|
|
163
|
+
if valueType == "Vector3" then
|
|
164
|
+
return { value.X, value.Y, value.Z }
|
|
165
|
+
end
|
|
166
|
+
if valueType == "Color3" then
|
|
167
|
+
return "#" .. value:ToHex()
|
|
168
|
+
end
|
|
169
|
+
if valueType == "BrickColor" then
|
|
170
|
+
return value.Name
|
|
171
|
+
end
|
|
172
|
+
if valueType == "EnumItem" then
|
|
173
|
+
return tostring(value)
|
|
174
|
+
end
|
|
175
|
+
if valueType == "Rect" then
|
|
176
|
+
return { value.Min.X, value.Min.Y, value.Max.X, value.Max.Y }
|
|
177
|
+
end
|
|
178
|
+
if valueType == "NumberRange" then
|
|
179
|
+
return { value.Min, value.Max }
|
|
180
|
+
end
|
|
181
|
+
if valueType == "CFrame" then
|
|
182
|
+
return { value:GetComponents() }
|
|
183
|
+
end
|
|
184
|
+
if valueType == "NumberSequence" then
|
|
185
|
+
local result = {}
|
|
186
|
+
for _, keypoint in value.Keypoints do
|
|
187
|
+
table.insert(result, { keypoint.Time, keypoint.Value, keypoint.Envelope })
|
|
188
|
+
end
|
|
189
|
+
return result
|
|
190
|
+
end
|
|
191
|
+
if valueType == "ColorSequence" then
|
|
192
|
+
local result = {}
|
|
193
|
+
for _, keypoint in value.Keypoints do
|
|
194
|
+
table.insert(result, { keypoint.Time, "#" .. keypoint.Value:ToHex() })
|
|
195
|
+
end
|
|
196
|
+
return result
|
|
197
|
+
end
|
|
198
|
+
if valueType == "Font" then
|
|
199
|
+
return { Family = value.Family, Weight = value.Weight.Name, Style = value.Style.Name }
|
|
200
|
+
end
|
|
201
|
+
if valueType == "Instance" then
|
|
202
|
+
return InstanceRegistry.toRef(value)
|
|
203
|
+
end
|
|
204
|
+
if valueType == "boolean" or valueType == "number" or valueType == "string" then
|
|
205
|
+
return value
|
|
206
|
+
end
|
|
207
|
+
return tostring(value)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
function ValueCodec.setProperty(instance, propertyName, value)
|
|
211
|
+
if propertyName == "Parent" or propertyName == "ClassName" then
|
|
212
|
+
error("Property cannot be changed through setProperties: " .. propertyName)
|
|
213
|
+
end
|
|
214
|
+
local readOk, current = pcall(function()
|
|
215
|
+
return instance[propertyName]
|
|
216
|
+
end)
|
|
217
|
+
if not readOk then
|
|
218
|
+
error("Unknown or unreadable property: " .. propertyName)
|
|
219
|
+
end
|
|
220
|
+
local decoded = ValueCodec.decodeForCurrent(current, value)
|
|
221
|
+
local writeOk, writeErr = pcall(function()
|
|
222
|
+
instance[propertyName] = decoded
|
|
223
|
+
end)
|
|
224
|
+
if not writeOk then
|
|
225
|
+
error("Failed to set " .. propertyName .. ": " .. tostring(writeErr))
|
|
226
|
+
end
|
|
227
|
+
return ValueCodec.encode(instance[propertyName])
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
return ValueCodec
|
package/plugin/src/Watcher.lua
CHANGED
|
@@ -1,85 +1,85 @@
|
|
|
1
|
-
--[[
|
|
2
|
-
Watcher: monitors LogService output and forwards to CLI
|
|
3
|
-
]]
|
|
4
|
-
|
|
5
|
-
local LogService = game:GetService("LogService")
|
|
6
|
-
|
|
7
|
-
local Watcher = {}
|
|
8
|
-
|
|
9
|
-
local outputBuffer = {}
|
|
10
|
-
local MAX_BUFFER = 500
|
|
11
|
-
local connection = nil
|
|
12
|
-
local outputCallback = nil
|
|
13
|
-
|
|
14
|
-
local function classifyLevel(messageType)
|
|
15
|
-
if messageType == Enum.MessageType.MessageOutput then
|
|
16
|
-
return "info"
|
|
17
|
-
elseif messageType == Enum.MessageType.MessageWarning then
|
|
18
|
-
return "warn"
|
|
19
|
-
elseif messageType == Enum.MessageType.MessageError then
|
|
20
|
-
return "error"
|
|
21
|
-
end
|
|
22
|
-
return "info"
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
function Watcher.start(callback)
|
|
26
|
-
outputCallback = callback
|
|
27
|
-
|
|
28
|
-
if connection then
|
|
29
|
-
connection:Disconnect()
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
connection = LogService.MessageOut:Connect(function(message, messageType)
|
|
33
|
-
-- Skip Dominus
|
|
34
|
-
if string.find(message, "[Dominus
|
|
35
|
-
return
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
local entry = {
|
|
39
|
-
message = message,
|
|
40
|
-
level = classifyLevel(messageType),
|
|
41
|
-
timestamp = os.clock(),
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
-- Buffer for get_output requests
|
|
45
|
-
table.insert(outputBuffer, entry)
|
|
46
|
-
if #outputBuffer > MAX_BUFFER then
|
|
47
|
-
table.remove(outputBuffer, 1)
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
-- Forward to CLI
|
|
51
|
-
if outputCallback then
|
|
52
|
-
outputCallback(entry)
|
|
53
|
-
end
|
|
54
|
-
end)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
function Watcher.stop()
|
|
58
|
-
if connection then
|
|
59
|
-
connection:Disconnect()
|
|
60
|
-
connection = nil
|
|
61
|
-
end
|
|
62
|
-
outputCallback = nil
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
function Watcher.getRecentOutput(limit, levelFilter)
|
|
66
|
-
limit = limit or 50
|
|
67
|
-
local result = {}
|
|
68
|
-
|
|
69
|
-
for i = math.max(1, #outputBuffer - limit + 1), #outputBuffer do
|
|
70
|
-
local entry = outputBuffer[i]
|
|
71
|
-
if entry then
|
|
72
|
-
if not levelFilter or entry.level == levelFilter then
|
|
73
|
-
table.insert(result, entry)
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
return { entries = result }
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
function Watcher.clearBuffer()
|
|
82
|
-
outputBuffer = {}
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
return Watcher
|
|
1
|
+
--[[
|
|
2
|
+
Watcher: monitors LogService output and forwards to CLI
|
|
3
|
+
]]
|
|
4
|
+
|
|
5
|
+
local LogService = game:GetService("LogService")
|
|
6
|
+
|
|
7
|
+
local Watcher = {}
|
|
8
|
+
|
|
9
|
+
local outputBuffer = {}
|
|
10
|
+
local MAX_BUFFER = 500
|
|
11
|
+
local connection = nil
|
|
12
|
+
local outputCallback = nil
|
|
13
|
+
|
|
14
|
+
local function classifyLevel(messageType)
|
|
15
|
+
if messageType == Enum.MessageType.MessageOutput then
|
|
16
|
+
return "info"
|
|
17
|
+
elseif messageType == Enum.MessageType.MessageWarning then
|
|
18
|
+
return "warn"
|
|
19
|
+
elseif messageType == Enum.MessageType.MessageError then
|
|
20
|
+
return "error"
|
|
21
|
+
end
|
|
22
|
+
return "info"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
function Watcher.start(callback)
|
|
26
|
+
outputCallback = callback
|
|
27
|
+
|
|
28
|
+
if connection then
|
|
29
|
+
connection:Disconnect()
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
connection = LogService.MessageOut:Connect(function(message, messageType)
|
|
33
|
+
-- Skip every Dominus-prefixed message, including transport errors.
|
|
34
|
+
if string.find(message, "[Dominus", 1, true) then
|
|
35
|
+
return
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
local entry = {
|
|
39
|
+
message = message,
|
|
40
|
+
level = classifyLevel(messageType),
|
|
41
|
+
timestamp = os.clock(),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
-- Buffer for get_output requests
|
|
45
|
+
table.insert(outputBuffer, entry)
|
|
46
|
+
if #outputBuffer > MAX_BUFFER then
|
|
47
|
+
table.remove(outputBuffer, 1)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
-- Forward to CLI
|
|
51
|
+
if outputCallback then
|
|
52
|
+
outputCallback(entry)
|
|
53
|
+
end
|
|
54
|
+
end)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
function Watcher.stop()
|
|
58
|
+
if connection then
|
|
59
|
+
connection:Disconnect()
|
|
60
|
+
connection = nil
|
|
61
|
+
end
|
|
62
|
+
outputCallback = nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
function Watcher.getRecentOutput(limit, levelFilter)
|
|
66
|
+
limit = limit or 50
|
|
67
|
+
local result = {}
|
|
68
|
+
|
|
69
|
+
for i = math.max(1, #outputBuffer - limit + 1), #outputBuffer do
|
|
70
|
+
local entry = outputBuffer[i]
|
|
71
|
+
if entry then
|
|
72
|
+
if not levelFilter or entry.level == levelFilter then
|
|
73
|
+
table.insert(result, entry)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
return { entries = result }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
function Watcher.clearBuffer()
|
|
82
|
+
outputBuffer = {}
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
return Watcher
|
package/plugin/src/WsClient.lua
CHANGED
|
@@ -1,81 +1,153 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
local
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
function
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
|
|
1
|
+
local HttpService = game:GetService("HttpService")
|
|
2
|
+
|
|
3
|
+
local WsClient = {}
|
|
4
|
+
|
|
5
|
+
local currentConnection = nil
|
|
6
|
+
local currentSignals = {}
|
|
7
|
+
local generation = 0
|
|
8
|
+
|
|
9
|
+
local function disconnectSignals()
|
|
10
|
+
for _, signal in currentSignals do
|
|
11
|
+
signal:Disconnect()
|
|
12
|
+
end
|
|
13
|
+
currentSignals = {}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
local function closeCandidate(candidate)
|
|
17
|
+
pcall(function()
|
|
18
|
+
candidate:Close()
|
|
19
|
+
end)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
function WsClient.connect(url, callbacks, timeoutSeconds)
|
|
23
|
+
WsClient.disconnect()
|
|
24
|
+
generation += 1
|
|
25
|
+
local attempt = generation
|
|
26
|
+
local completion = Instance.new("BindableEvent")
|
|
27
|
+
local completed = false
|
|
28
|
+
local opened = false
|
|
29
|
+
|
|
30
|
+
local ok, candidateOrError = pcall(function()
|
|
31
|
+
return HttpService:CreateWebStreamClient(Enum.WebStreamClientType.WebSocket, { Url = url })
|
|
32
|
+
end)
|
|
33
|
+
if not ok then
|
|
34
|
+
completion:Destroy()
|
|
35
|
+
if callbacks.onError then
|
|
36
|
+
callbacks.onError(0, tostring(candidateOrError))
|
|
37
|
+
end
|
|
38
|
+
return false, tostring(candidateOrError)
|
|
39
|
+
end
|
|
40
|
+
local candidate = candidateOrError
|
|
41
|
+
|
|
42
|
+
local function finish(success, reason)
|
|
43
|
+
if completed then
|
|
44
|
+
return
|
|
45
|
+
end
|
|
46
|
+
completed = true
|
|
47
|
+
completion:Fire(success, reason)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
table.insert(
|
|
51
|
+
currentSignals,
|
|
52
|
+
candidate.Opened:Connect(function(statusCode, headers)
|
|
53
|
+
if attempt ~= generation then
|
|
54
|
+
return
|
|
55
|
+
end
|
|
56
|
+
opened = true
|
|
57
|
+
currentConnection = candidate
|
|
58
|
+
if callbacks.onOpen then
|
|
59
|
+
callbacks.onOpen(statusCode, headers)
|
|
60
|
+
end
|
|
61
|
+
finish(true)
|
|
62
|
+
end)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
table.insert(
|
|
66
|
+
currentSignals,
|
|
67
|
+
candidate.MessageReceived:Connect(function(message)
|
|
68
|
+
if attempt == generation and opened and callbacks.onMessage then
|
|
69
|
+
callbacks.onMessage(message)
|
|
70
|
+
end
|
|
71
|
+
end)
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
table.insert(
|
|
75
|
+
currentSignals,
|
|
76
|
+
candidate.Error:Connect(function(statusCode, errorMessage)
|
|
77
|
+
if attempt ~= generation then
|
|
78
|
+
return
|
|
79
|
+
end
|
|
80
|
+
if callbacks.onError then
|
|
81
|
+
callbacks.onError(statusCode, errorMessage)
|
|
82
|
+
end
|
|
83
|
+
if not opened then
|
|
84
|
+
finish(false, errorMessage)
|
|
85
|
+
end
|
|
86
|
+
end)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
table.insert(
|
|
90
|
+
currentSignals,
|
|
91
|
+
candidate.Closed:Connect(function()
|
|
92
|
+
if attempt ~= generation then
|
|
93
|
+
return
|
|
94
|
+
end
|
|
95
|
+
local wasOpened = opened
|
|
96
|
+
currentConnection = nil
|
|
97
|
+
opened = false
|
|
98
|
+
if callbacks.onClose then
|
|
99
|
+
callbacks.onClose()
|
|
100
|
+
end
|
|
101
|
+
if not wasOpened then
|
|
102
|
+
finish(false, "Connection closed before opening")
|
|
103
|
+
end
|
|
104
|
+
end)
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
task.delay(timeoutSeconds or 4, function()
|
|
108
|
+
if attempt == generation and not completed then
|
|
109
|
+
closeCandidate(candidate)
|
|
110
|
+
finish(false, "Connection timed out")
|
|
111
|
+
end
|
|
112
|
+
end)
|
|
113
|
+
|
|
114
|
+
local success, reason = completion.Event:Wait()
|
|
115
|
+
completion:Destroy()
|
|
116
|
+
if not success then
|
|
117
|
+
closeCandidate(candidate)
|
|
118
|
+
if attempt == generation then
|
|
119
|
+
currentConnection = nil
|
|
120
|
+
disconnectSignals()
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
return success, reason
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
function WsClient.send(message)
|
|
127
|
+
if not currentConnection then
|
|
128
|
+
return false, "WebSocket is not open"
|
|
129
|
+
end
|
|
130
|
+
local ok, err = pcall(function()
|
|
131
|
+
currentConnection:Send(message)
|
|
132
|
+
end)
|
|
133
|
+
if not ok then
|
|
134
|
+
return false, tostring(err)
|
|
135
|
+
end
|
|
136
|
+
return true
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
function WsClient.disconnect()
|
|
140
|
+
generation += 1
|
|
141
|
+
local connection = currentConnection
|
|
142
|
+
currentConnection = nil
|
|
143
|
+
disconnectSignals()
|
|
144
|
+
if connection then
|
|
145
|
+
closeCandidate(connection)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
function WsClient.isConnected()
|
|
150
|
+
return currentConnection ~= nil
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
return WsClient
|