diff --git a/CHANGELOG.md b/CHANGELOG.md index f743c6677..33cc35758 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## v0.22.9 - 2019-12-24 + +* enhanced support for WebChannels and websockets operations + ## v0.22.8 - 2019-12-05 * more tests diff --git a/Manifest.toml b/Manifest.toml index 1bcb3aa96..27a537ae4 100755 --- a/Manifest.toml +++ b/Manifest.toml @@ -108,9 +108,9 @@ version = "0.21.0" [[JuliaInterpreter]] deps = ["CodeTracking", "InteractiveUtils", "Random", "UUIDs"] -git-tree-sha1 = "0bf5e88aa07b9ffe36dca3215642d5d1ea2901cc" +git-tree-sha1 = "cf43000752f15c92bef4a080966810de886f3560" uuid = "aa1ae85d-cabe-5617-a682-6adf51b2e16a" -version = "0.7.5" +version = "0.7.6" [[LibGit2]] uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" @@ -153,9 +153,9 @@ uuid = "739be429-bea8-5141-9913-cc70e7f3736d" version = "0.7.0" [[Millboard]] -git-tree-sha1 = "c6d4a61d1b74ba35b1efe5cb025bf852145521e1" +git-tree-sha1 = "19b2fe6b9cdef06291f66692335ec25fd2e1f037" uuid = "39ec1447-df44-5f4c-beaa-866f30b4d3b2" -version = "0.2.1" +version = "0.2.2" [[Mmap]] uuid = "a63ad114-7e13-5084-954f-fe012c677804" @@ -207,9 +207,9 @@ version = "0.2.0" [[Revise]] deps = ["CodeTracking", "Distributed", "FileWatching", "JuliaInterpreter", "LibGit2", "LoweredCodeUtils", "OrderedCollections", "Pkg", "REPL", "UUIDs", "Unicode"] -git-tree-sha1 = "855751b1fc9337d8cbe1e6d80ab6aa948a004a7e" +git-tree-sha1 = "2ecbd19f31a934b035bfc38036d5f7ac575d0878" uuid = "295af30f-e4ad-537b-8983-00126c2a3abe" -version = "2.3.2" +version = "2.5.0" [[SHA]] uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" diff --git a/Project.toml b/Project.toml index 66346568f..2a1bd342b 100755 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Genie" uuid = "c43c736e-a2d1-11e8-161f-af95117fbd1e" authors = ["Adrian Salceanu "] -version = "0.22.8" +version = "0.22.9" [deps] ArgParse = "c7e460c6-2fb9-53a9-8c5b-16f535851c63" diff --git a/files/new_app/assets/js/application.js b/files/new_app/assets/js/application.js index 965a1d4fa..4de236f8e 100644 --- a/files/new_app/assets/js/application.js +++ b/files/new_app/assets/js/application.js @@ -1,12 +1 @@ -require("../css/application.css"); - -const WEBSOCKETS_SERVER = false; - -if ( WEBSOCKETS_SERVER ) { - require("./channels.js"); - WebChannels.load_channels(); - - WebChannels.messageHandlers.push(function(event){ - console.log(event.data); - }); -} +require("../css/application.css"); \ No newline at end of file diff --git a/files/new_app/assets/js/channels.js b/files/new_app/assets/js/channels.js deleted file mode 100644 index 1ef8a4a45..000000000 --- a/files/new_app/assets/js/channels.js +++ /dev/null @@ -1,70 +0,0 @@ -window.WebChannels = {}; -window.WebChannels.load_channels = function() { - const SERVER_HOST = 'localhost'; - const SERVER_PORT = 8001; - - var socket = new WebSocket('ws://' + SERVER_HOST + ':' + SERVER_PORT); - var channels = window.WebChannels; - - channels.channel = socket; - channels.sendMessageTo = sendMessageTo; - - channels.messageHandlers = []; - channels.errorHandlers = []; - channels.openHandlers = []; - channels.closeHandlers = []; - - socket.addEventListener('open', function(event) { - for (var i = 0; i < channels.openHandlers.length; i++) { - var f = channels.openHandlers[i]; - if (typeof f === 'function') { - f(event); - } - } - }); - - socket.addEventListener('message', function(event) { - for (var i = 0; i < channels.messageHandlers.length; i++) { - var f = channels.messageHandlers[i]; - if (typeof f === 'function') { - f(event); - } - } - }); - - socket.addEventListener('error', function(event) { - for (var i = 0; i < channels.errorHandlers.length; i++) { - var f = channels.errorHandlers[i]; - if (typeof f === 'function') { - f(event); - } - } - }); - - socket.addEventListener('close', function(event) { - for (var i = 0; i < channels.closeHandlers.length; i++) { - var f = channels.closeHandlers[i]; - if (typeof f === 'function') { - f(event); - } - } - }); - - // A message maps to a channel route so that channel + message = /action/controller - // The payload is the data made exposed in the Channel Controller - function sendMessageTo(channel, message, payload) { - if (socket.readyState === 1) { - socket.send(JSON.stringify({ - 'channel': channel, - 'message': message, - 'payload': payload - })); - } - } -}; - -window.addEventListener('beforeunload', function (event) { - if (WebChannels.channel.readyState === 1) { - WebChannels.channel.close(); - } -}); diff --git a/files/new_app/public/js/app/channels.js b/files/new_app/public/js/app/channels.js index 840522fd5..78b455dff 100644 --- a/files/new_app/public/js/app/channels.js +++ b/files/new_app/public/js/app/channels.js @@ -52,7 +52,7 @@ window.WebChannels.load_channels = function() { // A message maps to a channel route so that channel + message = /action/controller // The payload is the data made exposed in the Channel Controller - function sendMessageTo(channel, message, payload) { + function sendMessageTo(channel, message, payload = {}) { if (socket.readyState === 1) { socket.send(JSON.stringify({ 'channel': channel, @@ -74,9 +74,39 @@ WebChannels.load_channels(); WebChannels.messageHandlers.push(function(event) { console.log(event.data); }); + +WebChannels.messageHandlers.push(function(event){ + try { + if (event.data.startsWith('{') && event.data.endsWith('}')) { + window.parse_payload(JSON.parse(event.data)); + } else { + window.parse_payload(event.data); + } + } catch (ex) { + console.log(ex); + } +}); + WebChannels.errorHandlers.push(function(event) { console.log(event.data); }); + WebChannels.closeHandlers.push(function(event) { console.log("Server closed WebSocket connection"); -}); \ No newline at end of file +}); + +function parse_payload(json_data) { + console.log("Overwrite window.parse_payload to handle messages from the server") + console.log(json_data); +}; + +// subscribe +function subscribe() { + if (document.readyState === "complete" || document.readyState === "interactive") { + WebChannels.sendMessageTo("__", "subscribe"); + console.log("Subscription ready"); + } else { + console.log("Queuing subscription"); + setTimeout(subscribe, 1000); + } +}; \ No newline at end of file diff --git a/src/AppServer.jl b/src/AppServer.jl index 96d6aa821..14f47ec91 100755 --- a/src/AppServer.jl +++ b/src/AppServer.jl @@ -3,8 +3,10 @@ Handles Http server related functionality, manages requests and responses and th """ module AppServer -import Revise, HTTP, HTTP.IOExtras, HTTP.Sockets, Millboard, MbedTLS, URIParser, Sockets, Distributed, Logging -import Genie, Genie.Configuration, Genie.Sessions, Genie.Flax, Genie.Router, Genie.WebChannels, Genie.Exceptions +import Revise +import HTTP, HTTP.IOExtras, HTTP.Sockets +import Millboard, MbedTLS, URIParser, Sockets, Distributed, Logging +import Genie, Genie.Configuration, Genie.Sessions, Genie.Flax, Genie.Router, Genie.WebChannels, Genie.Exceptions, Genie.Headers mutable struct ServersCollection webserver::Union{Task,Nothing} @@ -72,62 +74,22 @@ function startup(port::Int = Genie.config.server_port, host::String = Genie.conf end -""" - set_headers!(req::HTTP.Request, res::HTTP.Response, app_response::HTTP.Response) :: HTTP.Response - -Configures the response headers. -""" -function set_headers!(req::HTTP.Request, res::HTTP.Response, app_response::HTTP.Response) :: HTTP.Response - if req.method == Genie.Router.OPTIONS || req.method == Genie.Router.GET - - request_origin = get(Dict(req.headers), "Origin", "") - - allowed_origin_dict = Dict("Access-Control-Allow-Origin" => - in(request_origin, Genie.config.cors_allowed_origins) - ? request_origin - : strip(Genie.config.cors_headers["Access-Control-Allow-Origin"]) - ) - - app_response.headers = [d for d in merge(Genie.config.cors_headers, allowed_origin_dict, Dict(res.headers), Dict(app_response.headers))] - end - - app_response.headers = [d for d in merge(Dict(res.headers), Dict(app_response.headers))] - - app_response -end - - -""" - sign_response!(res::HTTP.Response) :: HTTP.Response - -Adds a signature header to the response using the value in `Genie.config.server_signature`. -If `Genie.config.server_signature` is empty, the header is not added. -""" -function sign_response!(res::HTTP.Response) :: HTTP.Response - headers = Dict(res.headers) - isempty(Genie.config.server_signature) || (headers["Server"] = Genie.config.server_signature) - - res.headers = [k for k in headers] - res -end - - """ handle_request(req::HTTP.Request, res::HTTP.Response, ip::IPv4 = IPv4(Genie.config.server_host)) :: HTTP.Response Http server handler function - invoked when the server gets a request. """ function handle_request(req::HTTP.Request, res::HTTP.Response, ip::Sockets.IPv4 = Sockets.IPv4(Genie.config.server_host)) :: HTTP.Response - isempty(Genie.config.server_signature) && sign_response!(res) + isempty(Genie.config.server_signature) && Headers.sign_response!(res) try - req = normalize_headers(req) + req = Headers.normalize_headers(req) catch ex @error ex end try - set_headers!(req, res, Genie.Router.route_request(req, res, ip)) + Headers.set_headers!(req, res, Genie.Router.route_request(req, res, ip)) catch ex @error ex rethrow(ex) @@ -135,25 +97,6 @@ function handle_request(req::HTTP.Request, res::HTTP.Response, ip::Sockets.IPv4 end -function normalize_headers(req::HTTP.Request) :: HTTP.Request - headers = Dict(req.headers) - normalized_headers = Dict{String,String}() - - for (k,v) in headers - normalized_headers[normalize_header_key(string(k))] = string(v) - end - - req.headers = [k for k in normalized_headers] - - req -end - - -function normalize_header_key(key::String) :: String - join(map(x -> uppercasefirst(lowercase(x)), split(key, '-')), '-') -end - - """ setup_http_handler(req::HTTP.Request, res::HTTP.Response = HTTP.Response()) :: HTTP.Response diff --git a/src/Assets.jl b/src/Assets.jl index 05d55dfcf..fdeb1ef3b 100644 --- a/src/Assets.jl +++ b/src/Assets.jl @@ -3,7 +3,8 @@ Helper functions for working with frontend assets (including JS, CSS, etc files) """ module Assets -import Genie, Genie.Configuration +import Revise +import Genie, Genie.Configuration, Genie.Router, Genie.WebChannels export include_asset, css_asset, js_asset @@ -49,4 +50,45 @@ function js_asset(file_name::String; fingerprinted::Bool = Genie.config.assets_f include_asset(:js, file_name, fingerprinted = fingerprinted) end + +""" + embeded(path::String) :: String + +Reads and outputs the file at `path` within Genie's root package dir +""" +function embedded(path::String) :: String + read(joinpath(@__DIR__, "..", path) |> normpath, String) end + + +""" + channels() :: String + +Outputs the channels.js file included with the Genie package +""" +function channels() :: String + embedded(joinpath("files", "new_app", "public", "js", "app", "channels.js")) +end + + +function channels_script() :: String +""" + +""" +end + + +function channels_support() :: String + Router.route("/__/channels.js", channels) + + Router.channel("/__/subscribe") do + WebChannels.subscribe(Genie.Requests.wsclient(), "__") + "OK" + end + + "" +end + +end \ No newline at end of file diff --git a/src/Configuration.jl b/src/Configuration.jl index e515cb3cf..38702e3b1 100755 --- a/src/Configuration.jl +++ b/src/Configuration.jl @@ -3,7 +3,7 @@ Core genie configuration / settings functionality. """ module Configuration -const GENIE_VERSION = v"0.22.8" +const GENIE_VERSION = v"0.22.9" import Logging import Genie diff --git a/src/Genie.jl b/src/Genie.jl index be87ea421..aebac0536 100755 --- a/src/Genie.jl +++ b/src/Genie.jl @@ -37,9 +37,10 @@ include("Sessions.jl") include("Input.jl") include("Flax.jl") include("Renderer.jl") -include("Assets.jl") include("Router.jl") include("WebChannels.jl") +include("Headers.jl") +include("Assets.jl") include("AppServer.jl") include("Commands.jl") include("Cache.jl") diff --git a/src/Headers.jl b/src/Headers.jl new file mode 100644 index 000000000..5136a63e1 --- /dev/null +++ b/src/Headers.jl @@ -0,0 +1,64 @@ +module Headers + +import Revise, HTTP +import Genie + +""" + set_headers!(req::HTTP.Request, res::HTTP.Response, app_response::HTTP.Response) :: HTTP.Response + +Configures the response headers. +""" +function set_headers!(req::HTTP.Request, res::HTTP.Response, app_response::HTTP.Response) :: HTTP.Response + if req.method == Genie.Router.OPTIONS || req.method == Genie.Router.GET + + request_origin = get(Dict(req.headers), "Origin", "") + + allowed_origin_dict = Dict("Access-Control-Allow-Origin" => + in(request_origin, Genie.config.cors_allowed_origins) + ? request_origin + : strip(Genie.config.cors_headers["Access-Control-Allow-Origin"]) + ) + + app_response.headers = [d for d in merge(Genie.config.cors_headers, allowed_origin_dict, Dict(res.headers), Dict(app_response.headers))] + end + + app_response.headers = [d for d in merge(Dict(res.headers), Dict(app_response.headers))] + + app_response +end + + +""" + sign_response!(res::HTTP.Response) :: HTTP.Response + +Adds a signature header to the response using the value in `Genie.config.server_signature`. +If `Genie.config.server_signature` is empty, the header is not added. +""" +function sign_response!(res::HTTP.Response) :: HTTP.Response + headers = Dict(res.headers) + isempty(Genie.config.server_signature) || (headers["Server"] = Genie.config.server_signature) + + res.headers = [k for k in headers] + res +end + + +function normalize_headers(req::HTTP.Request) :: HTTP.Request + headers = Dict(req.headers) + normalized_headers = Dict{String,String}() + + for (k,v) in headers + normalized_headers[normalize_header_key(string(k))] = string(v) + end + + req.headers = [k for k in normalized_headers] + + req +end + + +function normalize_header_key(key::String) :: String + join(map(x -> uppercasefirst(lowercase(x)), split(key, '-')), '-') +end + +end \ No newline at end of file diff --git a/src/Requests.jl b/src/Requests.jl index 7c2706098..de30d0215 100644 --- a/src/Requests.jl +++ b/src/Requests.jl @@ -3,10 +3,11 @@ Collection of utilities for working with Requests data """ module Requests +import Revise import Genie, Genie.Router, Genie.Input import HTTP, Reexport -export jsonpayload, rawpayload, filespayload, postpayload, getpayload, request, matchedroute, matchedchannel +export jsonpayload, rawpayload, filespayload, postpayload, getpayload, request, matchedroute, matchedchannel, wsclient export infilespayload, filename, payload Reexport.@reexport using HTTP @@ -224,4 +225,14 @@ function matchedchannel() :: Genie.Router.Channel Router.@params(Genie.PARAMS_CHANNELS_KEY) end + +""" + wsclient() :: HTTP.WebSockets.WebSocket + +The web sockets client for the current request. +""" +function wsclient() :: HTTP.WebSockets.WebSocket + Router.@params(Genie.PARAMS_WS_CLIENT) +end + end \ No newline at end of file diff --git a/src/Router.jl b/src/Router.jl index 886f62f20..84c0a8708 100755 --- a/src/Router.jl +++ b/src/Router.jl @@ -552,7 +552,7 @@ function match_channels(req, msg::String, ws_client, params::Params, session::Un action_controller_params(c.action, params) - params.collection[Genie.PARAMS_CHANNEL_KEY] = c + params.collection[Genie.PARAMS_CHANNELS_KEY] = c task_local_storage(:__params, params.collection) @@ -832,19 +832,19 @@ function setup_base_params(req::HTTP.Request = HTTP.Request(), res::Union{HTTP.R params[Genie.PARAMS_RESPONSE_KEY] = res params[Genie.PARAMS_SESSION_KEY] = session params[Genie.PARAMS_FLASH_KEY] = Genie.config.session_auto_start ? - begin - if session !== nothing - s = Genie.Sessions.get(session, Genie.PARAMS_FLASH_KEY) - if s === nothing - "" + begin + if session !== nothing + s = Genie.Sessions.get(session, Genie.PARAMS_FLASH_KEY) + if s === nothing + "" + else + Genie.Sessions.unset!(session, Genie.PARAMS_FLASH_KEY) + s + end else - Genie.Sessions.unset!(session, Genie.PARAMS_FLASH_KEY) - s + "" end - else - "" - end - end : "" + end : "" params[Genie.PARAMS_POST_KEY] = Dict{Symbol,Any}() params[Genie.PARAMS_GET_KEY] = Dict{Symbol,Any}() diff --git a/src/WebChannels.jl b/src/WebChannels.jl index a1e62a9eb..be8957bcf 100755 --- a/src/WebChannels.jl +++ b/src/WebChannels.jl @@ -10,6 +10,10 @@ import Genie, Genie.Renderer const ClientId = UInt # web socket hash const ChannelName = String +struct ChannelNotFoundException <: Exception + name::ChannelName +end + mutable struct ChannelClient client::HTTP.WebSockets.WebSocket channels::Vector{ChannelName} @@ -206,13 +210,16 @@ Pushes `msg` (and `payload`) to all the clients subscribed to the channels in `c function broadcast(channels::Union{ChannelName,Vector{ChannelName}}, msg::String) :: Bool isa(channels, Array) || (channels = ChannelName[channels]) - try - for channel in channels - for client in SUBSCRIPTIONS[channel] + for channel in channels + haskey(SUBSCRIPTIONS, channel) || throw(ChannelNotFoundException(channel)) + + for client in SUBSCRIPTIONS[channel] + try message(client, msg) + catch ex + @error ex end end - catch end true @@ -220,15 +227,16 @@ end function broadcast(channels::Union{ChannelName,Vector{ChannelName}}, msg::String, payload::Dict) :: Bool isa(channels, Array) || (channels = [channels]) - try - for channel in channels - in(channel, keys(SUBSCRIPTIONS)) || continue + for channel in channels + in(channel, keys(SUBSCRIPTIONS)) || continue - for client in SUBSCRIPTIONS[channel] + for client in SUBSCRIPTIONS[channel] + try message(client, ChannelMessage(channel, client, msg, payload) |> Renderer.JSONParser.json) + catch ex + @error ex end end - catch end true diff --git a/test/Manifest.toml b/test/Manifest.toml index 3d6edd832..75c94d536 100644 --- a/test/Manifest.toml +++ b/test/Manifest.toml @@ -52,7 +52,7 @@ uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" uuid = "a63ad114-7e13-5084-954f-fe012c677804" [[Pkg]] -deps = ["Dates", "LibGit2", "Markdown", "Printf", "REPL", "Random", "SHA", "UUIDs"] +deps = ["Dates", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "UUIDs"] uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" [[Printf]] @@ -94,19 +94,13 @@ uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" deps = ["LinearAlgebra", "SparseArrays"] uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" -[[Suppressor]] -deps = ["Compat"] -git-tree-sha1 = "a39342763981e766a72938b59032dc02a2d74da5" -uuid = "fd094767-a336-5f1f-9728-57cf17d0bbfb" -version = "0.1.1" - [[Test]] deps = ["Distributed", "InteractiveUtils", "Logging", "Random"] uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [[TestSetExtensions]] -deps = ["DeepDiffs", "Distributed", "Suppressor", "Test"] -path = "/Users/adrian/.julia/dev/TestSetExtensions" +deps = ["DeepDiffs", "Distributed", "Test"] +git-tree-sha1 = "3a2919a78b04c29a1a57b05e1618e473162b15d0" uuid = "98d24dd4-01ad-11ea-1b02-c9a08f80db04" version = "2.0.0" diff --git a/test/tests_assets_rendering.jl b/test/tests_assets_rendering.jl new file mode 100644 index 000000000..babf19b51 --- /dev/null +++ b/test/tests_assets_rendering.jl @@ -0,0 +1,15 @@ +@safetestset "Assets rendering" begin + + @safetestset "Embedded assets" begin + using Genie + using Genie.Renderer + using Genie.Assets + + @test Assets.embedded(joinpath("files", "new_app", "public", "js", "app", "channels.js")) == Assets.channels() + + @test Assets.channels()[1:23] == "window.WebChannels = {}" + + @test Assets.channels_script()[1:34] == "