diff --git a/assets/js/channels.js b/assets/js/channels.js
index a89e1ed58..37fd066ff 100644
--- a/assets/js/channels.js
+++ b/assets/js/channels.js
@@ -1,21 +1,42 @@
/*
-** channels.js v1.4 // 7th Nov 2024
+** channels.js v1.3 // 7th July 2023
** Author: Adrian Salceanu and contributors // @essenciary
** GenieFramework.com // Genie.jl
*/
-Genie.WebChannels = {};
-var _lastMessageAt = 0;
-
-Genie.WebChannels.initialize = function() {
- Genie.WebChannels.sendMessageTo = sendMessageTo;
- Genie.WebChannels.messageHandlers = [];
- Genie.WebChannels.errorHandlers = [];
- Genie.WebChannels.openHandlers = [];
- Genie.WebChannels.closeHandlers = [];
- Genie.WebChannels.subscriptionHandlers = [];
- Genie.WebChannels.processingHandlers = [];
-
- const waitForOpenConnection = () => {
+
+Genie.initWebChannel = function(channel = Genie.Settings.webchannels_default_route) {
+ // A message maps to a channel route so that channel + message = /action/controller
+ // The payload is the data exposed in the Channel Controller
+ var WebChannel = {};
+ WebChannel.sendMessageTo = async (channel, message, payload = {}) => {
+ let msg = JSON.stringify({
+ 'channel': channel,
+ 'message': message,
+ 'payload': payload
+ });
+ if (WebChannel.socket.readyState === 1) {
+ WebChannel.socket.send(msg);
+ } else if (Object.keys(payload).length > 0) {
+ try {
+ await waitForOpenConnection(WebChannel)
+ WebChannel.socket.send(msg);
+ } catch (err) {
+ console.error(err);
+ console.warn('Could not send message: ' + msg);
+ }
+ }
+ WebChannel.lastMessageAt = Date.now();
+ }
+
+ WebChannel.channel = channel;
+ WebChannel.messageHandlers = [];
+ WebChannel.errorHandlers = [];
+ WebChannel.openHandlers = [];
+ WebChannel.closeHandlers = [];
+ WebChannel.subscriptionHandlers = [];
+ WebChannel.processingHandlers = [];
+
+ const waitForOpenConnection = (WebChannel) => {
return new Promise((resolve, reject) => {
const maxNumberOfAttempts = Genie.Settings.webchannels_connection_attempts;
const delay = Genie.Settings.webchannels_reconnect_delay;
@@ -25,7 +46,7 @@ Genie.WebChannels.initialize = function() {
if (currentAttempt > maxNumberOfAttempts - 1) {
clearInterval(interval);
reject(new Error('Maximum number of attempts exceeded: Message not sent.'));
- } else if (eval('Genie.WebChannels.socket.readyState') === 1) {
+ } else if (WebChannel.socket.readyState === 1) {
clearInterval(interval);
resolve();
};
@@ -34,84 +55,140 @@ Genie.WebChannels.initialize = function() {
})
}
- // A message maps to a channel route so that channel + message = /action/controller
- // The payload is the data exposed in the Channel Controller
- async function sendMessageTo(channel, message, payload = {}) {
- let msg = JSON.stringify({
- 'channel': channel,
- 'message': message,
- 'payload': payload
- });
- if (Genie.WebChannels.socket.readyState === 1) {
- Genie.WebChannels.socket.send(msg);
- } else if (Object.keys(payload).length > 0) {
- try {
- await waitForOpenConnection()
- eval('Genie.WebChannels.socket').send(msg);
- } catch (err) {
- console.error(err);
- console.warn('Could not send message: ' + msg);
+ WebChannel.socket = newSocketConnection(WebChannel);
+
+ WebChannel.processingHandlers.push(event => {
+ window.parse_payload(WebChannel, event.data);
+ });
+
+ WebChannel.messageHandlers.push(event => {
+ try {
+ let ed = event.data.trim();
+
+ // if payload is marked as base64 encoded, remove the marker and decode
+ if (ed.startsWith(Genie.Settings.webchannels_base64_marker)) {
+ ed = atob(ed.substring(Genie.Settings.webchannels_base64_marker.length).trim());
}
+
+ if (ed.startsWith('{') && ed.endsWith('}')) {
+ window.parse_payload(WebChannel, JSON.parse(ed, Genie.Revivers.reviver));
+ } else if (ed.startsWith(Genie.Settings.webchannels_eval_command)) {
+ return Function('"use strict";return (' + ed.substring(Genie.Settings.webchannels_eval_command.length).trim() + ')')();
+ } else if (ed == 'Subscription: OK') {
+ window.subscription_ready(WebChannel);
+ } else {
+ window.process_payload(WebChannel, event);
+ }
+ } catch (ex) {
+ console.error(ex);
+ console.error(event.data);
}
- _lastMessageAt = Date.now();
- }
-}
-
-let wsconnectionalert_triggered = false;
-let wsconnectionalert_elemid = 'wsconnectionalert';
+ });
+
+ WebChannel.errorHandlers.push(event => {
+ if (isDev()) {
+ console.error(event.target);
+ }
+ });
+
+ WebChannel.closeHandlers.push(event => {
+ if (isDev()) {
+ console.warn('WebSocket connection closed: ' + event.code + ' ' + event.reason + ' ' + event.wasClean);
+ }
+ });
+
+ WebChannel.closeHandlers.push(event => {
+ displayAlert(WebChannel);
+ if (Genie.Settings.webchannels_autosubscribe) {
+ if (isDev()) console.info('Attempting to reconnect! ');
+ setTimeout(function() {
+ WebChannel.socket = newSocketConnection(WebChannel);
+ }, Genie.Settings.webchannels_reconnect_delay);
+ }
+ });
+
+ WebChannel.openHandlers.push(event => {
+ if (Genie.Settings.webchannels_autosubscribe) {
+ subscribe(WebChannel);
+ }
+ });
+
+ window.addEventListener('beforeunload', _ => {
+ if (isDev()) {
+ console.info('Preparing to unload');
+ }
+
+ if (Genie.Settings.webchannels_autosubscribe) {
+ unsubscribe(WebChannel);
+ }
+
+ if (WebChannel.socket.readyState === 1) {
+ WebChannel.socket.close();
+ }
+ });
-function displayAlert(content = 'Can not reach the server. Trying to reconnect...') {
- if (document.getElementById(wsconnectionalert_elemid) !== null || wsconnectionalert_triggered) return;
+ Genie.AllWebChannels = Genie.AllWebChannels || [];
+ Genie.AllWebChannels.push(WebChannel);
+ Genie.WebChannels = WebChannel; // for compatibility with older code
- let elem = document.createElement('div');
- elem.id = wsconnectionalert_elemid;
- elem.style.cssText = 'position:fixed;top:0;width:100%;z-index:100;background:#e63946;color:#f1faee;text-align:center;';
- elem.style.height = '1.8em';
- elem.innerHTML = content;
+ return WebChannel
+}
- let elemspacer = document.createElement('div');
- elemspacer.id = wsconnectionalert_elemid + 'spacer';
- elemspacer.style.height = (Genie.Settings.webchannels_alert_overlay) ? 0 : elem.style.height;
+Genie.wsconnectionalert_elemid = 'wsconnectionalert';
+Genie.allConnected = function() {
+ for (let i = 0; i < Genie.AllWebChannels.length; i++) {
+ if (Genie.AllWebChannels[i].ws_disconnected) {
+ return false
+ }
+ }
+ return true
+}
- wsconnectionalert_triggered = true;
+function displayAlert(WebChannel, content = 'Can not reach the server. Trying to reconnect...') {
+ if (document.getElementById(Genie.wsconnectionalert_elemid) || WebChannel.ws_disconnected) return;
+
+ let allConnected = Genie.allConnected();
+ WebChannel.ws_disconnected = true;
+
+ WebChannel.alertTimeout = setTimeout(() => {
+ if (Genie.Settings.webchannels_show_alert && allConnected) {
+ let elem = document.createElement('div');
+ elem.id = Genie.wsconnectionalert_elemid;
+ elem.style.cssText = 'position:fixed;top:0;width:100%;z-index:100;background:#e63946;color:#f1faee;text-align:center;';
+ elem.style.height = '1.8em';
+ elem.innerHTML = content;
+
+ let elemspacer = document.createElement('div');
+ elemspacer.id = Genie.wsconnectionalert_elemid + 'spacer';
+ elemspacer.style.height = (Genie.Settings.webchannels_alert_overlay) ? 0 : elem.style.height;
- Genie.WebChannels.alertTimeout = setTimeout(() => {
- if (Genie.Settings.webchannels_show_alert) {
document.body.prepend(elem);
document.body.prepend(elemspacer);
}
- if (window.GENIEMODEL) GENIEMODEL.ws_disconnected = true;
+ if (WebChannel.parent) WebChannel.parent.ws_disconnected = true;
}, Genie.Settings.webchannels_server_gone_alert_timeout);
}
-function deleteAlert() {
- clearInterval(Genie.WebChannels.alertTimeout);
-
- if (window.GENIEMODEL) GENIEMODEL.ws_disconnected = false;
-
- let elem = document.getElementById(wsconnectionalert_elemid);
- let elemspacer = document.getElementById(wsconnectionalert_elemid + 'spacer');
-
- if (elem !== null) {
- elem.remove();
- }
+function deleteAlert(WebChannel) {
+ WebChannel.ws_disconnected = false;
+ clearInterval(WebChannel.alertTimeout);
+ if (WebChannel.parent) WebChannel.parent.ws_disconnected = false;
- if (elemspacer !== null) {
- elemspacer.remove();
+ if (Genie.allConnected()) {
+ document.getElementById(Genie.wsconnectionalert_elemid)?.remove();
+ document.getElementById(Genie.wsconnectionalert_elemid + 'spacer')?.remove();
}
-
- wsconnectionalert_triggered = false;
}
-function newSocketConnection(host = Genie.Settings.websockets_exposed_host) {
+function newSocketConnection(WebChannel, host = Genie.Settings.websockets_exposed_host) {
let ws = new WebSocket(Genie.Settings.websockets_protocol + '//' + host
+ (Genie.Settings.websockets_exposed_port > 0 ? (':' + Genie.Settings.websockets_exposed_port) : '')
+ ( ((Genie.Settings.base_path.trim() === '' || Genie.Settings.base_path.startsWith('/')) ? '' : '/') + Genie.Settings.base_path)
+ ( ((Genie.Settings.websockets_base_path.trim() === '' || Genie.Settings.websockets_base_path.startsWith('/')) ? '' : '/') + Genie.Settings.websockets_base_path));
ws.addEventListener('open', event => {
- for (let i = 0; i < Genie.WebChannels.openHandlers.length; i++) {
- let f = Genie.WebChannels.openHandlers[i];
+ for (let i = 0; i < WebChannel.openHandlers.length; i++) {
+ let f = WebChannel.openHandlers[i];
if (typeof f === 'function') {
f(event);
}
@@ -119,18 +196,18 @@ function newSocketConnection(host = Genie.Settings.websockets_exposed_host) {
});
ws.addEventListener('message', event => {
- for (let i = 0; i < Genie.WebChannels.messageHandlers.length; i++) {
- let f = Genie.WebChannels.messageHandlers[i];
+ for (let i = 0; i < WebChannel.messageHandlers.length; i++) {
+ let f = WebChannel.messageHandlers[i];
if (typeof f === 'function') {
f(event);
}
}
- _lastMessageAt = Date.now();
+ WebChannel.lastMessageAt = Date.now();
});
ws.addEventListener('error', event => {
- for (let i = 0; i < Genie.WebChannels.errorHandlers.length; i++) {
- let f = Genie.WebChannels.errorHandlers[i];
+ for (let i = 0; i < WebChannel.errorHandlers.length; i++) {
+ let f = WebChannel.errorHandlers[i];
if (typeof f === 'function') {
f(event);
}
@@ -138,8 +215,8 @@ function newSocketConnection(host = Genie.Settings.websockets_exposed_host) {
});
ws.addEventListener('close', event => {
- for (let i = 0; i < Genie.WebChannels.closeHandlers.length; i++) {
- let f = Genie.WebChannels.closeHandlers[i];
+ for (let i = 0; i < WebChannel.closeHandlers.length; i++) {
+ let f = WebChannel.closeHandlers[i];
if (typeof f === 'function') {
f(event);
}
@@ -152,15 +229,12 @@ function newSocketConnection(host = Genie.Settings.websockets_exposed_host) {
});
ws.addEventListener('error', _ => {
- // Genie.WebChannels.socket = newSocketConnection();
+ // WebChannel.socket = newSocketConnection();
});
return ws
}
-Genie.WebChannels.initialize();
-Genie.WebChannels.socket = newSocketConnection();
-
// --------------- Revivers ---------------
Genie.Revivers = {};
@@ -171,6 +245,7 @@ Genie.Revivers.rebuildReviver = function() {
}
Genie.Revivers.addReviver = function(reviver) {
+ if (Genie.Revivers.revivers.includes(reviver)) return
Genie.Revivers.revivers.push(reviver)
Genie.Revivers.rebuildReviver()
}
@@ -186,122 +261,55 @@ Genie.Revivers.revive_undefined = function(key, value) {
Genie.Revivers.revivers = [Genie.Revivers.revive_undefined]
Genie.Revivers.rebuildReviver()
-window.addEventListener('beforeunload', _ => {
- if (isDev()) {
- console.info('Preparing to unload');
- }
-
- if (Genie.Settings.webchannels_autosubscribe) {
- unsubscribe();
- }
-
- if (Genie.WebChannels.socket.readyState === 1) {
- Genie.WebChannels.socket.close();
- }
-});
-
-Genie.WebChannels.processingHandlers.push(event => {
- window.parse_payload(event.data);
-});
-
-Genie.WebChannels.messageHandlers.push(event => {
- try {
- let ed = event.data.trim();
-
- // if payload is marked as base64 encoded, remove the marker and decode
- if (ed.startsWith(Genie.Settings.webchannels_base64_marker)) {
- ed = atob(ed.substring(Genie.Settings.webchannels_base64_marker.length).trim());
- }
-
- if (ed.startsWith('{') && ed.endsWith('}')) {
- window.parse_payload(JSON.parse(ed, Genie.Revivers.reviver));
- } else if (ed.startsWith(Genie.Settings.webchannels_eval_command)) {
- return Function('"use strict";return (' + ed.substring(Genie.Settings.webchannels_eval_command.length).trim() + ')')();
- } else if (ed == 'Subscription: OK') {
- window.subscription_ready();
- } else {
- window.process_payload(event);
- }
- } catch (ex) {
- console.error(ex);
- console.error(event.data);
- }
-});
-
-Genie.WebChannels.errorHandlers.push(event => {
- if (isDev()) {
- console.error(event.data);
- }
-});
-
-Genie.WebChannels.closeHandlers.push(event => {
- if (isDev()) {
- console.warn('WebSocket connection closed: ' + event.code + ' ' + event.reason + ' ' + event.wasClean);
- }
-});
-
-Genie.WebChannels.closeHandlers.push(event => {
- if (Genie.Settings.webchannels_autosubscribe) {
- if (isDev()) console.info('Attempting to reconnect! ');
- // setTimeout(function() {
- Genie.WebChannels.socket = newSocketConnection();
- subscribe();
- // }, Genie.Settings.webchannels_timeout);
- }
-});
-
-Genie.WebChannels.openHandlers.push(event => {
- if (Genie.Settings.webchannels_autosubscribe) {
- subscribe();
- }
-});
-
-function parse_payload(json_data) {
+function parse_payload(WebChannel, json_data) {
if (isDev()) {
console.info('Overwrite window.parse_payload to handle messages from the server');
console.info(json_data);
}
};
-function process_payload(event) {
- for (let i = 0; i < Genie.WebChannels.processingHandlers.length; i++) {
- let f = Genie.WebChannels.processingHandlers[i];
+function process_payload(WebChannel, event) {
+ for (let i = 0; i < WebChannel.processingHandlers.length; i++) {
+ let f = WebChannel.processingHandlers[i];
if (typeof f === 'function') {
f(event);
}
}
};
-function subscription_ready() {
- for (let i = 0; i < Genie.WebChannels.subscriptionHandlers.length; i++) {
- let f = Genie.WebChannels.subscriptionHandlers[i];
+function subscription_ready(WebChannel) {
+ for (let i = 0; i < WebChannel.subscriptionHandlers.length; i++) {
+ let f = WebChannel.subscriptionHandlers[i];
if (typeof f === 'function') {
f();
}
}
-
- deleteAlert();
+ deleteAlert(WebChannel);
if (isDev()) console.info('Subscription ready');
};
-function subscribe(trial = 1) {
- if (Genie.WebChannels.socket.readyState && (document.readyState === 'complete' || document.readyState === 'interactive')) {
- Genie.WebChannels.sendMessageTo(window.Genie.Settings.webchannels_default_route, window.Genie.Settings.webchannels_subscribe_channel);
- } else if (trial < Genie.Settings.webchannels_subscription_trails) {
+function subscribe(WebChannel, trial = 1) {
+ if (WebChannel.socket.readyState == 1 && (document.readyState === 'complete' || document.readyState === 'interactive')) {
+ WebChannel.sendMessageTo(WebChannel.channel, window.Genie.Settings.webchannels_subscribe_channel);
+ } else if (trial < Genie.Settings.webchannels_subscription_trials) {
if (isDev()) console.warn('Queuing subscription');
trial++;
- setTimeout(subscribe.bind(this, trial), Genie.Settings.webchannels_timeout);
+ setTimeout(subscribe.bind(this, WebChannel, trial), Genie.Settings.webchannels_timeout);
} else {
- displayAlert();
+ displayAlert(WebChannel);
}
};
-function unsubscribe() {
- Genie.WebChannels.sendMessageTo(window.Genie.Settings.webchannels_default_route, window.Genie.Settings.webchannels_unsubscribe_channel);
+function unsubscribe(WebChannel) {
+ WebChannel.sendMessageTo(WebChannel.channel, window.Genie.Settings.webchannels_unsubscribe_channel);
if (isDev()) console.info('Unsubscription completed');
};
function isDev() {
return Genie.Settings.env === 'dev';
-}
\ No newline at end of file
+}
+
+// --------------- Initialize WebChannel ---------------
+
+// Genie.WebChannels = Genie.initWebChannel();
diff --git a/assets/js/webthreads.js b/assets/js/webthreads.js
index 68021a485..192544d59 100644
--- a/assets/js/webthreads.js
+++ b/assets/js/webthreads.js
@@ -97,7 +97,7 @@ function subscribe(trial = 1) {
if (document.readyState === 'complete' || document.readyState === 'interactive') {
Genie.WebChannels.channel.start('GET', uri_factory(Genie.Settings.webchannels_subscribe_channel), {}, '');
pull();
- } else if (trial < Genie.Settings.webchannels_subscription_trails) {
+ } else if (trial < Genie.Settings.webchannels_subscription_trials) {
if (isDev()) console.warn('Queuing subscription');
trial++;
setTimeout(subscribe.bind(this, trial), Genie.WebChannels.poll_interval);
diff --git a/src/Assets.jl b/src/Assets.jl
index 551002934..c97a50f07 100644
--- a/src/Assets.jl
+++ b/src/Assets.jl
@@ -240,7 +240,7 @@ function js_settings(channel::String = Genie.config.webchannels_default_route) :
:webchannels_server_gone_alert_timeout => Genie.config.webchannels_server_gone_alert_timeout,
:webchannels_connection_attempts => Genie.config.webchannels_connection_attempts,
:webchannels_reconnect_delay => Genie.config.webchannels_reconnect_delay,
- :webchannels_subscription_trails => Genie.config.webchannels_subscription_trails,
+ :webchannels_subscription_trials => Genie.config.webchannels_subscription_trials,
:webchannels_show_alert => Genie.config.webchannels_show_alert,
:webchannels_alert_overlay => Genie.config.webchannels_alert_overlay,
@@ -403,9 +403,9 @@ end
function channels_script_tag(channel::AbstractString = Genie.config.webchannels_default_route) :: String
if ! external_assets()
- Genie.Renderer.Html.script(src = assets_endpoint())
+ Genie.Renderer.Html.script(src = assets_endpoint(), defer = true)
else
- Genie.Renderer.Html.script([channels(channel)])
+ Genie.Renderer.Html.script([channels(channel)], defer = true)
end
end
diff --git a/src/Configuration.jl b/src/Configuration.jl
index 29e150361..0a9b498ea 100755
--- a/src/Configuration.jl
+++ b/src/Configuration.jl
@@ -259,7 +259,7 @@ Base.@kwdef mutable struct Settings
webchannels_server_gone_alert_timeout::Int = 10_000 # 10 seconds
webchannels_connection_attempts = 10
webchannels_reconnect_delay = 500 # milliseconds
- webchannels_subscription_trails = 4
+ webchannels_subscription_trials = 4
webchannels_show_alert::Bool = true
webchannels_alert_overlay::Bool = false
diff --git a/test/tests_Assets.jl b/test/tests_Assets.jl
index 263c92231..91c6c5e43 100644
--- a/test/tests_Assets.jl
+++ b/test/tests_Assets.jl
@@ -14,7 +14,7 @@
using Genie, Genie.Assets
Genie.config.websockets_port = 8000 # state gets affected depending on how tests are run -- let's set it explicitly
- @test strip(js_settings()) == strip("window.Genie = {};\nGenie.Settings = {\"websockets_exposed_port\":window.location.port,\"server_host\":\"127.0.0.1\",\"webchannels_autosubscribe\":true,\"webchannels_reconnect_delay\":500,\"webchannels_subscription_trails\":4,\"env\":\"dev\",\"webchannels_eval_command\":\">eval:\",\"webchannels_alert_overlay\":false,\"websockets_host\":\"127.0.0.1\",\"webchannels_show_alert\":true,\"webthreads_js_file\":\"webthreads.js\",\"webchannels_base64_marker\":\"base64:\",\"webchannels_unsubscribe_channel\":\"unsubscribe\",\"webthreads_default_route\":\"____\",\"webchannels_subscribe_channel\":\"subscribe\",\"server_port\":8000,\"webchannels_keepalive_frequency\":30000,\"websockets_exposed_host\":window.location.hostname,\"webchannels_connection_attempts\":10,\"base_path\":\"\",\"websockets_protocol\":window.location.protocol.replace('http', 'ws'),\"webthreads_pull_route\":\"pull\",\"webchannels_default_route\":\"____\",\"webchannels_server_gone_alert_timeout\":10000,\"webchannels_timeout\":1000,\"webthreads_push_route\":\"push\",\"websockets_port\":8000,\"websockets_base_path\":\"\"};\n")
+ @test strip(js_settings()) == strip("window.Genie = {};\nGenie.Settings = {\"websockets_exposed_port\":window.location.port,\"server_host\":\"127.0.0.1\",\"webchannels_autosubscribe\":true,\"webchannels_reconnect_delay\":500,\"env\":\"dev\",\"webchannels_eval_command\":\">eval:\",\"webchannels_alert_overlay\":false,\"websockets_host\":\"127.0.0.1\",\"webchannels_show_alert\":true,\"webthreads_js_file\":\"webthreads.js\",\"webchannels_base64_marker\":\"base64:\",\"webchannels_unsubscribe_channel\":\"unsubscribe\",\"webthreads_default_route\":\"____\",\"webchannels_subscription_trials\":4,\"webchannels_subscribe_channel\":\"subscribe\",\"server_port\":8000,\"webchannels_keepalive_frequency\":30000,\"websockets_exposed_host\":window.location.hostname,\"webchannels_connection_attempts\":10,\"base_path\":\"\",\"websockets_protocol\":window.location.protocol.replace('http', 'ws'),\"webthreads_pull_route\":\"pull\",\"webchannels_default_route\":\"____\",\"webchannels_server_gone_alert_timeout\":10000,\"webchannels_timeout\":1000,\"webthreads_push_route\":\"push\",\"websockets_port\":8000,\"websockets_base_path\":\"\"};")
end
@safetestset "Embedded assets" begin
@@ -23,7 +23,7 @@
@test Assets.channels()[1:18] == "window.Genie = {};"
@test channels_script()[1:27] == ""
+ @test channels_support() == ""
@test Genie.Router.routes()[1].path == "/genie.jl/$(Genie.Assets.package_version("Genie"))/assets/js/channels.js"
@test Genie.Router.channels()[1].path == "/$(Genie.config.webchannels_default_route)/unsubscribe"
@test Genie.Router.channels()[2].path == "/$(Genie.config.webchannels_default_route)/subscribe"