Use WebSocket instead of Long Polling (#1142)

This commit is contained in:
boatbomber
2025-11-26 19:57:01 -08:00
committed by GitHub
parent a61a1bef55
commit 87f58e0a55
44 changed files with 1750 additions and 971 deletions

View File

@@ -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` - `syncUnscriptable` defaults to `true` instead of `false`
- `ignoreTrees` doesn't require the root of the project's name in it. - `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]) * 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]) * 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 [#937]: https://github.com/rojo-rbx/rojo/pull/937
[#994]: https://github.com/rojo-rbx/rojo/pull/994 [#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 [#1159]: https://github.com/rojo-rbx/rojo/pull/1159
[#1172]: https://github.com/rojo-rbx/rojo/pull/1172 [#1172]: https://github.com/rojo-rbx/rojo/pull/1172

1566
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -72,6 +72,7 @@ futures = "0.3.30"
globset = "0.4.14" globset = "0.4.14"
humantime = "2.1.0" humantime = "2.1.0"
hyper = { version = "0.14.28", features = ["server", "tcp", "http1"] } hyper = { version = "0.14.28", features = ["server", "tcp", "http1"] }
hyper-tungstenite = "0.11.0"
jod-thread = "0.1.2" jod-thread = "0.1.2"
log = "0.4.21" log = "0.4.21"
num_cpus = "1.16.0" num_cpus = "1.16.0"
@@ -87,10 +88,11 @@ roblox_install = "1.0.0"
serde = { version = "1.0.197", features = ["derive", "rc"] } serde = { version = "1.0.197", features = ["derive", "rc"] }
serde_json = "1.0.145" serde_json = "1.0.145"
jsonc-parser = { version = "0.27.0", features = ["serde"] } jsonc-parser = { version = "0.27.0", features = ["serde"] }
strum = { version = "0.27", features = ["derive"] }
toml = "0.5.11" toml = "0.5.11"
termcolor = "1.4.1" termcolor = "1.4.1"
thiserror = "1.0.57" 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"] } uuid = { version = "1.7.0", features = ["v4", "serde"] }
clap = { version = "3.2.25", features = ["derive"] } clap = { version = "3.2.25", features = ["derive"] }
profiling = "1.0.15" profiling = "1.0.15"

View File

@@ -1,4 +1,5 @@
local Packages = script.Parent.Parent.Packages local Packages = script.Parent.Parent.Packages
local HttpService = game:GetService("HttpService")
local Http = require(Packages.Http) local Http = require(Packages.Http)
local Log = require(Packages.Log) local Log = require(Packages.Log)
local Promise = require(Packages.Promise) local Promise = require(Packages.Promise)
@@ -9,7 +10,7 @@ local Version = require(script.Parent.Version)
local validateApiInfo = Types.ifEnabled(Types.ApiInfoResponse) local validateApiInfo = Types.ifEnabled(Types.ApiInfoResponse)
local validateApiRead = Types.ifEnabled(Types.ApiReadResponse) 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 validateApiSerialize = Types.ifEnabled(Types.ApiSerializeResponse)
local validateApiRefPatch = Types.ifEnabled(Types.ApiRefPatchResponse) local validateApiRefPatch = Types.ifEnabled(Types.ApiRefPatchResponse)
@@ -99,6 +100,7 @@ function ApiContext.new(baseUrl)
__baseUrl = baseUrl, __baseUrl = baseUrl,
__sessionId = nil, __sessionId = nil,
__messageCursor = -1, __messageCursor = -1,
__wsClient = nil,
__connected = true, __connected = true,
__activeRequests = {}, __activeRequests = {},
} }
@@ -126,6 +128,12 @@ function ApiContext:disconnect()
request:cancel() request:cancel()
end end
self.__activeRequests = {} self.__activeRequests = {}
if self.__wsClient then
Log.trace("Closing WebSocket client")
self.__wsClient:Close()
end
self.__wsClient = nil
end end
function ApiContext:setMessageCursor(index) function ApiContext:setMessageCursor(index)
@@ -207,38 +215,65 @@ function ApiContext:write(patch)
end) end)
end end
function ApiContext:retrieveMessages() function ApiContext:connectWebSocket(packetHandlers)
local url = ("%s/api/subscribe/%s"):format(self.__baseUrl, self.__messageCursor) 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() return Promise.new(function(resolve, reject)
local request = Http.get(url):catch(function(err) local success, wsClient =
if err.type == Http.Error.Kind.Timeout and self.__connected then pcall(HttpService.CreateWebStreamClient, HttpService, Enum.WebStreamClientType.WebSocket, {
return sendRequest() 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 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) end)
Log.trace("Tracking request {}", request) closed = self.__wsClient.Closed:Connect(function()
self.__activeRequests[request] = true closed:Disconnect()
errored:Disconnect()
received:Disconnect()
return request:finally(function(...) if self.__connected then
Log.trace("Cleaning up request {}", request) reject("WebSocket connection closed unexpectedly")
self.__activeRequests[request] = nil else
return ... resolve()
end
end) end)
end
return sendRequest():andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body) errored = self.__wsClient.Error:Connect(function(code, msg)
if body.sessionId ~= self.__sessionId then closed:Disconnect()
return Promise.reject("Server changed ID") errored:Disconnect()
end received:Disconnect()
assert(validateApiSubscribe(body)) reject("WebSocket error: " .. code .. " - " .. msg)
end)
self:setMessageCursor(body.messageCursor)
return body.messages
end) end)
end end

