forked from rojo-rbx/rojo
Use WebSocket instead of Long Polling (#1142)
This commit is contained in:
@@ -69,10 +69,12 @@ Making a new release? Simply add the new header with the version and date undern
|
||||
- `syncUnscriptable` defaults to `true` instead of `false`
|
||||
- `ignoreTrees` doesn't require the root of the project's name in it.
|
||||
* Fixed bugs and improved performance & UX for the script diff viewer ([#994])
|
||||
* Rebuilt the internal communication between the server and plugin to use [websockets](https://devforum.roblox.com/t/websockets-support-in-studio-is-now-available/4021932/1) instead of [long polling](https://en.wikipedia.org/wiki/Push_technology#Long_polling) ([#1142])
|
||||
* Added support for `.jsonc` files for all JSON-related files (e.g. `.project.jsonc` and `.meta.jsonc`) to accompany JSONC support ([#1159])
|
||||
|
||||
[#937]: https://github.com/rojo-rbx/rojo/pull/937
|
||||
[#994]: https://github.com/rojo-rbx/rojo/pull/994
|
||||
[#1142]: https://github.com/rojo-rbx/rojo/pull/1142
|
||||
[#1159]: https://github.com/rojo-rbx/rojo/pull/1159
|
||||
[#1172]: https://github.com/rojo-rbx/rojo/pull/1172
|
||||
|
||||
|
||||
1566
Cargo.lock
generated
1566
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -72,6 +72,7 @@ futures = "0.3.30"
|
||||
globset = "0.4.14"
|
||||
humantime = "2.1.0"
|
||||
hyper = { version = "0.14.28", features = ["server", "tcp", "http1"] }
|
||||
hyper-tungstenite = "0.11.0"
|
||||
jod-thread = "0.1.2"
|
||||
log = "0.4.21"
|
||||
num_cpus = "1.16.0"
|
||||
@@ -87,10 +88,11 @@ roblox_install = "1.0.0"
|
||||
serde = { version = "1.0.197", features = ["derive", "rc"] }
|
||||
serde_json = "1.0.145"
|
||||
jsonc-parser = { version = "0.27.0", features = ["serde"] }
|
||||
strum = { version = "0.27", features = ["derive"] }
|
||||
toml = "0.5.11"
|
||||
termcolor = "1.4.1"
|
||||
thiserror = "1.0.57"
|
||||
tokio = { version = "1.36.0", features = ["rt", "rt-multi-thread"] }
|
||||
tokio = { version = "1.36.0", features = ["rt", "rt-multi-thread", "macros"] }
|
||||
uuid = { version = "1.7.0", features = ["v4", "serde"] }
|
||||
clap = { version = "3.2.25", features = ["derive"] }
|
||||
profiling = "1.0.15"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
local Packages = script.Parent.Parent.Packages
|
||||
local HttpService = game:GetService("HttpService")
|
||||
local Http = require(Packages.Http)
|
||||
local Log = require(Packages.Log)
|
||||
local Promise = require(Packages.Promise)
|
||||
@@ -9,7 +10,7 @@ local Version = require(script.Parent.Version)
|
||||
|
||||
local validateApiInfo = Types.ifEnabled(Types.ApiInfoResponse)
|
||||
local validateApiRead = Types.ifEnabled(Types.ApiReadResponse)
|
||||
local validateApiSubscribe = Types.ifEnabled(Types.ApiSubscribeResponse)
|
||||
local validateApiSocketPacket = Types.ifEnabled(Types.ApiSocketPacket)
|
||||
local validateApiSerialize = Types.ifEnabled(Types.ApiSerializeResponse)
|
||||
local validateApiRefPatch = Types.ifEnabled(Types.ApiRefPatchResponse)
|
||||
|
||||
@@ -99,6 +100,7 @@ function ApiContext.new(baseUrl)
|
||||
__baseUrl = baseUrl,
|
||||
__sessionId = nil,
|
||||
__messageCursor = -1,
|
||||
__wsClient = nil,
|
||||
__connected = true,
|
||||
__activeRequests = {},
|
||||
}
|
||||
@@ -126,6 +128,12 @@ function ApiContext:disconnect()
|
||||
request:cancel()
|
||||
end
|
||||
self.__activeRequests = {}
|
||||
|
||||
if self.__wsClient then
|
||||
Log.trace("Closing WebSocket client")
|
||||
self.__wsClient:Close()
|
||||
end
|
||||
self.__wsClient = nil
|
||||
end
|
||||
|
||||
function ApiContext:setMessageCursor(index)
|
||||
@@ -207,38 +215,65 @@ function ApiContext:write(patch)
|
||||
end)
|
||||
end
|
||||
|
||||
function ApiContext:retrieveMessages()
|
||||
local url = ("%s/api/subscribe/%s"):format(self.__baseUrl, self.__messageCursor)
|
||||
function ApiContext:connectWebSocket(packetHandlers)
|
||||
local url = ("%s/api/socket/%s"):format(self.__baseUrl, self.__messageCursor)
|
||||
-- Convert HTTP/HTTPS URL to WS/WSS
|
||||
url = url:gsub("^http://", "ws://"):gsub("^https://", "wss://")
|
||||
|
||||
local function sendRequest()
|
||||
local request = Http.get(url):catch(function(err)
|
||||
if err.type == Http.Error.Kind.Timeout and self.__connected then
|
||||
return sendRequest()
|
||||
return Promise.new(function(resolve, reject)
|
||||
local success, wsClient =
|
||||
pcall(HttpService.CreateWebStreamClient, HttpService, Enum.WebStreamClientType.WebSocket, {
|
||||
Url = url,
|
||||
})
|
||||
if not success then
|
||||
reject("Failed to create WebSocket client: " .. tostring(wsClient))
|
||||
return
|
||||
end
|
||||
self.__wsClient = wsClient
|
||||
|
||||
local closed, errored, received
|
||||
|
||||
received = self.__wsClient.MessageReceived:Connect(function(msg)
|
||||
local data = Http.jsonDecode(msg)
|
||||
if data.sessionId ~= self.__sessionId then
|
||||
Log.warn("Received message with wrong session ID; ignoring")
|
||||
return
|
||||
end
|
||||
|
||||
return Promise.reject(err)
|
||||
assert(validateApiSocketPacket(data))
|
||||
|
||||
Log.trace("Received websocket packet: {:#?}", data)
|
||||
|
||||
local handler = packetHandlers[data.packetType]
|
||||
if handler then
|
||||
local ok, err = pcall(handler, data.body)
|
||||
if not ok then
|
||||
Log.error("Error in WebSocket packet handler for type '%s': %s", data.packetType, err)
|
||||
end
|
||||
else
|
||||
Log.warn("No handler for WebSocket packet type '%s'", data.packetType)
|
||||
end
|
||||
end)
|
||||
|
||||
Log.trace("Tracking request {}", request)
|
||||
self.__activeRequests[request] = true
|
||||
closed = self.__wsClient.Closed:Connect(function()
|
||||
closed:Disconnect()
|
||||
errored:Disconnect()
|
||||
received:Disconnect()
|
||||
|
||||
return request:finally(function(...)
|
||||
Log.trace("Cleaning up request {}", request)
|
||||
self.__activeRequests[request] = nil
|
||||
return ...
|
||||
if self.__connected then
|
||||
reject("WebSocket connection closed unexpectedly")
|
||||
else
|
||||
resolve()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
return sendRequest():andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body)
|
||||
if body.sessionId ~= self.__sessionId then
|
||||
return Promise.reject("Server changed ID")
|
||||
end
|
||||
errored = self.__wsClient.Error:Connect(function(code, msg)
|
||||
closed:Disconnect()
|
||||
errored:Disconnect()
|
||||
received:Disconnect()
|
||||
|
||||
assert(validateApiSubscribe(body))
|
||||
|
||||
self:setMessageCursor(body.messageCursor)
|
||||
|
||||
return body.messages
|
||||
reject("WebSocket error: " .. code .. " - " .. msg)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
@@ -174,6 +174,8 @@ function App:init()
|
||||
end
|
||||
|
||||
function App:willUnmount()
|
||||
self:endSession()
|
||||
|
||||
self.waypointConnection:Disconnect()
|
||||
self.confirmationBindable:Destroy()
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ return strict("Config", {
|
||||
codename = "Epiphany",
|
||||
version = realVersion,
|
||||
expectedServerVersionString = ("%d.%d or newer"):format(realVersion[1], realVersion[2]),
|
||||
protocolVersion = 4,
|
||||
protocolVersion = 5,
|
||||
defaultHost = "localhost",
|
||||
defaultPort = "34872",
|
||||
})
|
||||
|
||||
@@ -201,7 +201,20 @@ function ServeSession:start()
|
||||
self:__setStatus(Status.Connected, serverInfo.projectName)
|
||||
self:__applyGameAndPlaceId(serverInfo)
|
||||
|
||||
return self:__mainSyncLoop()
|
||||
return self.__apiContext:connectWebSocket({
|
||||
["messages"] = function(messagesPacket)
|
||||
if self.__status == Status.Disconnected then
|
||||
return
|
||||
end
|
||||
|
||||
Log.debug("Received {} messages from Rojo server", #messagesPacket.messages)
|
||||
|
||||
for _, message in messagesPacket.messages do
|
||||
self:__applyPatch(message)
|
||||
end
|
||||
self.__apiContext:setMessageCursor(messagesPacket.messageCursor)
|
||||
end,
|
||||
})
|
||||
end)
|
||||
end)
|
||||
:catch(function(err)
|
||||
@@ -536,40 +549,6 @@ function ServeSession:__initialSync(serverInfo)
|
||||
end)
|
||||
end
|
||||
|
||||
function ServeSession:__mainSyncLoop()
|
||||
return Promise.new(function(resolve, reject)
|
||||
while self.__status == Status.Connected do
|
||||
local success, result = self.__apiContext
|
||||
:retrieveMessages()
|
||||
:andThen(function(messages)
|
||||
if self.__status == Status.Disconnected then
|
||||
-- In the time it took to retrieve messages, we disconnected
|
||||
-- so we just resolve immediately without patching anything
|
||||
return
|
||||
end
|
||||
|
||||
Log.trace("Serve session {} retrieved {} messages", tostring(self), #messages)
|
||||
|
||||
for _, message in messages do
|
||||
self:__applyPatch(message)
|
||||
end
|
||||
end)
|
||||
:await()
|
||||
|
||||
if self.__status == Status.Disconnected then
|
||||
-- If we are no longer connected after applying, we stop silently
|
||||
-- without checking for errors as they are no longer relevant
|
||||
break
|
||||
elseif success == false then
|
||||
reject(result)
|
||||
end
|
||||
end
|
||||
|
||||
-- We are no longer connected, so we resolve the promise
|
||||
resolve()
|
||||
end)
|
||||
end
|
||||
|
||||
function ServeSession:__stopInternal(err)
|
||||
self:__setStatus(Status.Disconnected, err)
|
||||
self.__apiContext:disconnect()
|
||||
|
||||
@@ -49,12 +49,21 @@ local ApiReadResponse = t.interface({
|
||||
instances = t.map(RbxId, ApiInstance),
|
||||
})
|
||||
|
||||
local ApiSubscribeResponse = t.interface({
|
||||
sessionId = t.string,
|
||||
local SocketPacketType = t.union(t.literal("messages"))
|
||||
|
||||
local MessagesPacket = t.interface({
|
||||
messageCursor = t.number,
|
||||
messages = t.array(ApiSubscribeMessage),
|
||||
})
|
||||
|
||||
local SocketPacketBody = t.union(MessagesPacket)
|
||||
|
||||
local ApiSocketPacket = t.interface({
|
||||
sessionId = t.string,
|
||||
packetType = SocketPacketType,
|
||||
body = SocketPacketBody,
|
||||
})
|
||||
|
||||
local ApiSerializeResponse = t.interface({
|
||||
sessionId = t.string,
|
||||
modelContents = t.buffer,
|
||||
@@ -85,7 +94,7 @@ return strict("Types", {
|
||||
|
||||
ApiInfoResponse = ApiInfoResponse,
|
||||
ApiReadResponse = ApiReadResponse,
|
||||
ApiSubscribeResponse = ApiSubscribeResponse,
|
||||
ApiSocketPacket = ApiSocketPacket,
|
||||
ApiError = ApiError,
|
||||
|
||||
ApiInstance = ApiInstance,
|
||||
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: add_folder
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
||||
expression: "socket_packet.intern_and_redact(&mut redactions, ())"
|
||||
---
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added:
|
||||
id-3:
|
||||
Children: []
|
||||
ClassName: Folder
|
||||
Id: id-3
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: my-new-folder
|
||||
Parent: id-2
|
||||
Properties: {}
|
||||
removed: []
|
||||
updated: []
|
||||
body:
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added:
|
||||
id-3:
|
||||
Children: []
|
||||
ClassName: Folder
|
||||
Id: id-3
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: my-new-folder
|
||||
Parent: id-2
|
||||
Properties: {}
|
||||
removed: []
|
||||
updated: []
|
||||
packetType: messages
|
||||
sessionId: id-1
|
||||
|
||||
@@ -7,7 +7,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: optional
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: edit_init
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
||||
|
||||
expression: "socket_packet.intern_and_redact(&mut redactions, ())"
|
||||
---
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added: {}
|
||||
removed: []
|
||||
updated:
|
||||
- changedClassName: ~
|
||||
changedMetadata: ~
|
||||
changedName: ~
|
||||
changedProperties:
|
||||
Source:
|
||||
String: "-- Edited contents"
|
||||
id: id-2
|
||||
body:
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added: {}
|
||||
removed: []
|
||||
updated:
|
||||
- changedClassName: ~
|
||||
changedMetadata: ~
|
||||
changedName: ~
|
||||
changedProperties:
|
||||
Source:
|
||||
String: "-- Edited contents"
|
||||
id: id-2
|
||||
packetType: messages
|
||||
sessionId: id-1
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: empty
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: empty_folder
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
||||
expression: "socket_packet.intern_and_redact(&mut redactions, ())"
|
||||
---
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added:
|
||||
id-3:
|
||||
Children: []
|
||||
ClassName: Model
|
||||
Id: id-3
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: test
|
||||
Parent: id-2
|
||||
Properties:
|
||||
NeedsPivotMigration:
|
||||
Bool: false
|
||||
removed: []
|
||||
updated: []
|
||||
body:
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added:
|
||||
id-3:
|
||||
Children: []
|
||||
ClassName: Model
|
||||
Id: id-3
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: test
|
||||
Parent: id-2
|
||||
Properties:
|
||||
NeedsPivotMigration:
|
||||
Bool: false
|
||||
removed: []
|
||||
updated: []
|
||||
packetType: messages
|
||||
sessionId: id-1
|
||||
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: forced_parent
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: meshpart
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
||||
expression: "socket_packet.intern_and_redact(&mut redactions, ())"
|
||||
---
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added:
|
||||
id-6:
|
||||
Children: []
|
||||
ClassName: Actor
|
||||
Id: id-6
|
||||
Metadata:
|
||||
ignoreUnknownInstances: true
|
||||
Name: Actor
|
||||
Parent: id-3
|
||||
Properties:
|
||||
NeedsPivotMigration:
|
||||
Bool: false
|
||||
removed: []
|
||||
updated:
|
||||
- changedClassName: ~
|
||||
changedMetadata:
|
||||
ignoreUnknownInstances: true
|
||||
changedName: ~
|
||||
changedProperties: {}
|
||||
id: id-3
|
||||
body:
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added:
|
||||
id-6:
|
||||
Children: []
|
||||
ClassName: Actor
|
||||
Id: id-6
|
||||
Metadata:
|
||||
ignoreUnknownInstances: true
|
||||
Name: Actor
|
||||
Parent: id-3
|
||||
Properties:
|
||||
NeedsPivotMigration:
|
||||
Bool: false
|
||||
removed: []
|
||||
updated:
|
||||
- changedClassName: ~
|
||||
changedMetadata:
|
||||
ignoreUnknownInstances: true
|
||||
changedName: ~
|
||||
changedProperties: {}
|
||||
id: id-3
|
||||
packetType: messages
|
||||
sessionId: id-1
|
||||
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: move_folder_of_stuff
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -1,141 +1,141 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
||||
|
||||
expression: "socket_packet.intern_and_redact(&mut redactions, ())"
|
||||
---
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added:
|
||||
id-10:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
Id: id-10
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: "6"
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
String: "File #6"
|
||||
id-11:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
Id: id-11
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: "7"
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
String: "File #7"
|
||||
id-12:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
Id: id-12
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: "8"
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
String: "File #8"
|
||||
id-13:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
Id: id-13
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: "9"
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
String: "File #9"
|
||||
id-3:
|
||||
Children:
|
||||
- id-4
|
||||
- id-5
|
||||
- id-6
|
||||
- id-7
|
||||
- id-8
|
||||
- id-9
|
||||
- id-10
|
||||
- id-11
|
||||
- id-12
|
||||
- id-13
|
||||
ClassName: Folder
|
||||
Id: id-3
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: new-stuff
|
||||
Parent: id-2
|
||||
Properties: {}
|
||||
id-4:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
Id: id-4
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: "0"
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
String: "File #0"
|
||||
id-5:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
Id: id-5
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: "1"
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
String: "File #1"
|
||||
id-6:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
Id: id-6
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: "2"
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
String: "File #2"
|
||||
id-7:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
Id: id-7
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: "3"
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
String: "File #3"
|
||||
id-8:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
Id: id-8
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: "4"
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
String: "File #4"
|
||||
id-9:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
Id: id-9
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: "5"
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
String: "File #5"
|
||||
removed: []
|
||||
updated: []
|
||||
body:
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added:
|
||||
id-10:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
Id: id-10
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: "6"
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
String: "File #6"
|
||||
id-11:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
Id: id-11
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: "7"
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
String: "File #7"
|
||||
id-12:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
Id: id-12
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: "8"
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
String: "File #8"
|
||||
id-13:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
Id: id-13
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: "9"
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
String: "File #9"
|
||||
id-3:
|
||||
Children:
|
||||
- id-4
|
||||
- id-5
|
||||
- id-6
|
||||
- id-7
|
||||
- id-8
|
||||
- id-9
|
||||
- id-10
|
||||
- id-11
|
||||
- id-12
|
||||
- id-13
|
||||
ClassName: Folder
|
||||
Id: id-3
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: new-stuff
|
||||
Parent: id-2
|
||||
Properties: {}
|
||||
id-4:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
Id: id-4
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: "0"
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
String: "File #0"
|
||||
id-5:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
Id: id-5
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: "1"
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
String: "File #1"
|
||||
id-6:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
Id: id-6
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: "2"
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
String: "File #2"
|
||||
id-7:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
Id: id-7
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: "3"
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
String: "File #3"
|
||||
id-8:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
Id: id-8
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: "4"
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
String: "File #4"
|
||||
id-9:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
Id: id-9
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: "5"
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
String: "File #5"
|
||||
removed: []
|
||||
updated: []
|
||||
packetType: messages
|
||||
sessionId: id-1
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: top-level
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: no_name_project
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: no_name_top_level_project
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: pivot_migration
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: ref_properties
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: ref_properties
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
||||
expression: "socket_packet.intern_and_redact(&mut redactions, ())"
|
||||
---
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added: {}
|
||||
removed: []
|
||||
updated:
|
||||
- changedClassName: ~
|
||||
changedMetadata: ~
|
||||
changedName: ~
|
||||
changedProperties:
|
||||
Scale:
|
||||
Float32: 1
|
||||
id: id-8
|
||||
body:
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added: {}
|
||||
removed: []
|
||||
updated:
|
||||
- changedClassName: ~
|
||||
changedMetadata: ~
|
||||
changedName: ~
|
||||
changedProperties:
|
||||
Scale:
|
||||
Float32: 1
|
||||
id: id-8
|
||||
packetType: messages
|
||||
sessionId: id-1
|
||||
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: ref_properties_remove
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
||||
expression: "socket_packet.intern_and_redact(&mut redactions, ())"
|
||||
---
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added: {}
|
||||
removed:
|
||||
- id-4
|
||||
updated: []
|
||||
body:
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added: {}
|
||||
removed:
|
||||
- id-4
|
||||
updated: []
|
||||
packetType: messages
|
||||
sessionId: id-1
|
||||
|
||||
|
||||
@@ -1,47 +1,49 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
||||
expression: "socket_packet.intern_and_redact(&mut redactions, ())"
|
||||
---
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added:
|
||||
id-11:
|
||||
Children: []
|
||||
ClassName: Model
|
||||
Id: id-11
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: ProjectPointer
|
||||
Parent: id-7
|
||||
Properties:
|
||||
Attributes:
|
||||
body:
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added:
|
||||
id-11:
|
||||
Children: []
|
||||
ClassName: Model
|
||||
Id: id-11
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: ProjectPointer
|
||||
Parent: id-7
|
||||
Properties:
|
||||
Attributes:
|
||||
Rojo_Target_PrimaryPart:
|
||||
String: project target
|
||||
NeedsPivotMigration:
|
||||
Bool: false
|
||||
PrimaryPart:
|
||||
Ref: id-9
|
||||
removed: []
|
||||
updated:
|
||||
- changedClassName: ~
|
||||
changedMetadata:
|
||||
ignoreUnknownInstances: false
|
||||
changedName: ~
|
||||
changedProperties:
|
||||
Attributes:
|
||||
Attributes:
|
||||
Rojo_Target_PrimaryPart:
|
||||
String: project target
|
||||
NeedsPivotMigration:
|
||||
Bool: false
|
||||
PrimaryPart:
|
||||
Ref: id-9
|
||||
removed: []
|
||||
updated:
|
||||
- changedClassName: ~
|
||||
changedMetadata:
|
||||
ignoreUnknownInstances: false
|
||||
changedName: ~
|
||||
changedProperties:
|
||||
Attributes:
|
||||
Rojo_Id:
|
||||
String: model target 2
|
||||
id: id-7
|
||||
- changedClassName: ~
|
||||
changedMetadata: ~
|
||||
changedName: ~
|
||||
changedProperties:
|
||||
Attributes:
|
||||
Attributes:
|
||||
Rojo_Id:
|
||||
String: model target 2
|
||||
id: id-7
|
||||
- changedClassName: ~
|
||||
changedMetadata: ~
|
||||
changedName: ~
|
||||
changedProperties:
|
||||
Attributes:
|
||||
Rojo_Target_PrimaryPart:
|
||||
String: model target 2
|
||||
PrimaryPart: ~
|
||||
id: id-8
|
||||
Attributes:
|
||||
Rojo_Target_PrimaryPart:
|
||||
String: model target 2
|
||||
PrimaryPart: ~
|
||||
id: id-8
|
||||
packetType: messages
|
||||
sessionId: id-1
|
||||
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: remove_file
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
||||
expression: "socket_packet.intern_and_redact(&mut redactions, ())"
|
||||
---
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added: {}
|
||||
removed:
|
||||
- id-3
|
||||
updated: []
|
||||
body:
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added: {}
|
||||
removed:
|
||||
- id-3
|
||||
updated: []
|
||||
packetType: messages
|
||||
sessionId: id-1
|
||||
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: scripts
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
||||
|
||||
expression: "socket_packet.intern_and_redact(&mut redactions, ())"
|
||||
---
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added: {}
|
||||
removed: []
|
||||
updated:
|
||||
- changedClassName: ~
|
||||
changedMetadata: ~
|
||||
changedName: ~
|
||||
changedProperties:
|
||||
Source:
|
||||
String: Updated foo!
|
||||
id: id-4
|
||||
body:
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added: {}
|
||||
removed: []
|
||||
updated:
|
||||
- changedClassName: ~
|
||||
changedMetadata: ~
|
||||
changedName: ~
|
||||
changedProperties:
|
||||
Source:
|
||||
String: Updated foo!
|
||||
id: id-4
|
||||
packetType: messages
|
||||
sessionId: id-1
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: sync_rule_alone
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: sync_rule_complex
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: sync_rule_no_extension
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: sync_rule_no_name_project
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
188
src/web/api.rs
188
src/web/api.rs
@@ -9,7 +9,9 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use futures::{sink::SinkExt, stream::StreamExt};
|
||||
use hyper::{body, Body, Method, Request, Response, StatusCode};
|
||||
use hyper_tungstenite::{is_upgrade_request, tungstenite::Message, upgrade, HyperWebsocket};
|
||||
use opener::OpenError;
|
||||
use rbx_dom_weak::{
|
||||
types::{Ref, Variant},
|
||||
@@ -22,16 +24,16 @@ use crate::{
|
||||
snapshot::{InstanceWithMeta, PatchSet, PatchUpdate},
|
||||
web::{
|
||||
interface::{
|
||||
ErrorResponse, Instance, OpenResponse, ReadResponse, ServerInfoResponse,
|
||||
SubscribeMessage, SubscribeResponse, WriteRequest, WriteResponse, PROTOCOL_VERSION,
|
||||
SERVER_VERSION,
|
||||
ErrorResponse, Instance, MessagesPacket, OpenResponse, ReadResponse,
|
||||
ServerInfoResponse, SocketPacket, SocketPacketBody, SocketPacketType, SubscribeMessage,
|
||||
WriteRequest, WriteResponse, PROTOCOL_VERSION, SERVER_VERSION,
|
||||
},
|
||||
util::{json, json_ok},
|
||||
},
|
||||
web_api::{BufferEncode, InstanceUpdate, RefPatchResponse, SerializeResponse},
|
||||
};
|
||||
|
||||
pub async fn call(serve_session: Arc<ServeSession>, request: Request<Body>) -> Response<Body> {
|
||||
pub async fn call(serve_session: Arc<ServeSession>, mut request: Request<Body>) -> Response<Body> {
|
||||
let service = ApiService::new(serve_session);
|
||||
|
||||
match (request.method(), request.uri().path()) {
|
||||
@@ -39,8 +41,17 @@ pub async fn call(serve_session: Arc<ServeSession>, request: Request<Body>) -> R
|
||||
(&Method::GET, path) if path.starts_with("/api/read/") => {
|
||||
service.handle_api_read(request).await
|
||||
}
|
||||
(&Method::GET, path) if path.starts_with("/api/subscribe/") => {
|
||||
service.handle_api_subscribe(request).await
|
||||
(&Method::GET, path) if path.starts_with("/api/socket/") => {
|
||||
if is_upgrade_request(&request) {
|
||||
service.handle_api_socket(&mut request).await
|
||||
} else {
|
||||
json(
|
||||
ErrorResponse::bad_request(
|
||||
"/api/socket must be called as a websocket upgrade request",
|
||||
),
|
||||
StatusCode::BAD_REQUEST,
|
||||
)
|
||||
}
|
||||
}
|
||||
(&Method::GET, path) if path.starts_with("/api/serialize/") => {
|
||||
service.handle_api_serialize(request).await
|
||||
@@ -88,10 +99,9 @@ impl ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
/// Retrieve any messages past the given cursor index, and if
|
||||
/// there weren't any, subscribe to receive any new messages.
|
||||
async fn handle_api_subscribe(&self, request: Request<Body>) -> Response<Body> {
|
||||
let argument = &request.uri().path()["/api/subscribe/".len()..];
|
||||
/// Handle WebSocket upgrade for real-time message streaming
|
||||
async fn handle_api_socket(&self, request: &mut Request<Body>) -> Response<Body> {
|
||||
let argument = &request.uri().path()["/api/socket/".len()..];
|
||||
let input_cursor: u32 = match argument.parse() {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
@@ -102,36 +112,29 @@ impl ApiService {
|
||||
}
|
||||
};
|
||||
|
||||
let session_id = self.serve_session.session_id();
|
||||
|
||||
let result = self
|
||||
.serve_session
|
||||
.message_queue()
|
||||
.subscribe(input_cursor)
|
||||
.await;
|
||||
|
||||
let tree_handle = self.serve_session.tree_handle();
|
||||
|
||||
match result {
|
||||
Ok((message_cursor, messages)) => {
|
||||
let tree = tree_handle.lock().unwrap();
|
||||
|
||||
let api_messages = messages
|
||||
.into_iter()
|
||||
.map(|patch| SubscribeMessage::from_patch_update(&tree, patch))
|
||||
.collect();
|
||||
|
||||
json_ok(SubscribeResponse {
|
||||
session_id,
|
||||
message_cursor,
|
||||
messages: api_messages,
|
||||
})
|
||||
// Upgrade the connection to WebSocket
|
||||
let (response, websocket) = match upgrade(request, None) {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
return json(
|
||||
ErrorResponse::internal_error(format!("WebSocket upgrade failed: {}", err)),
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
Err(_) => json(
|
||||
ErrorResponse::internal_error("Message queue disconnected sender"),
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
),
|
||||
}
|
||||
};
|
||||
|
||||
let serve_session = Arc::clone(&self.serve_session);
|
||||
|
||||
// Spawn a task to handle the WebSocket connection
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) =
|
||||
handle_websocket_subscription(serve_session, websocket, input_cursor).await
|
||||
{
|
||||
log::error!("Error in websocket subscription: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
response
|
||||
}
|
||||
|
||||
async fn handle_api_write(&self, request: Request<Body>) -> Response<Body> {
|
||||
@@ -444,6 +447,113 @@ fn pick_script_path(instance: InstanceWithMeta<'_>) -> Option<PathBuf> {
|
||||
.map(|path| path.to_owned())
|
||||
}
|
||||
|
||||
/// Handle WebSocket connection for streaming subscription messages
|
||||
async fn handle_websocket_subscription(
|
||||
serve_session: Arc<ServeSession>,
|
||||
websocket: HyperWebsocket,
|
||||
input_cursor: u32,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut websocket = websocket.await?;
|
||||
|
||||
let session_id = serve_session.session_id();
|
||||
let tree_handle = serve_session.tree_handle();
|
||||
let message_queue = serve_session.message_queue();
|
||||
|
||||
log::debug!(
|
||||
"WebSocket subscription established for session {}",
|
||||
session_id
|
||||
);
|
||||
|
||||
// Now continuously listen for new messages using select to handle both incoming messages
|
||||
// and WebSocket control messages concurrently
|
||||
let mut cursor = input_cursor;
|
||||
loop {
|
||||
let receiver = message_queue.subscribe(cursor);
|
||||
|
||||
tokio::select! {
|
||||
// Handle new messages from the message queue
|
||||
result = receiver => {
|
||||
match result {
|
||||
Ok((new_cursor, messages)) => {
|
||||
if !messages.is_empty() {
|
||||
let json_message = {
|
||||
let tree = tree_handle.lock().unwrap();
|
||||
let api_messages = messages
|
||||
.into_iter()
|
||||
.map(|patch| SubscribeMessage::from_patch_update(&tree, patch))
|
||||
.collect();
|
||||
|
||||
let response = SocketPacket {
|
||||
session_id,
|
||||
packet_type: SocketPacketType::Messages,
|
||||
body: SocketPacketBody::Messages(MessagesPacket {
|
||||
message_cursor: new_cursor,
|
||||
messages: api_messages,
|
||||
}),
|
||||
};
|
||||
|
||||
serde_json::to_string(&response)?
|
||||
};
|
||||
|
||||
log::debug!("Sending batch of messages over WebSocket subscription");
|
||||
|
||||
if websocket.send(Message::Text(json_message)).await.is_err() {
|
||||
// Client disconnected
|
||||
log::debug!("WebSocket subscription closed by client");
|
||||
break;
|
||||
}
|
||||
cursor = new_cursor;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// Message queue disconnected
|
||||
log::debug!("Message queue disconnected; closing WebSocket subscription");
|
||||
let _ = websocket.send(Message::Close(None)).await;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle incoming WebSocket messages (ping/pong/close)
|
||||
msg = websocket.next() => {
|
||||
match msg {
|
||||
Some(Ok(Message::Close(_))) => {
|
||||
log::debug!("WebSocket subscription closed by client");
|
||||
break;
|
||||
}
|
||||
Some(Ok(Message::Ping(data))) => {
|
||||
// tungstenite handles pong automatically
|
||||
log::debug!("Received ping: {:?}", data);
|
||||
}
|
||||
Some(Ok(Message::Pong(data))) => {
|
||||
log::debug!("Received pong: {:?}", data);
|
||||
}
|
||||
Some(Ok(Message::Text(_))) | Some(Ok(Message::Binary(_))) => {
|
||||
// Ignore text/binary messages from client for subscription endpoint
|
||||
// TODO: Use this for bidirectional sync or requesting fallbacks?
|
||||
log::debug!("Ignoring message from client since we don't use it for anything yet: {:?}", msg);
|
||||
}
|
||||
Some(Ok(Message::Frame(_))) => {
|
||||
// This should never happen according to tungstenite docs
|
||||
unreachable!();
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
log::error!("WebSocket error: {}", e);
|
||||
break;
|
||||
}
|
||||
None => {
|
||||
// WebSocket stream ended
|
||||
log::debug!("WebSocket stream ended");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Certain Instances MUST be a child of specific classes. This function
|
||||
/// tracks that information for the Serialize endpoint.
|
||||
///
|
||||
|
||||
@@ -12,6 +12,7 @@ use rbx_dom_weak::{
|
||||
Ustr, UstrMap,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::Display;
|
||||
|
||||
use crate::{
|
||||
session_id::SessionId,
|
||||
@@ -24,7 +25,7 @@ use crate::{
|
||||
pub(crate) const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
/// Current protocol version, which is required to match.
|
||||
pub const PROTOCOL_VERSION: u64 = 4;
|
||||
pub const PROTOCOL_VERSION: u64 = 5;
|
||||
|
||||
/// Message returned by Rojo API when a change has occurred.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -192,15 +193,44 @@ pub struct WriteResponse {
|
||||
pub session_id: SessionId,
|
||||
}
|
||||
|
||||
/// Response body from /api/subscribe/{cursor}
|
||||
/// Packet type enum for different websocket message types
|
||||
#[derive(Debug, Serialize, Deserialize, Display, Eq, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[strum(serialize_all = "camelCase")]
|
||||
pub enum SocketPacketType {
|
||||
Messages,
|
||||
// TODO: Can we cleanly use the socket for all communication?
|
||||
// Serialize,
|
||||
// RefPatch,
|
||||
}
|
||||
|
||||
/// Body content for messages packet type
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SubscribeResponse<'a> {
|
||||
pub session_id: SessionId,
|
||||
pub struct MessagesPacket<'a> {
|
||||
pub message_cursor: u32,
|
||||
pub messages: Vec<SubscribeMessage<'a>>,
|
||||
}
|
||||
|
||||
/// Body content for different packet types
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum SocketPacketBody<'a> {
|
||||
Messages(MessagesPacket<'a>),
|
||||
// TODO: Can we cleanly use the socket for all communication?
|
||||
// Serialize(SerializePacket),
|
||||
// RefPatch(RefPatchPacket<'a>),
|
||||
}
|
||||
|
||||
/// Message content from /api/socket
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SocketPacket<'a> {
|
||||
pub session_id: SessionId,
|
||||
pub packet_type: SocketPacketType,
|
||||
pub body: SocketPacketBody<'a>,
|
||||
}
|
||||
|
||||
/// Response body from /api/open/{id}
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
||||
@@ -3,7 +3,9 @@ use std::collections::HashMap;
|
||||
use rbx_dom_weak::types::Ref;
|
||||
use serde::Serialize;
|
||||
|
||||
use librojo::web_api::{Instance, InstanceUpdate, ReadResponse, SubscribeResponse};
|
||||
use librojo::web_api::{
|
||||
Instance, InstanceUpdate, MessagesPacket, ReadResponse, SocketPacket, SocketPacketBody,
|
||||
};
|
||||
use rojo_insta_ext::RedactionMap;
|
||||
|
||||
/// A convenience method to store all of the redactable data from a piece of
|
||||
@@ -54,7 +56,16 @@ impl<'a> Internable<&'a HashMap<Ref, Instance<'_>>> for Instance<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl Internable<()> for SubscribeResponse<'_> {
|
||||
impl Internable<()> for SocketPacket<'_> {
|
||||
fn intern(&self, redactions: &mut RedactionMap, extra: ()) {
|
||||
redactions.intern(&self.session_id);
|
||||
match &self.body {
|
||||
SocketPacketBody::Messages(packet) => packet.intern(redactions, extra),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Internable<()> for MessagesPacket<'_> {
|
||||
fn intern(&self, redactions: &mut RedactionMap, _extra: ()) {
|
||||
for message in &self.messages {
|
||||
intern_instance_updates(redactions, &message.updated);
|
||||
|
||||
@@ -8,11 +8,14 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use hyper_tungstenite::tungstenite::{connect, Message};
|
||||
use rbx_dom_weak::types::Ref;
|
||||
|
||||
use tempfile::{tempdir, TempDir};
|
||||
|
||||
use librojo::web_api::{ReadResponse, SerializeResponse, ServerInfoResponse, SubscribeResponse};
|
||||
use librojo::web_api::{
|
||||
ReadResponse, SerializeResponse, ServerInfoResponse, SocketPacket, SocketPacketType,
|
||||
};
|
||||
use rojo_insta_ext::RedactionMap;
|
||||
|
||||
use crate::rojo_test::io_util::{
|
||||
@@ -173,13 +176,54 @@ impl TestServeSession {
|
||||
Ok(serde_json::from_value(value).expect("Server returned malformed response"))
|
||||
}
|
||||
|
||||
pub fn get_api_subscribe(
|
||||
pub fn get_api_socket_packet(
|
||||
&self,
|
||||
packet_type: SocketPacketType,
|
||||
cursor: u32,
|
||||
) -> Result<SubscribeResponse<'static>, reqwest::Error> {
|
||||
let url = format!("http://localhost:{}/api/subscribe/{}", self.port, cursor);
|
||||
) -> Result<SocketPacket<'static>, Box<dyn std::error::Error>> {
|
||||
let url = format!("ws://localhost:{}/api/socket/{}", self.port, cursor);
|
||||
|
||||
reqwest::blocking::get(url)?.json()
|
||||
let (mut socket, _response) = connect(url)?;
|
||||
|
||||
// Wait for messages with a timeout
|
||||
let timeout = Duration::from_secs(10);
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
loop {
|
||||
if start.elapsed() > timeout {
|
||||
return Err("Timeout waiting for packet from WebSocket".into());
|
||||
}
|
||||
|
||||
match socket.read() {
|
||||
Ok(Message::Text(text)) => {
|
||||
let packet: SocketPacket = serde_json::from_str(&text)?;
|
||||
if packet.packet_type != packet_type {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Close the WebSocket connection now that we got what we were waiting for
|
||||
let _ = socket.close(None);
|
||||
return Ok(packet);
|
||||
}
|
||||
Ok(Message::Close(_)) => {
|
||||
return Err("WebSocket closed before receiving messages".into());
|
||||
}
|
||||
Ok(_) => {
|
||||
// Ignore other message types (ping, pong, binary)
|
||||
continue;
|
||||
}
|
||||
Err(hyper_tungstenite::tungstenite::Error::Io(e))
|
||||
if e.kind() == std::io::ErrorKind::WouldBlock =>
|
||||
{
|
||||
// No data available yet, sleep a bit and try again
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(e.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_api_serialize(&self, ids: &[Ref]) -> Result<SerializeResponse, reqwest::Error> {
|
||||
|
||||
@@ -8,6 +8,8 @@ use crate::rojo_test::{
|
||||
serve_util::{run_serve_test, serialize_to_xml_model},
|
||||
};
|
||||
|
||||
use librojo::web_api::SocketPacketType;
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
run_serve_test("empty", |session, mut redactions| {
|
||||
@@ -42,10 +44,12 @@ fn scripts() {
|
||||
|
||||
fs::write(session.path().join("src/foo.lua"), "Updated foo!").unwrap();
|
||||
|
||||
let subscribe_response = session.get_api_subscribe(0).unwrap();
|
||||
let socket_packet = session
|
||||
.get_api_socket_packet(SocketPacketType::Messages, 0)
|
||||
.unwrap();
|
||||
assert_yaml_snapshot!(
|
||||
"scripts_subscribe",
|
||||
subscribe_response.intern_and_redact(&mut redactions, ())
|
||||
socket_packet.intern_and_redact(&mut redactions, ())
|
||||
);
|
||||
|
||||
let read_response = session.get_api_read(root_id).unwrap();
|
||||
@@ -74,10 +78,12 @@ fn add_folder() {
|
||||
|
||||
fs::create_dir(session.path().join("src/my-new-folder")).unwrap();
|
||||
|
||||
let subscribe_response = session.get_api_subscribe(0).unwrap();
|
||||
let socket_packet = session
|
||||
.get_api_socket_packet(SocketPacketType::Messages, 0)
|
||||
.unwrap();
|
||||
assert_yaml_snapshot!(
|
||||
"add_folder_subscribe",
|
||||
subscribe_response.intern_and_redact(&mut redactions, ())
|
||||
socket_packet.intern_and_redact(&mut redactions, ())
|
||||
);
|
||||
|
||||
let read_response = session.get_api_read(root_id).unwrap();
|
||||
@@ -104,10 +110,12 @@ fn remove_file() {
|
||||
|
||||
fs::remove_file(session.path().join("src/hello.txt")).unwrap();
|
||||
|
||||
let subscribe_response = session.get_api_subscribe(0).unwrap();
|
||||
let socket_packet = session
|
||||
.get_api_socket_packet(SocketPacketType::Messages, 0)
|
||||
.unwrap();
|
||||
assert_yaml_snapshot!(
|
||||
"remove_file_subscribe",
|
||||
subscribe_response.intern_and_redact(&mut redactions, ())
|
||||
socket_packet.intern_and_redact(&mut redactions, ())
|
||||
);
|
||||
|
||||
let read_response = session.get_api_read(root_id).unwrap();
|
||||
@@ -134,10 +142,12 @@ fn edit_init() {
|
||||
|
||||
fs::write(session.path().join("src/init.lua"), b"-- Edited contents").unwrap();
|
||||
|
||||
let subscribe_response = session.get_api_subscribe(0).unwrap();
|
||||
let socket_packet = session
|
||||
.get_api_socket_packet(SocketPacketType::Messages, 0)
|
||||
.unwrap();
|
||||
assert_yaml_snapshot!(
|
||||
"edit_init_subscribe",
|
||||
subscribe_response.intern_and_redact(&mut redactions, ())
|
||||
socket_packet.intern_and_redact(&mut redactions, ())
|
||||
);
|
||||
|
||||
let read_response = session.get_api_read(root_id).unwrap();
|
||||
@@ -180,10 +190,12 @@ fn move_folder_of_stuff() {
|
||||
// will fail otherwise.
|
||||
fs::rename(stuff_path, session.path().join("src/new-stuff")).unwrap();
|
||||
|
||||
let subscribe_response = session.get_api_subscribe(0).unwrap();
|
||||
let socket_packet = session
|
||||
.get_api_socket_packet(SocketPacketType::Messages, 0)
|
||||
.unwrap();
|
||||
assert_yaml_snapshot!(
|
||||
"move_folder_of_stuff_subscribe",
|
||||
subscribe_response.intern_and_redact(&mut redactions, ())
|
||||
socket_packet.intern_and_redact(&mut redactions, ())
|
||||
);
|
||||
|
||||
let read_response = session.get_api_read(root_id).unwrap();
|
||||
@@ -214,10 +226,12 @@ fn empty_json_model() {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let subscribe_response = session.get_api_subscribe(0).unwrap();
|
||||
let socket_packet = session
|
||||
.get_api_socket_packet(SocketPacketType::Messages, 0)
|
||||
.unwrap();
|
||||
assert_yaml_snapshot!(
|
||||
"empty_json_model_subscribe",
|
||||
subscribe_response.intern_and_redact(&mut redactions, ())
|
||||
socket_packet.intern_and_redact(&mut redactions, ())
|
||||
);
|
||||
|
||||
let read_response = session.get_api_read(root_id).unwrap();
|
||||
@@ -245,10 +259,12 @@ fn add_optional_folder() {
|
||||
|
||||
fs::create_dir(session.path().join("create-later")).unwrap();
|
||||
|
||||
let subscribe_response = session.get_api_subscribe(0).unwrap();
|
||||
let socket_packet = session
|
||||
.get_api_socket_packet(SocketPacketType::Messages, 0)
|
||||
.unwrap();
|
||||
assert_yaml_snapshot!(
|
||||
"add_optional_folder_subscribe",
|
||||
subscribe_response.intern_and_redact(&mut redactions, ())
|
||||
socket_packet.intern_and_redact(&mut redactions, ())
|
||||
);
|
||||
|
||||
let read_response = session.get_api_read(root_id).unwrap();
|
||||
@@ -437,10 +453,12 @@ fn ref_properties() {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let subscribe_response = session.get_api_subscribe(0).unwrap();
|
||||
let socket_packet = session
|
||||
.get_api_socket_packet(SocketPacketType::Messages, 0)
|
||||
.unwrap();
|
||||
assert_yaml_snapshot!(
|
||||
"ref_properties_subscribe",
|
||||
subscribe_response.intern_and_redact(&mut redactions, ())
|
||||
socket_packet.intern_and_redact(&mut redactions, ())
|
||||
);
|
||||
|
||||
let read_response = session.get_api_read(root_id).unwrap();
|
||||
@@ -467,10 +485,12 @@ fn ref_properties_remove() {
|
||||
|
||||
fs::remove_file(session.path().join("src/target.model.json")).unwrap();
|
||||
|
||||
let subscribe_response = session.get_api_subscribe(0).unwrap();
|
||||
let socket_packet = session
|
||||
.get_api_socket_packet(SocketPacketType::Messages, 0)
|
||||
.unwrap();
|
||||
assert_yaml_snapshot!(
|
||||
"ref_properties_remove_subscribe",
|
||||
subscribe_response.intern_and_redact(&mut redactions, ())
|
||||
socket_packet.intern_and_redact(&mut redactions, ())
|
||||
);
|
||||
|
||||
let read_response = session.get_api_read(root_id).unwrap();
|
||||
@@ -529,10 +549,12 @@ fn ref_properties_patch_update() {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let subscribe_response = session.get_api_subscribe(0).unwrap();
|
||||
let socket_packet = session
|
||||
.get_api_socket_packet(SocketPacketType::Messages, 0)
|
||||
.unwrap();
|
||||
assert_yaml_snapshot!(
|
||||
"ref_properties_patch_update_subscribe",
|
||||
subscribe_response.intern_and_redact(&mut redactions, ())
|
||||
socket_packet.intern_and_redact(&mut redactions, ())
|
||||
);
|
||||
|
||||
let read_response = session.get_api_read(root_id).unwrap();
|
||||
@@ -581,10 +603,12 @@ fn model_pivot_migration() {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let subscribe_response = session.get_api_subscribe(0).unwrap();
|
||||
let socket_packet = session
|
||||
.get_api_socket_packet(SocketPacketType::Messages, 0)
|
||||
.unwrap();
|
||||
assert_yaml_snapshot!(
|
||||
"model_pivot_migration_all",
|
||||
subscribe_response.intern_and_redact(&mut redactions, ())
|
||||
socket_packet.intern_and_redact(&mut redactions, ())
|
||||
);
|
||||
|
||||
let read_response = session.get_api_read(root_id).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user