Implement long-polling message passing

This commit is contained in:
Lucien Greathouse 2018-05-26 00:16:27 -07:00
parent a9d24f1493
commit 92fa52c56f
8 changed files with 93 additions and 39 deletions

View file

@ -22,6 +22,10 @@
"path": "modules/testez/lib", "path": "modules/testez/lib",
"target": "ReplicatedStorage.TestEZ" "target": "ReplicatedStorage.TestEZ"
}, },
"modules/promise": {
"path": "modules/promise/lib",
"target": "ReplicatedStorage.Rojo.modules.Promise"
},
"tests": { "tests": {
"path": "tests", "path": "tests",
"target": "TestService" "target": "TestService"

View file

@ -1,5 +1,6 @@
local Promise = require(script.Parent.Parent.modules.Promise)
local Config = require(script.Parent.Config) local Config = require(script.Parent.Config)
local Promise = require(script.Parent.Promise)
local Version = require(script.Parent.Version) local Version = require(script.Parent.Version)
local Http = require(script.Parent.Http) local Http = require(script.Parent.Http)
@ -24,7 +25,8 @@ function ApiContext.new(url, onMessage)
url = url, url = url,
onMessage = onMessage, onMessage = onMessage,
serverId = nil, serverId = nil,
currentTime = 0, connected = false,
eventCursor = -1,
} }
setmetatable(context, ApiContext) setmetatable(context, ApiContext)
@ -35,9 +37,9 @@ end
function ApiContext:connect() function ApiContext:connect()
return Http.get(self.url) return Http.get(self.url)
:andThen(function(response) :andThen(function(response)
response = response:json() local body = response:json()
if response.protocolVersion ~= Config.protocolVersion then if body.protocolVersion ~= Config.protocolVersion then
local message = ( local message = (
"Found a Rojo dev server, but it's using a different protocol version, and is incompatible." .. "Found a Rojo dev server, but it's using a different protocol version, and is incompatible." ..
"\nMake sure you have matching versions of both the Rojo plugin and server!" .. "\nMake sure you have matching versions of both the Rojo plugin and server!" ..
@ -47,14 +49,43 @@ function ApiContext:connect()
):format( ):format(
Version.display(Config.version), Config.protocolVersion, Version.display(Config.version), Config.protocolVersion,
Config.expectedApiContextVersionString, Config.expectedApiContextVersionString,
response.serverVersion, response.protocolVersion body.serverVersion, body.protocolVersion
) )
return Promise.reject(message) return Promise.reject(message)
end end
self.serverId = response.serverId self.serverId = body.serverId
self.currentTime = response.currentTime self.connected = true
return self:_retrieveMessages()
end)
end
function ApiContext:_retrieveMessages()
if not self.connected then
return Promise.resolve()
end
return Http.get(self.url .. "/subscribe/" .. self.eventCursor)
:andThen(function(response)
local body = response:json()
if body.serverId ~= self.serverId then
return Promise.reject("server changed ID")
end
for _, event in ipairs(body.events) do
self.onMessage(event)
end
self.eventCursor = body.eventCursor
return self:_retrieveMessages()
end, function(err)
self.connected = false
return Promise.reject(err)
end) end)
end end

View file

@ -2,7 +2,8 @@ local HttpService = game:GetService("HttpService")
local HTTP_DEBUG = false local HTTP_DEBUG = false
local Promise = require(script.Parent.Promise) local Promise = require(script.Parent.Parent.modules.Promise)
local HttpError = require(script.Parent.HttpError) local HttpError = require(script.Parent.HttpError)
local HttpResponse = require(script.Parent.HttpResponse) local HttpResponse = require(script.Parent.HttpResponse)

View file

@ -43,12 +43,19 @@ local function main()
local toolbar = plugin:CreateToolbar("Rojo " .. displayedVersion) local toolbar = plugin:CreateToolbar("Rojo " .. displayedVersion)
local currentSession
toolbar:CreateButton("Connect", "Connect to Rojo Session", "") toolbar:CreateButton("Connect", "Connect to Rojo Session", "")
.Click:Connect(function() .Click:Connect(function()
checkUpgrade() checkUpgrade()
Session.new()
error("NYI") if currentSession ~= nil then
warn("Rojo: A session is already running!")
return
end
print("Rojo: Started session.")
currentSession = Session.new()
end) end)
end end