View File

@@ -174,6 +174,8 @@ function App:init()
end end
function App:willUnmount() function App:willUnmount()
self:endSession()
self.waypointConnection:Disconnect() self.waypointConnection:Disconnect()
self.confirmationBindable:Destroy() self.confirmationBindable:Destroy()

View File

@@ -21,7 +21,7 @@ return strict("Config", {
codename = "Epiphany", codename = "Epiphany",
version = realVersion, version = realVersion,
expectedServerVersionString = ("%d.%d or newer"):format(realVersion[1], realVersion[2]), expectedServerVersionString = ("%d.%d or newer"):format(realVersion[1], realVersion[2]),
protocolVersion = 4, protocolVersion = 5,
defaultHost = "localhost", defaultHost = "localhost",
defaultPort = "34872", defaultPort = "34872",
}) })

View File

@@ -201,7 +201,20 @@ function ServeSession:start()
self:__setStatus(Status.Connected, serverInfo.projectName) self:__setStatus(Status.Connected, serverInfo.projectName)
self:__applyGameAndPlaceId(serverInfo) 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)
end) end)
:catch(function(err) :catch(function(err)
@@ -536,40 +549,6 @@ function ServeSession:__initialSync(serverInfo)
end) end)
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) function ServeSession:__stopInternal(err)
self:__setStatus(Status.Disconnected, err) self:__setStatus(Status.Disconnected, err)
self.__apiContext:disconnect() self.__apiContext:disconnect()

View File

