Move get_updates into a module.

(imported from commit 9a6c0ab1e76dd96abad8626bc5b1fdbd234b2645)
This commit is contained in:
Tim Abbott
2014-01-30 13:25:25 -05:00
committed by Steve Howell
parent 33690d1dc6
commit a7b1b29bf0
8 changed files with 319 additions and 300 deletions

View File

@@ -491,7 +491,7 @@ exports.send_message_success = function (local_id, message_id, start_time, local
setTimeout(function () { setTimeout(function () {
if (exports.send_times_data[message_id].received === undefined) { if (exports.send_times_data[message_id].received === undefined) {
blueslip.error("Restarting get_updates due to delayed receipt of sent message " + message_id); blueslip.error("Restarting get_updates due to delayed receipt of sent message " + message_id);
restart_get_updates(); server_events.restart_get_updates();
} }
}, 5000); }, 5000);
}; };
@@ -544,11 +544,7 @@ function send_message(request) {
} }
exports.transmit_message(request, success, error); exports.transmit_message(request, success, error);
server_events.assert_get_updates_running("Restarting get_updates because it was not running during send");
if (get_updates_xhr === undefined && get_updates_timeout === undefined) {
restart_get_updates({dont_block: true});
blueslip.error("Restarting get_updates because it was not running during send");
}
if (feature_flags.local_echo && locally_echoed) { if (feature_flags.local_echo && locally_echoed) {
clear_compose_box(); clear_compose_box();

300
static/js/server_events.js Normal file
View File

@@ -0,0 +1,300 @@
var server_events = (function () {
var exports = {};
var waiting_on_homeview_load = true;
var events_stored_during_tutorial = [];
var get_updates_xhr;
var get_updates_timeout;
var get_updates_failures = 0;
exports.get_updates_params = {
pointer: -1
};
function get_updates_success(events) {
var messages = [];
var messages_to_update = [];
var new_pointer;
_.each(events, function (event) {
exports.get_updates_params.last_event_id = Math.max(exports.get_updates_params.last_event_id,
event.id);
});
if (tutorial.is_running()) {
events_stored_during_tutorial = events_stored_during_tutorial.concat(events);
return;
}
if (events_stored_during_tutorial.length > 0) {
events = events_stored_during_tutorial.concat(events);
events_stored_during_tutorial = [];
}
_.each(events, function (event) {
switch (event.type) {
case 'message':
var msg = event.message;
msg.flags = event.flags;
if (event.local_message_id !== undefined) {
msg.local_id = event.local_message_id;
}
messages.push(msg);
break;
case 'pointer':
new_pointer = event.pointer;
break;
case 'restart':
reload.initiate({message: "The application has been updated; reloading!"});
break;
case 'update_message':
messages_to_update.push(event);
break;
case 'realm_user':
if (event.op === 'add') {
add_person_in_realm(event.person);
} else if (event.op === 'remove') {
remove_person(event.person);
} else if (event.op === 'update') {
update_person(event.person);
}
break;
case 'stream':
if (event.op === 'update') {
// Legacy: Stream properties are still managed by subs.js on the client side.
subs.update_subscription_properties(event.name, event.property, event.value);
}
break;
case 'subscriptions':
if (event.op === 'add') {
_.each(event.subscriptions, function (subscription) {
$(document).trigger($.Event('subscription_add.zulip',
{subscription: subscription}));
});
} else if (event.op === 'remove') {
_.each(event.subscriptions, function (subscription) {
$(document).trigger($.Event('subscription_remove.zulip',
{subscription: subscription}));
});
} else if (event.op === 'update') {
subs.update_subscription_properties(event.name, event.property, event.value);
} else if (event.op === 'peer_add' || event.op === 'peer_remove') {
_.each(event.subscriptions, function (sub) {
var js_event_type;
if (event.op === 'peer_add') {
js_event_type = 'peer_subscribe.zulip';
stream_data.add_subscriber(sub, event.user_email);
} else if (event.op === 'peer_remove') {
js_event_type = 'peer_unsubscribe.zulip';
stream_data.remove_subscriber(sub, event.user_email);
}
$(document).trigger(js_event_type, {stream_name: sub,
user_email: event.user_email});
});
}
break;
case 'presence':
var users = {};
users[event.email] = event.presence;
activity.set_user_statuses(users, event.server_timestamp);
break;
case 'update_message_flags':
var new_value = event.operation === "add";
switch(event.flag) {
case 'starred':
_.each(event.messages, function (message_id) {
ui.update_starred(message_id, new_value);
});
break;
case 'read':
var msgs_to_update = _.map(event.messages, function (message_id) {
return msg_metadata_cache[message_id];
});
mark_messages_as_read(msgs_to_update, {from: "server"});
break;
}
break;
case 'referral':
referral.update_state(event.referrals.granted, event.referrals.used);
break;
case 'realm_emoji':
emoji.update_emojis(event.realm_emoji);
break;
case 'alert_words':
alert_words.words = event.alert_words;
break;
case 'muted_topics':
muting_ui.handle_updates(event.muted_topics);
break;
case 'realm_filters':
page_params.realm_filters = event.realm_filters;
echo.set_realm_filters(page_params.realm_filters);
break;
}
});
if (messages.length !== 0) {
messages = echo.process_from_server(messages);
insert_new_messages(messages);
}
if (new_pointer !== undefined
&& new_pointer > furthest_read)
{
furthest_read = new_pointer;
server_furthest_read = new_pointer;
home_msg_list.select_id(new_pointer, {then_scroll: true, use_closest: true});
}
if ((home_msg_list.selected_id() === -1) && !home_msg_list.empty()) {
home_msg_list.select_id(home_msg_list.first().id, {then_scroll: false});
}
if (messages_to_update.length !== 0) {
update_messages(messages_to_update);
}
}
exports.get_updates = function get_updates(options) {
options = _.extend({dont_block: false}, options);
exports.get_updates_params.pointer = furthest_read;
exports.get_updates_params.dont_block = options.dont_block || get_updates_failures > 0;
if (exports.get_updates_params.queue_id === undefined) {
exports.get_updates_params.queue_id = page_params.event_queue_id;
exports.get_updates_params.last_event_id = page_params.last_event_id;
}
if (get_updates_xhr !== undefined) {
get_updates_xhr.abort();
}
if (get_updates_timeout !== undefined) {
clearTimeout(get_updates_timeout);
}
get_updates_timeout = undefined;
get_updates_xhr = channel.post({
url: '/json/get_events',
data: exports.get_updates_params,
idempotent: true,
timeout: page_params.poll_timeout,
success: function (data) {
get_updates_xhr = undefined;
get_updates_failures = 0;
$('#connection-error').hide();
get_updates_success(data.events);
get_updates_timeout = setTimeout(get_updates, 0);
},
error: function (xhr, error_type, exn) {
get_updates_xhr = undefined;
// If we are old enough to have messages outside of the
// Tornado cache or if we're old enough that our message
// queue has been garbage collected, immediately reload.
if ((xhr.status === 400) &&
($.parseJSON(xhr.responseText).msg.indexOf("too old") !== -1 ||
$.parseJSON(xhr.responseText).msg.indexOf("Bad event queue id") !== -1)) {
page_params.event_queue_expired = true;
reload.initiate({immediate: true});
}
if (error_type === 'abort') {
// Don't restart if we explicitly aborted
return;
} else if (error_type === 'timeout') {
// Retry indefinitely on timeout.
get_updates_failures = 0;
$('#connection-error').hide();
} else {
get_updates_failures += 1;
}
if (get_updates_failures >= 5) {
$('#connection-error').show();
} else {
$('#connection-error').hide();
}
var retry_sec = Math.min(90, Math.exp(get_updates_failures/2));
get_updates_timeout = setTimeout(get_updates, retry_sec*1000);
}
});
};
exports.assert_get_updates_running = function assert_get_updates_running(error_message) {
if (get_updates_xhr === undefined && get_updates_timeout === undefined) {
exports.restart_get_updates({dont_block: true});
blueslip.error(error_message);
}
};
exports.restart_get_updates = function restart_get_updates(options) {
exports.get_updates(options);
};
exports.force_get_updates = function force_get_updates() {
get_updates_timeout = setTimeout(exports.get_updates, 0);
};
exports.home_view_loaded = function home_view_loaded() {
if (!waiting_on_homeview_load) {
return;
}
waiting_on_homeview_load = false;
$(document).trigger("home_view_loaded.zulip");
};
var watchdog_time = $.now();
setInterval(function () {
var new_time = $.now();
if ((new_time - watchdog_time) > 20000) { // 20 seconds.
// Defensively reset watchdog_time here in case there's an
// exception in one of the event handlers
watchdog_time = new_time;
// Our app's JS wasn't running, which probably means the machine was
// asleep.
$(document).trigger($.Event('unsuspend'));
}
watchdog_time = new_time;
}, 5000);
$(function () {
$(document).on('unsuspend', function () {
// Immediately poll for new updates on unsuspend
blueslip.log("Restarting get_updates due to unsuspend");
get_updates_failures = 0;
exports.restart_get_updates({dont_block: true});
});
});
function cleanup_event_queue() {
// Submit a request to the server to cleanup our event queue
if (page_params.event_queue_expired === true) {
return;
}
channel.del({
url: '/json/events',
data: {queue_id: page_params.event_queue_id}
});
}
window.addEventListener("beforeunload", function (event) {
cleanup_event_queue();
});
return exports;
}());
if (typeof module !== 'undefined') {
module.exports = server_events;
}

View File

@@ -183,7 +183,7 @@ Socket.prototype = {
// If this is a reconnect, network was probably // If this is a reconnect, network was probably
// recently interrupted, so we optimistically restart // recently interrupted, so we optimistically restart
// get_updates // get_updates
restart_get_updates(); server_events.restart_get_updates();
} }
that._is_open = true; that._is_open = true;

View File

@@ -327,7 +327,7 @@ function finale() {
is_running = false; is_running = false;
current_msg_list.clear(); current_msg_list.clear();
// Force a check on new events before we re-render the message list. // Force a check on new events before we re-render the message list.
force_get_updates(); server_events.force_get_updates();
stream_data.set_stream_info(real_stream_info); stream_data.set_stream_info(real_stream_info);
util.show_first_run_message(); util.show_first_run_message();
current_msg_list.rerender(); current_msg_list.rerender();

View File

@@ -1591,7 +1591,7 @@ $(function () {
$('#logout_form').submit(); $('#logout_form').submit();
}); });
$('.restart_get_updates_button').click(function (e) { $('.restart_get_updates_button').click(function (e) {
restart_get_updates({dont_block: true}); server_events.restart_get_updates({dont_block: true});
}); });
$('#api_key_button').click(function (e) { $('#api_key_button').click(function (e) {

View File

@@ -24,11 +24,6 @@ var recent_subjects = new Dict({fold_case: true});
var queued_mark_as_read = []; var queued_mark_as_read = [];
var queued_flag_timer; var queued_flag_timer;
var get_updates_params = {
pointer: -1
};
var get_updates_failures = 0;
var load_more_enabled = true; var load_more_enabled = true;
// If the browser hasn't scrolled away from the top of the page // If the browser hasn't scrolled away from the top of the page
// since the last time that we ran load_more_messages(), we do // since the last time that we ran load_more_messages(), we do
@@ -49,12 +44,8 @@ var unread_messages_read_in_narrow = false;
var pointer_update_in_flight = false; var pointer_update_in_flight = false;
var suppress_unread_counts = true; var suppress_unread_counts = true;
var events_stored_during_tutorial = [];
var waiting_on_browser_scroll = true; var waiting_on_browser_scroll = true;
var waiting_on_homeview_load = true;
function add_person(person, in_realm) { function add_person(person, in_realm) {
page_params.people_list.push(person); page_params.people_list.push(person);
people_dict.set(person.email, person); people_dict.set(person.email, person);
@@ -649,7 +640,7 @@ function add_message_metadata(message) {
} }
return cached_msg; return cached_msg;
} }
get_updates_params.last = Math.max(get_updates_params.last || 0, message.id); server_events.get_updates_params.last = Math.max(server_events.get_updates_params.last || 0, message.id);
var involved_people; var involved_people;
@@ -911,250 +902,6 @@ function insert_new_messages(messages) {
stream_list.update_streams_sidebar(); stream_list.update_streams_sidebar();
} }
function get_updates_success(events) {
var messages = [];
var messages_to_update = [];
var new_pointer;
_.each(events, function (event) {
get_updates_params.last_event_id = Math.max(get_updates_params.last_event_id,
event.id);
});
if (tutorial.is_running()) {
events_stored_during_tutorial = events_stored_during_tutorial.concat(events);
return;
}
if (events_stored_during_tutorial.length > 0) {
events = events_stored_during_tutorial.concat(events);
events_stored_during_tutorial = [];
}
_.each(events, function (event) {
switch (event.type) {
case 'message':
var msg = event.message;
msg.flags = event.flags;
if (event.local_message_id !== undefined) {
msg.local_id = event.local_message_id;
}
messages.push(msg);
break;
case 'pointer':
new_pointer = event.pointer;
break;
case 'restart':
reload.initiate({message: "The application has been updated; reloading!"});
break;
case 'update_message':
messages_to_update.push(event);
break;
case 'realm_user':
if (event.op === 'add') {
add_person_in_realm(event.person);
} else if (event.op === 'remove') {
remove_person(event.person);
} else if (event.op === 'update') {
update_person(event.person);
}
break;
case 'stream':
if (event.op === 'update') {
// Legacy: Stream properties are still managed by subs.js on the client side.
subs.update_subscription_properties(event.name, event.property, event.value);
}
break;
case 'subscriptions':
if (event.op === 'add') {
_.each(event.subscriptions, function (subscription) {
$(document).trigger($.Event('subscription_add.zulip',
{subscription: subscription}));
});
} else if (event.op === 'remove') {
_.each(event.subscriptions, function (subscription) {
$(document).trigger($.Event('subscription_remove.zulip',
{subscription: subscription}));
});
} else if (event.op === 'update') {
subs.update_subscription_properties(event.name, event.property, event.value);
} else if (event.op === 'peer_add' || event.op === 'peer_remove') {
_.each(event.subscriptions, function (sub) {
var js_event_type;
if (event.op === 'peer_add') {
js_event_type = 'peer_subscribe.zulip';
stream_data.add_subscriber(sub, event.user_email);
} else if (event.op === 'peer_remove') {
js_event_type = 'peer_unsubscribe.zulip';
stream_data.remove_subscriber(sub, event.user_email);
}
$(document).trigger(js_event_type, {stream_name: sub,
user_email: event.user_email});
});
}
break;
case 'presence':
var users = {};
users[event.email] = event.presence;
activity.set_user_statuses(users, event.server_timestamp);
break;
case 'update_message_flags':
var new_value = event.operation === "add";
switch(event.flag) {
case 'starred':
_.each(event.messages, function (message_id) {
ui.update_starred(message_id, new_value);
});
break;
case 'read':
var msgs_to_update = _.map(event.messages, function (message_id) {
return msg_metadata_cache[message_id];
});
mark_messages_as_read(msgs_to_update, {from: "server"});
break;
}
break;
case 'referral':
referral.update_state(event.referrals.granted, event.referrals.used);
break;
case 'realm_emoji':
emoji.update_emojis(event.realm_emoji);
break;
case 'alert_words':
alert_words.words = event.alert_words;
break;
case 'muted_topics':
muting_ui.handle_updates(event.muted_topics);
break;
case 'realm_filters':
page_params.realm_filters = event.realm_filters;
echo.set_realm_filters(page_params.realm_filters);
break;
}
});
if (messages.length !== 0) {
messages = echo.process_from_server(messages);
insert_new_messages(messages);
}
if (new_pointer !== undefined
&& new_pointer > furthest_read)
{
furthest_read = new_pointer;
server_furthest_read = new_pointer;
home_msg_list.select_id(new_pointer, {then_scroll: true, use_closest: true});
}
if ((home_msg_list.selected_id() === -1) && !home_msg_list.empty()) {
home_msg_list.select_id(home_msg_list.first().id, {then_scroll: false});
}
if (messages_to_update.length !== 0) {
update_messages(messages_to_update);
}
}
var get_updates_xhr;
var get_updates_timeout;
function get_updates(options) {
options = _.extend({dont_block: false}, options);
get_updates_params.pointer = furthest_read;
get_updates_params.dont_block = options.dont_block || get_updates_failures > 0;
if (get_updates_params.queue_id === undefined) {
get_updates_params.queue_id = page_params.event_queue_id;
get_updates_params.last_event_id = page_params.last_event_id;
}
if (get_updates_xhr !== undefined) {
get_updates_xhr.abort();
}
if (get_updates_timeout !== undefined) {
clearTimeout(get_updates_timeout);
}
get_updates_timeout = undefined;
get_updates_xhr = channel.post({
url: '/json/get_events',
data: get_updates_params,
idempotent: true,
timeout: page_params.poll_timeout,
success: function (data) {
get_updates_xhr = undefined;
get_updates_failures = 0;
$('#connection-error').hide();
get_updates_success(data.events);
get_updates_timeout = setTimeout(get_updates, 0);
},
error: function (xhr, error_type, exn) {
get_updates_xhr = undefined;
// If we are old enough to have messages outside of the
// Tornado cache or if we're old enough that our message
// queue has been garbage collected, immediately reload.
if ((xhr.status === 400) &&
($.parseJSON(xhr.responseText).msg.indexOf("too old") !== -1 ||
$.parseJSON(xhr.responseText).msg.indexOf("Bad event queue id") !== -1)) {
page_params.event_queue_expired = true;
reload.initiate({immediate: true});
}
if (error_type === 'abort') {
// Don't restart if we explicitly aborted
return;
} else if (error_type === 'timeout') {
// Retry indefinitely on timeout.
get_updates_failures = 0;
$('#connection-error').hide();
} else {
get_updates_failures += 1;
}
if (get_updates_failures >= 5) {
$('#connection-error').show();
} else {
$('#connection-error').hide();
}
var retry_sec = Math.min(90, Math.exp(get_updates_failures/2));
get_updates_timeout = setTimeout(get_updates, retry_sec*1000);
}
});
}
function force_get_updates() {
get_updates_timeout = setTimeout(get_updates, 0);
}
function home_view_loaded() {
if (!waiting_on_homeview_load) {
return;
}
waiting_on_homeview_load = false;
$(document).trigger("home_view_loaded.zulip");
}
function cleanup_event_queue() {
// Submit a request to the server to cleanup our event queue
if (page_params.event_queue_expired === true) {
return;
}
channel.del({
url: '/json/events',
data: {queue_id: page_params.event_queue_id}
});
}
window.addEventListener("beforeunload", function (event) {
cleanup_event_queue();
});
function process_result(messages, opts) { function process_result(messages, opts) {
$('#get_old_messages_error').hide(); $('#get_old_messages_error').hide();
@@ -1219,8 +966,8 @@ function get_old_messages_success(data, opts) {
process_result(data.messages, opts); process_result(data.messages, opts);
ui.resize_bottom_whitespace(); ui.resize_bottom_whitespace();
if (waiting_on_homeview_load && opts.msg_list === home_msg_list) { if (opts.msg_list === home_msg_list) {
home_view_loaded(); server_events.home_view_loaded();
} }
} }
@@ -1274,10 +1021,6 @@ function load_old_messages(opts) {
}); });
} }
function restart_get_updates(options) {
get_updates(options);
}
function reset_load_more_status() { function reset_load_more_status() {
load_more_enabled = true; load_more_enabled = true;
have_scrolled_away_from_top = true; have_scrolled_away_from_top = true;
@@ -1311,29 +1054,6 @@ function load_more_messages(msg_list) {
}); });
} }
var watchdog_time = $.now();
setInterval(function () {
var new_time = $.now();
if ((new_time - watchdog_time) > 20000) { // 20 seconds.
// Defensively reset watchdog_time here in case there's an
// exception in one of the event handlers
watchdog_time = new_time;
// Our app's JS wasn't running, which probably means the machine was
// asleep.
$(document).trigger($.Event('unsuspend'));
}
watchdog_time = new_time;
}, 5000);
$(function () {
$(document).on('unsuspend', function () {
// Immediately poll for new updates on unsuspend
blueslip.log("Restarting get_updates due to unsuspend");
get_updates_failures = 0;
restart_get_updates({dont_block: true});
});
});
function fast_forward_pointer() { function fast_forward_pointer() {
channel.post({ channel.post({
url: '/json/get_profile', url: '/json/get_profile',
@@ -1463,7 +1183,7 @@ function main() {
} }
} }
// now start subscribing to updates // now start subscribing to updates
get_updates(); server_events.get_updates();
// backfill more messages after the user is idle // backfill more messages after the user is idle
var backfill_batch_size = 1000; var backfill_batch_size = 1000;
@@ -1488,8 +1208,8 @@ function main() {
cont: load_more cont: load_more
}); });
} else { } else {
home_view_loaded(); server_events.home_view_loaded();
get_updates(); server_events.get_updates();
} }
$(document).on('message_id_changed', function (event) { $(document).on('message_id_changed', function (event) {
@@ -1497,8 +1217,8 @@ function main() {
if (furthest_read === old_id) { if (furthest_read === old_id) {
furthest_read = new_id; furthest_read = new_id;
} }
if (get_updates_params.pointer === old_id) { if (server_events.get_updates_params.pointer === old_id) {
get_updates_params.pointer = new_id; server_events.get_updates_params.pointer = new_id;
} }
if (msg_metadata_cache[old_id]) { if (msg_metadata_cache[old_id]) {
msg_metadata_cache[new_id] = msg_metadata_cache[old_id]; msg_metadata_cache[new_id] = msg_metadata_cache[old_id];

View File

@@ -23,7 +23,7 @@ var globals =
// Modules, defined in their respective files. // Modules, defined in their respective files.
+ ' compose compose_fade rows hotkeys narrow reload notifications_bar search subs' + ' compose compose_fade rows hotkeys narrow reload notifications_bar search subs'
+ ' composebox_typeahead typeahead_helper notifications hashchange' + ' composebox_typeahead server_events typeahead_helper notifications hashchange'
+ ' invite ui util activity timerender MessageList MessageListView blueslip unread stream_list' + ' invite ui util activity timerender MessageList MessageListView blueslip unread stream_list'
+ ' message_edit tab_bar emoji popovers navigate settings' + ' message_edit tab_bar emoji popovers navigate settings'
+ ' avatar feature_flags search_suggestion referral stream_color Dict' + ' avatar feature_flags search_suggestion referral stream_color Dict'
@@ -59,19 +59,21 @@ var globals =
+ ' scroll_to_selected get_private_message_recipient' + ' scroll_to_selected get_private_message_recipient'
+ ' load_old_messages enable_unread_counts' + ' load_old_messages enable_unread_counts'
+ ' at_top_of_viewport at_bottom_of_viewport within_viewport' + ' at_top_of_viewport at_bottom_of_viewport within_viewport'
+ ' process_visible_unread_messages viewport restart_get_updates force_get_updates' + ' process_visible_unread_messages mark_messages_as_read viewport '
+ ' load_more_messages reset_load_more_status have_scrolled_away_from_top' + ' load_more_messages reset_load_more_status have_scrolled_away_from_top'
+ ' maybe_scroll_to_selected recenter_pointer_on_display suppress_scroll_pointer_update' + ' maybe_scroll_to_selected recenter_pointer_on_display suppress_scroll_pointer_update'
+ ' mark_current_list_as_read message_range message_in_table process_loaded_for_unread' + ' mark_current_list_as_read message_range message_in_table process_loaded_for_unread'
+ ' mark_all_as_read message_unread process_read_messages unread_in_current_view' + ' mark_all_as_read message_unread process_read_messages unread_in_current_view'
+ ' fast_forward_pointer recent_subjects unread_subjects' + ' fast_forward_pointer recent_subjects unread_subjects'
+ ' add_person_in_realm remove_person update_person'
+ ' furthest_read server_furthest_read update_messages'
+ ' add_message_metadata' + ' add_message_metadata'
+ ' mark_message_as_read batched_flag_updater' + ' mark_message_as_read batched_flag_updater'
+ ' send_summarize_in_home' + ' send_summarize_in_home'
+ ' send_summarize_in_stream' + ' send_summarize_in_stream'
+ ' suppress_unread_counts' + ' suppress_unread_counts'
+ ' msg_metadata_cache' + ' msg_metadata_cache'
+ ' get_updates_xhr get_updates_timeout report_as_received' + ' report_as_received'
+ ' insert_new_messages process_message_for_recent_subjects reify_person' + ' insert_new_messages process_message_for_recent_subjects reify_person'
; ;

View File

@@ -553,6 +553,7 @@ JS_SPECS = {
'js/message_list.js', 'js/message_list.js',
'js/alert_words.js', 'js/alert_words.js',
'js/alert_words_ui.js', 'js/alert_words_ui.js',
'js/server_events.js',
'js/zulip.js', 'js/zulip.js',
'js/activity.js', 'js/activity.js',
'js/colorspace.js', 'js/colorspace.js',