View file

@ -1,7 +1,8 @@
local Promise = require(script.Parent.Parent.modules.Promise)
local Config = require(script.Parent.Config) local Config = require(script.Parent.Config)
local Http = require(script.Parent.Http) local Http = require(script.Parent.Http)
local ApiContext = require(script.Parent.ApiContext) local ApiContext = require(script.Parent.ApiContext)
local Promise = require(script.Parent.Promise)
local REMOTE_URL = ("http://localhost:%d"):format(Config.port) local REMOTE_URL = ("http://localhost:%d"):format(Config.port)
@ -13,7 +14,9 @@ function Session.new()
setmetatable(self, Session) setmetatable(self, Session)
local api = ApiContext.new(REMOTE_URL) local api = ApiContext.new(REMOTE_URL, function(message)
print("Got message:", message)
end)
api:connect() api:connect()
return self return self

View file

@ -1,7 +1,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::path::PathBuf; use std::path::PathBuf;
use std::process; use std::process;
use std::time::Instant;
use std::fs; use std::fs;
use rand; use rand;
@ -50,7 +49,6 @@ pub fn serve(project_path: &PathBuf, port: Option<u64>) {
let web_config = web::WebConfig { let web_config = web::WebConfig {
port: port.unwrap_or(project.serve_port), port: port.unwrap_or(project.serve_port),
server_id, server_id,
start_time: Instant::now(),
rbx_session: session.get_rbx_session(), rbx_session: session.get_rbx_session(),
events: session.get_events(), events: session.get_events(),
event_listeners: session.get_event_listeners(), event_listeners: session.get_event_listeners(),

View file

@ -26,7 +26,7 @@ pub struct Session {
config: SessionConfig, config: SessionConfig,
vfs_session: Arc<RwLock<VfsSession>>, vfs_session: Arc<RwLock<VfsSession>>,
rbx_session: Arc<RwLock<RbxSession>>, rbx_session: Arc<RwLock<RbxSession>>,
middlewares: Vec<Box<Middleware>>, // middlewares: Vec<Box<Middleware>>,
watchers: Vec<PartitionWatcher>, watchers: Vec<PartitionWatcher>,
events: Arc<RwLock<Vec<SessionEvent>>>, events: Arc<RwLock<Vec<SessionEvent>>>,
event_listeners: Arc<Mutex<HashMap<Id, mpsc::Sender<()>>>>, event_listeners: Arc<Mutex<HashMap<Id, mpsc::Sender<()>>>>,
@ -40,7 +40,7 @@ impl Session {
Session { Session {
vfs_session, vfs_session,
rbx_session, rbx_session,
middlewares: Vec::new(), // middlewares: Vec::new(),
watchers: Vec::new(), watchers: Vec::new(),
events: Arc::new(RwLock::new(Vec::new())), events: Arc::new(RwLock::new(Vec::new())),
event_listeners: Arc::new(Mutex::new(HashMap::new())), event_listeners: Arc::new(Mutex::new(HashMap::new())),
@ -69,12 +69,32 @@ impl Session {
{ {
let vfs_session = self.vfs_session.clone(); let vfs_session = self.vfs_session.clone();
let event_listeners = self.event_listeners.clone();
let events = self.events.clone();
thread::spawn(move || { thread::spawn(move || {
loop { loop {
match rx.recv() { match rx.recv() {
Ok(change) => { Ok(change) => {
{
let mut vfs_session = vfs_session.write().unwrap(); let mut vfs_session = vfs_session.write().unwrap();
vfs_session.handle_change(change); vfs_session.handle_change(change);
}
// TODO: Handle change in RbxSession
{
let mut events = events.write().unwrap();
events.push(SessionEvent::Something);
}
{
let listeners = event_listeners.lock().unwrap();
for listener in listeners.values() {
listener.send(()).unwrap();
}
}
}, },
Err(_) => break, Err(_) => break,
} }

View file

@ -1,5 +1,4 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::time::Instant;
use std::sync::{mpsc, RwLock, Mutex, Arc}; use std::sync::{mpsc, RwLock, Mutex, Arc};
use rouille; use rouille;
@ -14,7 +13,6 @@ use session::SessionEvent;
pub struct WebConfig { pub struct WebConfig {
pub port: u64, pub port: u64,
pub server_id: u64, pub server_id: u64,
pub start_time: Instant,
pub rbx_session: Arc<RwLock<RbxSession>>, pub rbx_session: Arc<RwLock<RbxSession>>,
pub events: Arc<RwLock<Vec<SessionEvent>>>, pub events: Arc<RwLock<Vec<SessionEvent>>>,
pub event_listeners: Arc<Mutex<HashMap<Id, mpsc::Sender<()>>>>, pub event_listeners: Arc<Mutex<HashMap<Id, mpsc::Sender<()>>>>,
@ -26,22 +24,22 @@ struct ServerInfoResponse<'a> {
server_version: &'static str, server_version: &'static str,
protocol_version: u64, protocol_version: u64,
server_id: &'a str, server_id: &'a str,
current_time: f64,
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct ReadAllResponse<'a> { struct ReadAllResponse<'a> {
server_id: &'a str, server_id: &'a str,
current_time: f64,
instances: &'a HashMap<Id, RbxInstance>, instances: &'a HashMap<Id, RbxInstance>,
event_cursor: i32,
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct SubscribeResponse<'a> { struct SubscribeResponse<'a> {
server_id: &'a str, server_id: &'a str,
events: &'a [SessionEvent], events: &'a [SessionEvent],
cursor: i32, event_cursor: i32,
} }
/// Start the Rojo web server and park our current thread. /// Start the Rojo web server and park our current thread.
@ -57,17 +55,10 @@ pub fn start(config: WebConfig) {
(GET) (/) => { (GET) (/) => {
// Get a summary of information about the server. // Get a summary of information about the server.
let current_time = {
let elapsed = config.start_time.elapsed();
elapsed.as_secs() as f64 + elapsed.subsec_nanos() as f64 / 1_000_000_000.0
};
json_response(ServerInfoResponse { json_response(ServerInfoResponse {
server_version, server_version,
protocol_version: 2, protocol_version: 2,
server_id: &server_id, server_id: &server_id,
current_time,
}) })
}, },
@ -79,13 +70,13 @@ pub fn start(config: WebConfig) {
{ {
let events = config.events.read().unwrap(); let events = config.events.read().unwrap();
if cursor < events.len() as i32 - 1 { if cursor < events.len() as i32 - 1 {
let new_events = &events[cursor as usize + 1..]; let new_events = &events[(cursor + 1) as usize..];
let new_cursor = cursor + new_events.len() as i32; let new_cursor = cursor + new_events.len() as i32;
return json_response(SubscribeResponse { return json_response(SubscribeResponse {
server_id: &server_id, server_id: &server_id,
events: new_events, events: new_events,
cursor: new_cursor, event_cursor: new_cursor,
}); });
} }
} }
@ -110,13 +101,13 @@ pub fn start(config: WebConfig) {
{ {
let events = config.events.read().unwrap(); let events = config.events.read().unwrap();
let new_events = &events[cursor as usize + 1..]; let new_events = &events[(cursor + 1) as usize..];
let new_cursor = cursor + new_events.len() as i32; let new_cursor = cursor + new_events.len() as i32;
return json_response(SubscribeResponse { return json_response(SubscribeResponse {
server_id: &server_id, server_id: &server_id,
events: new_events, events: new_events,
cursor: new_cursor, event_cursor: new_cursor,
}); });
} }
}, },
@ -124,15 +115,14 @@ pub fn start(config: WebConfig) {
(GET) (/read_all) => { (GET) (/read_all) => {
let rbx_session = config.rbx_session.read().unwrap(); let rbx_session = config.rbx_session.read().unwrap();
let current_time = { let event_cursor = {
let elapsed = config.start_time.elapsed(); let events = config.events.read().unwrap();
events.len() as i32 - 1
elapsed.as_secs() as f64 + elapsed.subsec_nanos() as f64 / 1_000_000_000.0
}; };
json_response(ReadAllResponse { json_response(ReadAllResponse {
server_id: &server_id, server_id: &server_id,
current_time, event_cursor,
instances: &rbx_session.instances, instances: &rbx_session.instances,
}) })
}, },