@@ -49,12 +49,21 @@ local ApiReadResponse = t.interface({
instances = t.map(RbxId, ApiInstance), instances = t.map(RbxId, ApiInstance),
}) })
local ApiSubscribeResponse = t.interface({ local SocketPacketType = t.union(t.literal("messages"))
sessionId = t.string,
local MessagesPacket = t.interface({
messageCursor = t.number, messageCursor = t.number,
messages = t.array(ApiSubscribeMessage), messages = t.array(ApiSubscribeMessage),
}) })
local SocketPacketBody = t.union(MessagesPacket)
local ApiSocketPacket = t.interface({
sessionId = t.string,
packetType = SocketPacketType,
body = SocketPacketBody,
})
local ApiSerializeResponse = t.interface({ local ApiSerializeResponse = t.interface({
sessionId = t.string, sessionId = t.string,
modelContents = t.buffer, modelContents = t.buffer,
@@ -85,7 +94,7 @@ return strict("Types", {
ApiInfoResponse = ApiInfoResponse, ApiInfoResponse = ApiInfoResponse,
ApiReadResponse = ApiReadResponse, ApiReadResponse = ApiReadResponse,
ApiSubscribeResponse = ApiSubscribeResponse, ApiSocketPacket = ApiSocketPacket,
ApiError = ApiError, ApiError = ApiError,
ApiInstance = ApiInstance, ApiInstance = ApiInstance,

View File

@@ -6,7 +6,7 @@ expectedPlaceIds: ~
gameId: ~ gameId: ~
placeId: ~ placeId: ~
projectName: add_folder projectName: add_folder
protocolVersion: 4 protocolVersion: 5
rootInstanceId: id-2 rootInstanceId: id-2
serverVersion: "[server-version]" serverVersion: "[server-version]"
sessionId: id-1 sessionId: id-1

View File

@@ -1,19 +1,21 @@
--- ---
source: tests/tests/serve.rs source: tests/tests/serve.rs
expression: "subscribe_response.intern_and_redact(&mut redactions, ())" expression: "socket_packet.intern_and_redact(&mut redactions, ())"
--- ---
messageCursor: 1 body:
messages: messageCursor: 1
- added: messages:
id-3: - added:
Children: [] id-3:
ClassName: Folder Children: []
Id: id-3 ClassName: Folder
Metadata: Id: id-3
ignoreUnknownInstances: false Metadata:
Name: my-new-folder ignoreUnknownInstances: false
Parent: id-2 Name: my-new-folder
Properties: {} Parent: id-2
removed: [] Properties: {}
updated: [] removed: []
updated: []
packetType: messages
sessionId: id-1 sessionId: id-1

View File

@@ -7,7 +7,7 @@ expectedPlaceIds: ~
gameId: ~ gameId: ~
placeId: ~ placeId: ~
projectName: optional projectName: optional
protocolVersion: 4 protocolVersion: 5
rootInstanceId: id-2 rootInstanceId: id-2
serverVersion: "[server-version]" serverVersion: "[server-version]"
sessionId: id-1 sessionId: id-1

View File

@@ -6,7 +6,7 @@ expectedPlaceIds: ~
gameId: ~ gameId: ~
placeId: ~ placeId: ~
projectName: edit_init projectName: edit_init
protocolVersion: 4 protocolVersion: 5
rootInstanceId: id-2 rootInstanceId: id-2
serverVersion: "[server-version]" serverVersion: "[server-version]"
sessionId: id-1 sessionId: id-1

View File

@@ -1,19 +1,19 @@
--- ---
source: tests/tests/serve.rs source: tests/tests/serve.rs
expression: "subscribe_response.intern_and_redact(&mut redactions, ())" expression: "socket_packet.intern_and_redact(&mut redactions, ())"
--- ---
messageCursor: 1 body:
messages: messageCursor: 1
- added: {} messages:
removed: [] - added: {}
updated: removed: []
- changedClassName: ~ updated:
changedMetadata: ~ - changedClassName: ~
changedName: ~ changedMetadata: ~
changedProperties: changedName: ~
Source: changedProperties:
String: "-- Edited contents" Source:
id: id-2 String: "-- Edited contents"
id: id-2
packetType: messages
sessionId: id-1 sessionId: id-1

View File

@@ -6,7 +6,7 @@ expectedPlaceIds: ~
gameId: ~ gameId: ~
placeId: ~ placeId: ~
projectName: empty projectName: empty
protocolVersion: 4 protocolVersion: 5
rootInstanceId: id-2 rootInstanceId: id-2
serverVersion: "[server-version]" serverVersion: "[server-version]"
sessionId: id-1 sessionId: id-1

View File

@@ -6,7 +6,7 @@ expectedPlaceIds: ~
gameId: ~ gameId: ~
placeId: ~ placeId: ~
projectName: empty_folder projectName: empty_folder
protocolVersion: 4 protocolVersion: 5
rootInstanceId: id-2 rootInstanceId: id-2
serverVersion: "[server-version]" serverVersion: "[server-version]"
sessionId: id-1 sessionId: id-1

View File

@@ -1,21 +1,23 @@
--- ---
source: tests/tests/serve.rs source: tests/tests/serve.rs
expression: "subscribe_response.intern_and_redact(&mut redactions, ())" expression: "socket_packet.intern_and_redact(&mut redactions, ())"
--- ---
messageCursor: 1 body:
messages: messageCursor: 1
- added: messages:
id-3: - added:
Children: [] id-3:
ClassName: Model Children: []
Id: id-3 ClassName: Model
Metadata: Id: id-3
ignoreUnknownInstances: false Metadata:
Name: test ignoreUnknownInstances: false
Parent: id-2 Name: test
Properties: Parent: id-2
NeedsPivotMigration: Properties:
Bool: false NeedsPivotMigration:
removed: [] Bool: false
updated: [] removed: []
updated: []
packetType: messages
sessionId: id-1 sessionId: id-1

View File

@@ -6,7 +6,7 @@ expectedPlaceIds: ~
gameId: ~ gameId: ~
placeId: ~ placeId: ~
projectName: forced_parent projectName: forced_parent
protocolVersion: 4 protocolVersion: 5
rootInstanceId: id-2 rootInstanceId: id-2
serverVersion: "[server-version]" serverVersion: "[server-version]"
sessionId: id-1 sessionId: id-1

View File

@@ -6,7 +6,7 @@ expectedPlaceIds: ~
gameId: ~ gameId: ~
placeId: ~ placeId: ~
projectName: meshpart projectName: meshpart
protocolVersion: 4 protocolVersion: 5
rootInstanceId: id-2 rootInstanceId: id-2
serverVersion: "[server-version]" serverVersion: "[server-version]"
sessionId: id-1 sessionId: id-1

View File

@@ -1,27 +1,29 @@
--- ---
source: tests/tests/serve.rs source: tests/tests/serve.rs
expression: "subscribe_response.intern_and_redact(&mut redactions, ())" expression: "socket_packet.intern_and_redact(&mut redactions, ())"
--- ---
messageCursor: 1 body:
messages: messageCursor: 1
- added: messages:
id-6: - added:
Children: [] id-6:
ClassName: Actor Children: []
Id: id-6 ClassName: Actor
Metadata: Id: id-6
ignoreUnknownInstances: true Metadata:
Name: Actor ignoreUnknownInstances: true
Parent: id-3 Name: Actor
Properties: Parent: id-3
NeedsPivotMigration: Properties:
Bool: false NeedsPivotMigration:
removed: [] Bool: false
updated: removed: []
- changedClassName: ~ updated:
changedMetadata: - changedClassName: ~
ignoreUnknownInstances: true changedMetadata:
changedName: ~ ignoreUnknownInstances: true
changedProperties: {} changedName: ~
id: id-3 changedProperties: {}
id: id-3
packetType: messages
sessionId: id-1 sessionId: id-1

View File

@@ -6,7 +6,7 @@ expectedPlaceIds: ~
gameId: ~ gameId: ~
placeId: ~ placeId: ~
projectName: move_folder_of_stuff projectName: move_folder_of_stuff
protocolVersion: 4 protocolVersion: 5
rootInstanceId: id-2 rootInstanceId: id-2
serverVersion: "[server-version]" serverVersion: "[server-version]"
sessionId: id-1 sessionId: id-1

View File

@@ -1,141 +1,141 @@
--- ---
source: tests/tests/serve.rs source: tests/tests/serve.rs
expression: "subscribe_response.intern_and_redact(&mut redactions, ())" expression: "socket_packet.intern_and_redact(&mut redactions, ())"
--- ---
messageCursor: 1 body:
messages: messageCursor: 1
- added: messages:
id-10: - added:
Children: [] id-10:
ClassName: StringValue Children: []
Id: id-10 ClassName: StringValue
Metadata: Id: id-10
ignoreUnknownInstances: false Metadata:
Name: "6" ignoreUnknownInstances: false
Parent: id-3 Name: "6"
Properties: Parent: id-3
Value: Properties:
String: "File #6" Value:
id-11: String: "File #6"
Children: [] id-11:
ClassName: StringValue Children: []
Id: id-11 ClassName: StringValue
Metadata: Id: id-11
ignoreUnknownInstances: false Metadata:
Name: "7" ignoreUnknownInstances: false
Parent: id-3 Name: "7"
Properties: Parent: id-3
Value: Properties:
String: "File #7" Value:
id-12: String: "File #7"
Children: [] id-12:
ClassName: StringValue Children: []
Id: id-12 ClassName: StringValue
Metadata: Id: id-12
ignoreUnknownInstances: false Metadata:
Name: "8" ignoreUnknownInstances: false
Parent: id-3 Name: "8"
Properties: Parent: id-3
Value: Properties:
String: "File #8" Value:
id-13: String: "File #8"
Children: [] id-13:
ClassName: StringValue Children: []
Id: id-13 ClassName: StringValue
Metadata: Id: id-13
ignoreUnknownInstances: false Metadata:
Name: "9" ignoreUnknownInstances: false
Parent: id-3 Name: "9"
Properties: Parent: id-3
Value: Properties:
String: "File #9" Value:
id-3: String: "File #9"
Children: id-3:
- id-4 Children:
- id-5 - id-4
- id-6 - id-5
- id-7 - id-6
- id-8 - id-7
- id-9 - id-8
- id-10 - id-9
- id-11 - id-10
- id-12 - id-11
- id-13 - id-12
ClassName: Folder - id-13
Id: id-3 ClassName: Folder
Metadata: Id: id-3
ignoreUnknownInstances: false Metadata:
Name: new-stuff ignoreUnknownInstances: false
Parent: id-2 Name: new-stuff
Properties: {} Parent: id-2
id-4: Properties: {}
Children: [] id-4:
ClassName: StringValue Children: []
Id: id-4 ClassName: StringValue
Metadata: Id: id-4
ignoreUnknownInstances: false Metadata:
Name: "0" ignoreUnknownInstances: false
Parent: id-3 Name: "0"
Properties: Parent: id-3
Value: Properties:
String: "File #0" Value:
id-5: String: "File #0"
Children: [] id-5:
ClassName: StringValue Children: []
Id: id-5 ClassName: StringValue
Metadata: Id: id-5
ignoreUnknownInstances: false Metadata:
Name: "1" ignoreUnknownInstances: false
Parent: id-3 Name: "1"
Properties: Parent: id-3
Value: Properties:
String: "File #1" Value:
id-6: String: "File #1"
Children: [] id-6:
ClassName: StringValue Children: []
Id: id-6 ClassName: StringValue
Metadata: Id: id-6
ignoreUnknownInstances: false Metadata:
Name: "2" ignoreUnknownInstances: false
Parent: id-3 Name: "2"
Properties: Parent: id-3
Value: Properties:
String: "File #2" Value:
id-7: String: "File #2"
Children: [] id-7:
ClassName: StringValue Children: []
Id: id-7 ClassName: StringValue
Metadata: Id: id-7
ignoreUnknownInstances: false Metadata:
Name: "3" ignoreUnknownInstances: false
Parent: id-3 Name: "3"
Properties: Parent: id-3
Value: Properties:
String: "File #3" Value:
id-8: String: "File #3"
Children: [] id-8:
ClassName: StringValue Children: []
Id: id-8 ClassName: StringValue
Metadata: Id: id-8
ignoreUnknownInstances: false Metadata:
Name: "4" ignoreUnknownInstances: false
Parent: id-3 Name: "4"
Properties: Parent: id-3
Value: Properties:
String: "File #4" Value:
id-9: String: "File #4"
Children: [] id-9:
ClassName: StringValue Children: []
Id: id-9 ClassName: StringValue
Metadata: Id: id-9
ignoreUnknownInstances: false Metadata:
Name: "5" ignoreUnknownInstances: false
Parent: id-3 Name: "5"
Properties: Parent: id-3
Value: Properties:
String: "File #5" Value:
removed: [] String: "File #5"
updated: [] removed: []
updated: []
packetType: messages
sessionId: id-1 sessionId: id-1

View File

@@ -6,7 +6,7 @@ expectedPlaceIds: ~
gameId: ~ gameId: ~
placeId: ~ placeId: ~
projectName: top-level projectName: top-level
protocolVersion: 4 protocolVersion: 5
rootInstanceId: id-2 rootInstanceId: id-2
serverVersion: "[server-version]" serverVersion: "[server-version]"
sessionId: id-1 sessionId: id-1

View File

@@ -6,7 +6,7 @@ expectedPlaceIds: ~
gameId: ~ gameId: ~
placeId: ~ placeId: ~
projectName: no_name_project projectName: no_name_project
protocolVersion: 4 protocolVersion: 5
rootInstanceId: id-2 rootInstanceId: id-2
serverVersion: "[server-version]" serverVersion: "[server-version]"
sessionId: id-1 sessionId: id-1

View File

@@ -6,7 +6,7 @@ expectedPlaceIds: ~
gameId: ~ gameId: ~
placeId: ~ placeId: ~
projectName: no_name_top_level_project projectName: no_name_top_level_project
protocolVersion: 4 protocolVersion: 5
rootInstanceId: id-2 rootInstanceId: id-2
serverVersion: "[server-version]" serverVersion: "[server-version]"
sessionId: id-1 sessionId: id-1

View File

@@ -6,7 +6,7 @@ expectedPlaceIds: ~
gameId: ~ gameId: ~
placeId: ~ placeId: ~
projectName: pivot_migration projectName: pivot_migration
protocolVersion: 4 protocolVersion: 5
rootInstanceId: id-2 rootInstanceId: id-2
serverVersion: "[server-version]" serverVersion: "[server-version]"
sessionId: id-1 sessionId: id-1

View File

@@ -6,7 +6,7 @@ expectedPlaceIds: ~
gameId: ~ gameId: ~
placeId: ~ placeId: ~
projectName: ref_properties projectName: ref_properties
protocolVersion: 4 protocolVersion: 5
rootInstanceId: id-2 rootInstanceId: id-2
serverVersion: "[server-version]" serverVersion: "[server-version]"
sessionId: id-1 sessionId: id-1

View File

@@ -6,7 +6,7 @@ expectedPlaceIds: ~
gameId: ~ gameId: ~
placeId: ~ placeId: ~
projectName: ref_properties projectName: ref_properties
protocolVersion: 4 protocolVersion: 5
rootInstanceId: id-2 rootInstanceId: id-2
serverVersion: "[server-version]" serverVersion: "[server-version]"
sessionId: id-1 sessionId: id-1

View File

@@ -1,17 +1,19 @@
--- ---
source: tests/tests/serve.rs source: tests/tests/serve.rs
expression: "subscribe_response.intern_and_redact(&mut redactions, ())" expression: "socket_packet.intern_and_redact(&mut redactions, ())"
--- ---
messageCursor: 1 body:
messages: messageCursor: 1
- added: {} messages:
removed: [] - added: {}
updated: removed: []
- changedClassName: ~ updated:
changedMetadata: ~ - changedClassName: ~
changedName: ~ changedMetadata: ~
changedProperties: changedName: ~
Scale: changedProperties:
Float32: 1 Scale:
id: id-8 Float32: 1
id: id-8
packetType: messages
sessionId: id-1 sessionId: id-1

View File

@@ -6,7 +6,7 @@ expectedPlaceIds: ~
gameId: ~ gameId: ~
placeId: ~ placeId: ~
projectName: ref_properties_remove projectName: ref_properties_remove
protocolVersion: 4 protocolVersion: 5
rootInstanceId: id-2 rootInstanceId: id-2
serverVersion: "[server-version]" serverVersion: "[server-version]"
sessionId: id-1 sessionId: id-1

View File

@@ -1,12 +1,13 @@
--- ---
source: tests/tests/serve.rs source: tests/tests/serve.rs
expression: "subscribe_response.intern_and_redact(&mut redactions, ())" expression: "socket_packet.intern_and_redact(&mut redactions, ())"
--- ---
messageCursor: 1 body:
messages: messageCursor: 1
- added: {} messages:
removed: - added: {}
- id-4 removed:
updated: [] - id-4
updated: []
packetType: messages
sessionId: id-1 sessionId: id-1

View File

@@ -1,47 +1,49 @@
--- ---
source: tests/tests/serve.rs source: tests/tests/serve.rs
expression: "subscribe_response.intern_and_redact(&mut redactions, ())" expression: "socket_packet.intern_and_redact(&mut redactions, ())"
--- ---
messageCursor: 1 body:
messages: messageCursor: 1
- added: messages:
id-11: - added:
Children: [] id-11:
ClassName: Model Children: []
Id: id-11 ClassName: Model
Metadata: Id: id-11
ignoreUnknownInstances: false Metadata:
Name: ProjectPointer ignoreUnknownInstances: false
Parent: id-7 Name: ProjectPointer
Properties: Parent: id-7
Attributes: Properties:
Attributes: Attributes:
Rojo_Target_PrimaryPart: Attributes:
String: project target Rojo_Target_PrimaryPart:
NeedsPivotMigration: String: project target
Bool: false NeedsPivotMigration:
PrimaryPart: Bool: false
Ref: id-9 PrimaryPart:
removed: [] Ref: id-9
updated: removed: []
- changedClassName: ~ updated:
changedMetadata: - changedClassName: ~
ignoreUnknownInstances: false changedMetadata:
changedName: ~ ignoreUnknownInstances: false
changedProperties: changedName: ~
Attributes: changedProperties:
Attributes: Attributes:
Rojo_Id: Attributes:
String: model target 2 Rojo_Id:
id: id-7 String: model target 2
- changedClassName: ~ id: id-7
changedMetadata: ~ - changedClassName: ~
changedName: ~ changedMetadata: ~
changedProperties: changedName: ~
Attributes: changedProperties:
Attributes: Attributes:
Rojo_Target_PrimaryPart: Attributes:
String: model target 2 Rojo_Target_PrimaryPart:
PrimaryPart: ~ String: model target 2
id: id-8 PrimaryPart: ~
id: id-8
packetType: messages
sessionId: id-1 sessionId: id-1

View File

@@ -6,7 +6,7 @@ expectedPlaceIds: ~
gameId: ~ gameId: ~
placeId: ~ placeId: ~
projectName: remove_file projectName: remove_file
protocolVersion: 4 protocolVersion: 5
rootInstanceId: id-2 rootInstanceId: id-2
serverVersion: "[server-version]" serverVersion: "[server-version]"
sessionId: id-1 sessionId: id-1

View File

@@ -1,11 +1,13 @@
--- ---
source: tests/tests/serve.rs source: tests/tests/serve.rs
expression: "subscribe_response.intern_and_redact(&mut redactions, ())" expression: "socket_packet.intern_and_redact(&mut redactions, ())"
--- ---
messageCursor: 1 body:
messages: messageCursor: 1
- added: {} messages:
removed: - added: {}
- id-3 removed:
updated: [] - id-3
updated: []
packetType: messages
sessionId: id-1 sessionId: id-1

View File

@@ -6,7 +6,7 @@ expectedPlaceIds: ~
gameId: ~ gameId: ~
placeId: ~ placeId: ~
projectName: scripts projectName: scripts
protocolVersion: 4 protocolVersion: 5
rootInstanceId: id-2 rootInstanceId: id-2
serverVersion: "[server-version]" serverVersion: "[server-version]"
sessionId: id-1 sessionId: id-1

View File

@@ -1,19 +1,19 @@
--- ---
source: tests/tests/serve.rs source: tests/tests/serve.rs
expression: "subscribe_response.intern_and_redact(&mut redactions, ())" expression: "socket_packet.intern_and_redact(&mut redactions, ())"
--- ---
messageCursor: 1 body:
messages: messageCursor: 1
- added: {} messages:
removed: [] - added: {}
updated: removed: []
- changedClassName: ~ updated:
changedMetadata: ~ - changedClassName: ~
changedName: ~ changedMetadata: ~
changedProperties: changedName: ~
Source: changedProperties:
String: Updated foo! Source:
id: id-4 String: Updated foo!
id: id-4
packetType: messages
sessionId: id-1 sessionId: id-1

View File

@@ -6,7 +6,7 @@ expectedPlaceIds: ~
gameId: ~ gameId: ~
placeId: ~ placeId: ~
projectName: sync_rule_alone projectName: sync_rule_alone
protocolVersion: 4 protocolVersion: 5
rootInstanceId: id-2 rootInstanceId: id-2
serverVersion: "[server-version]" serverVersion: "[server-version]"
sessionId: id-1 sessionId: id-1

View File

@@ -6,7 +6,7 @@ expectedPlaceIds: ~
gameId: ~ gameId: ~
placeId: ~ placeId: ~
projectName: sync_rule_complex projectName: sync_rule_complex
protocolVersion: 4 protocolVersion: 5
rootInstanceId: id-2 rootInstanceId: id-2
serverVersion: "[server-version]" serverVersion: "[server-version]"
sessionId: id-1 sessionId: id-1

View File

@@ -6,7 +6,7 @@ expectedPlaceIds: ~
gameId: ~ gameId: ~
placeId: ~ placeId: ~
projectName: sync_rule_no_extension projectName: sync_rule_no_extension
protocolVersion: 4 protocolVersion: 5
rootInstanceId: id-2 rootInstanceId: id-2
serverVersion: "[server-version]" serverVersion: "[server-version]"
sessionId: id-1 sessionId: id-1

View File

@@ -6,7 +6,7 @@ expectedPlaceIds: ~
gameId: ~ gameId: ~
placeId: ~ placeId: ~
projectName: sync_rule_no_name_project projectName: sync_rule_no_name_project
protocolVersion: 4 protocolVersion: 5
rootInstanceId: id-2 rootInstanceId: id-2
serverVersion: "[server-version]" serverVersion: "[server-version]"
sessionId: id-1 sessionId: id-1

View File

@@ -9,7 +9,9 @@ use std::{
sync::Arc, sync::Arc,
}; };
use futures::{sink::SinkExt, stream::StreamExt};
use hyper::{body, Body, Method, Request, Response, StatusCode}; use hyper::{body, Body, Method, Request, Response, StatusCode};
use hyper_tungstenite::{is_upgrade_request, tungstenite::Message, upgrade, HyperWebsocket};
use opener::OpenError; use opener::OpenError;
use rbx_dom_weak::{ use rbx_dom_weak::{
types::{Ref, Variant}, types::{Ref, Variant},
@@ -22,16 +24,16 @@ use crate::{
snapshot::{InstanceWithMeta, PatchSet, PatchUpdate}, snapshot::{InstanceWithMeta, PatchSet, PatchUpdate},
web::{ web::{
interface::{ interface::{
ErrorResponse, Instance, OpenResponse, ReadResponse, ServerInfoResponse, ErrorResponse, Instance, MessagesPacket, OpenResponse, ReadResponse,
SubscribeMessage, SubscribeResponse, WriteRequest, WriteResponse, PROTOCOL_VERSION, ServerInfoResponse, SocketPacket, SocketPacketBody, SocketPacketType, SubscribeMessage,
SERVER_VERSION, WriteRequest, WriteResponse, PROTOCOL_VERSION, SERVER_VERSION,
}, },
util::{json, json_ok}, util::{json, json_ok},
}, },
web_api::{BufferEncode, InstanceUpdate, RefPatchResponse, SerializeResponse}, 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); let service = ApiService::new(serve_session);
match (request.method(), request.uri().path()) { 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/") => { (&Method::GET, path) if path.starts_with("/api/read/") => {
service.handle_api_read(request).await service.handle_api_read(request).await
} }
(&Method::GET, path) if path.starts_with("/api/subscribe/") => { (&Method::GET, path) if path.starts_with("/api/socket/") => {
service.handle_api_subscribe(request).await 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/") => { (&Method::GET, path) if path.starts_with("/api/serialize/") => {
service.handle_api_serialize(request).await service.handle_api_serialize(request).await
@@ -88,10 +99,9 @@ impl ApiService {
}) })
} }
/// Retrieve any messages past the given cursor index, and if /// Handle WebSocket upgrade for real-time message streaming
/// there weren't any, subscribe to receive any new messages. async fn handle_api_socket(&self, request: &mut Request<Body>) -> Response<Body> {
async fn handle_api_subscribe(&self, request: Request<Body>) -> Response<Body> { let argument = &request.uri().path()["/api/socket/".len()..];
let argument = &request.uri().path()["/api/subscribe/".len()..];
let input_cursor: u32 = match argument.parse() { let input_cursor: u32 = match argument.parse() {
Ok(v) => v, Ok(v) => v,
Err(err) => { Err(err) => {
@@ -102,36 +112,29 @@ impl ApiService {
} }
}; };
let session_id = self.serve_session.session_id(); // Upgrade the connection to WebSocket
let (response, websocket) = match upgrade(request, None) {
let result = self Ok(result) => result,
.serve_session Err(err) => {
.message_queue() return json(
.subscribe(input_cursor) ErrorResponse::internal_error(format!("WebSocket upgrade failed: {}", err)),
.await; StatusCode::INTERNAL_SERVER_ERROR,
);
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,
})
} }
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> { 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()) .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 /// Certain Instances MUST be a child of specific classes. This function
/// tracks that information for the Serialize endpoint. /// tracks that information for the Serialize endpoint.
/// ///

View File

@@ -12,6 +12,7 @@ use rbx_dom_weak::{
Ustr, UstrMap, Ustr, UstrMap,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use strum::Display;
use crate::{ use crate::{
session_id::SessionId, session_id::SessionId,
@@ -24,7 +25,7 @@ use crate::{
pub(crate) const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION"); pub(crate) const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION");
/// Current protocol version, which is required to match. /// 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. /// Message returned by Rojo API when a change has occurred.
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
@@ -192,15 +193,44 @@ pub struct WriteResponse {
pub session_id: SessionId, 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)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct SubscribeResponse<'a> { pub struct MessagesPacket<'a> {
pub session_id: SessionId,
pub message_cursor: u32, pub message_cursor: u32,
pub messages: Vec<SubscribeMessage<'a>>, 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} /// Response body from /api/open/{id}
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]

View File

@@ -3,7 +3,9 @@ use std::collections::HashMap;
use rbx_dom_weak::types::Ref; use rbx_dom_weak::types::Ref;
use serde::Serialize; 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; use rojo_insta_ext::RedactionMap;
/// A convenience method to store all of the redactable data from a piece of /// 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: ()) { fn intern(&self, redactions: &mut RedactionMap, _extra: ()) {
for message in &self.messages { for message in &self.messages {
intern_instance_updates(redactions, &message.updated); intern_instance_updates(redactions, &message.updated);

View File

@@ -8,11 +8,14 @@ use std::{
time::Duration, time::Duration,
}; };
use hyper_tungstenite::tungstenite::{connect, Message};
use rbx_dom_weak::types::Ref; use rbx_dom_weak::types::Ref;
use tempfile::{tempdir, TempDir}; 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 rojo_insta_ext::RedactionMap;
use crate::rojo_test::io_util::{ use crate::rojo_test::io_util::{
@@ -173,13 +176,54 @@ impl TestServeSession {
Ok(serde_json::from_value(value).expect("Server returned malformed response")) Ok(serde_json::from_value(value).expect("Server returned malformed response"))
} }
pub fn get_api_subscribe( pub fn get_api_socket_packet(
&self, &self,
packet_type: SocketPacketType,
cursor: u32, cursor: u32,
) -> Result<SubscribeResponse<'static>, reqwest::Error> { ) -> Result<SocketPacket<'static>, Box<dyn std::error::Error>> {
let url = format!("http://localhost:{}/api/subscribe/{}", self.port, cursor); 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> { pub fn get_api_serialize(&self, ids: &[Ref]) -> Result<SerializeResponse, reqwest::Error> {

View File

@@ -8,6 +8,8 @@ use crate::rojo_test::{
serve_util::{run_serve_test, serialize_to_xml_model}, serve_util::{run_serve_test, serialize_to_xml_model},
}; };
use librojo::web_api::SocketPacketType;
#[test] #[test]
fn empty() { fn empty() {
run_serve_test("empty", |session, mut redactions| { run_serve_test("empty", |session, mut redactions| {
@@ -42,10 +44,12 @@ fn scripts() {
fs::write(session.path().join("src/foo.lua"), "Updated foo!").unwrap(); 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!( assert_yaml_snapshot!(
"scripts_subscribe", "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(); 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(); 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!( assert_yaml_snapshot!(
"add_folder_subscribe", "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(); 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(); 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!( assert_yaml_snapshot!(
"remove_file_subscribe", "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(); 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(); 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!( assert_yaml_snapshot!(
"edit_init_subscribe", "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(); let read_response = session.get_api_read(root_id).unwrap();
@@ -180,10 +190,12 @@ fn move_folder_of_stuff() {
// will fail otherwise. // will fail otherwise.
fs::rename(stuff_path, session.path().join("src/new-stuff")).unwrap(); 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!( assert_yaml_snapshot!(
"move_folder_of_stuff_subscribe", "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(); let read_response = session.get_api_read(root_id).unwrap();
@@ -214,10 +226,12 @@ fn empty_json_model() {
) )
.unwrap(); .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!( assert_yaml_snapshot!(
"empty_json_model_subscribe", "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(); 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(); 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!( assert_yaml_snapshot!(
"add_optional_folder_subscribe", "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(); let read_response = session.get_api_read(root_id).unwrap();
@@ -437,10 +453,12 @@ fn ref_properties() {
) )
.unwrap(); .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!( assert_yaml_snapshot!(
"ref_properties_subscribe", "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(); 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(); 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!( assert_yaml_snapshot!(
"ref_properties_remove_subscribe", "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(); let read_response = session.get_api_read(root_id).unwrap();
@@ -529,10 +549,12 @@ fn ref_properties_patch_update() {
) )
.unwrap(); .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!( assert_yaml_snapshot!(
"ref_properties_patch_update_subscribe", "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(); let read_response = session.get_api_read(root_id).unwrap();
@@ -581,10 +603,12 @@ fn model_pivot_migration() {
) )
.unwrap(); .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!( assert_yaml_snapshot!(
"model_pivot_migration_all", "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(); let read_response = session.get_api_read(root_id).unwrap();