Commit 53d26c09 authored by Tomáš Peterka's avatar Tomáš Peterka

[renderjs_ui] ListBox asynchronous data fetching locks other ListBox operations

parent b2edbc2f
/*jslint indent: 2, maxerr: 3, nomen: true */
/*global window, document, rJS, URI, RSVP,
SimpleQuery, ComplexQuery, Query, Handlebars, console, QueryFactory*/
SimpleQuery, ComplexQuery, Query, Handlebars, console, QueryFactory,
lockGadgetInQueue, unlockGadgetInQueue, unlockGadgetInFailedQueue */
(function (window, document, rJS, URI, RSVP,
SimpleQuery, ComplexQuery, Query, Handlebars, console, QueryFactory) {
SimpleQuery, ComplexQuery, Query, Handlebars, console, QueryFactory,
lockGadgetInQueue, unlockGadgetInQueue, unlockGadgetInFailedQueue) {
"use strict";
var gadget_klass = rJS(window),
listbox_thead_source = gadget_klass.__template_element
......@@ -414,7 +416,6 @@
} else if ((modification_dict.hasOwnProperty('show_line_selector')) ||
(modification_dict.hasOwnProperty('allDocs_result'))) {
// Render the listbox content
result_queue
.push(function () {
......@@ -599,8 +600,12 @@
"query": gadget.state.query_string,
"limit": limit_options,
"select_list": select_list,
// "aggregation": aggregation_option_list
"sort_on": JSON.parse(gadget.state.sort_list_json)
})
// Lock gadget to show that state is not consistent thus any other
// operation must wait
.push(lockGadgetInQueue(gadget))
.push(function (result) {
return gadget.changeState({
allDocs_result: result
......@@ -608,7 +613,7 @@
}, function (error) {
// do not crash interface if allDocs fails
//this will catch all error, not only search criteria invalid error
// this will catch all error, not only search criteria invalid error
if (error instanceof RSVP.CancellationError) {
throw error;
}
......@@ -616,40 +621,50 @@
return gadget.changeState({
has_error: true
});
});
})
// gadget mutex is necessary because part of ListBox is rendered later
// via cancellable `fetchLineList` thus no other operation (eg. getContent) must be
// performed on ListBox before it is fully rendered
// unlock mutex in success and fail branch
.push(unlockGadgetInQueue(gadget), unlockGadgetInFailedQueue(gadget));
})
.declareMethod("getContent", function (options) {
var form_gadget = this,
k,
field_gadget,
count = form_gadget.props.cell_gadget_list.length,
data = {},
queue = new RSVP.Queue();
function extendData(field_data) {
var key;
for (key in field_data) {
if (field_data.hasOwnProperty(key)) {
data[key] = field_data[key];
}
var gadget = this;
return new RSVP.Queue()
// lock gadget to forbid sub-field modification while getting their values
.push(lockGadgetInQueue(gadget))
.push(function () {
// gather content from all sub-gadgets
return RSVP.all(
gadget.props.cell_gadget_list.map(
function (subgadget) {
if (subgadget.getContent !== undefined) {
return subgadget.getContent(options);
}
return {};
}
for (k = 0; k < count; k += 1) {
field_gadget = form_gadget.props.cell_gadget_list[k];
// XXX Hack until better defined
if (field_gadget.getContent !== undefined) {
queue
.push(field_gadget.getContent.bind(field_gadget, options))
.push(extendData);
)
);
})
.push(function (field_value_list) {
var data = {}; // will contain all subfields values
field_value_list.forEach(function (field_value) {
// imprint the content (object with possible multiple keys!) into `data`
var key;
for (key in field_value) {
if (field_value.hasOwnProperty(key)) {
data[key] = field_value[key];
}
}
return queue
.push(function () {
data[form_gadget.props.listbox_uid_dict.key] = form_gadget.props.listbox_uid_dict.value;
return data;
});
// gadget.props.listbox_uid_dict.value is an array of UIDs of all editable documents
data[gadget.props.listbox_uid_dict.key] = gadget.props.listbox_uid_dict.value;
return data;
})
// unlock passes through its argument
.push(unlockGadgetInQueue(gadget), unlockGadgetInFailedQueue(gadget));
})
.onEvent('click', function (evt) {
......@@ -738,4 +753,5 @@
});
}(window, document, rJS, URI, RSVP,
SimpleQuery, ComplexQuery, Query, Handlebars, console, QueryFactory));
SimpleQuery, ComplexQuery, Query, Handlebars, console, QueryFactory,
lockGadgetInQueue, unlockGadgetInQueue, unlockGadgetInFailedQueue));
......@@ -236,7 +236,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>963.40734.12928.52855</string> </value>
<value> <string>964.19650.6129.64921</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -254,7 +254,7 @@
</tuple>
<state>
<tuple>
<float>1511430200.52</float>
<float>1513931714.4</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -126,4 +126,97 @@
});
};
/** Internal function to prepare gadget to hold a mutex */
function ensureLockable(gadget) {
if (gadget.props === undefined) {
gadget.props = {};
}
// waiting_line is container of mutexes which already blocks some Promise
if (gadget.props.waiting_line === undefined) {
gadget.props.waiting_line = [];
}
}
/** Synchronously lock gadget and return previous lock's promise.
If used in a Queue (@see lockGadgetInQueue) it blocks when acquiring the lock.
*/
window.lockGadget = function (gadget) {
var ahead_of_me;
ensureLockable(gadget);
// step in line
gadget.props.waiting_line.push(RSVP.defer());
if (gadget.props.waiting_line.length >= 2) {
// wait for the promise ahead of me
ahead_of_me = gadget.props.waiting_line[gadget.props.waiting_line.length - 2].promise;
} else {
ahead_of_me = RSVP.resolve();
}
// return previous lock's Promise to postpone execution
return ahead_of_me;
};
/** Lock gadget as a step in RSVP.Queue waiting for previous lock to unlock.
Use in RSVP.Queue to block execution until manually called `unlockGadget`.
Both lock/unlockGadget pass through any value in RSVP.Queue manner.
Pass through any value. Not re-throwing errors in fail branch!
Example:
new RSVP.Queue()
.push(function () {return some_value;})
.push(lockGadgetInQueue(gadget))
.push(function (some_value) {return someWork(some_value);})
.push(unlockGadgetInQueue(gadget));
*/
window.lockGadgetInQueue = function (gadget) {
// return function to be used in RSVP.Queue
return function (pass_through) {
return new RSVP.Queue()
.push(function () {
return window.lockGadget(gadget);
})
.push(function () {
return pass_through;
});
};
};
/** Synchronously unlock gadget by resolving props.mutex.promise.
That promise is most likely blocking some RSVP.Queue or is then-ed on another
Promise.
*/
window.unlockGadget = function (gadget) {
if (gadget.props === undefined ||
gadget.props.waiting_line === undefined ||
gadget.props.waiting_line.length === 0) {
throw new Error("Gadget " + gadget + " has not been locked yet!");
}
gadget.props.waiting_line.shift().resolve();
};
/** Unlock gadget without blocking as a step in RSVP.Queue.
Pass through any value. Not re-throwing errors in fail branch!
For example @see lockGadgetInQueue.
*/
window.unlockGadgetInQueue = function (gadget) {
return function (pass_through) {
window.unlockGadget(gadget);
return pass_through;
};
};
/** Unlock gadget without blocking and throw any argument received. */
window.unlockGadgetInFailedQueue = function (gadget) {
return function (error) {
window.unlockGadget(gadget);
throw error;
};
};
}(window, RSVP, FileReader));
\ No newline at end of file
......@@ -230,7 +230,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>947.51167.64410.14796</string> </value>
<value> <string>964.18755.54967.20718</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -248,7 +248,7 @@
</tuple>
<state>
<tuple>
<float>1450099422.01</float>
<float>1513931649.44</float>
<string>UTC</string>
</tuple>
</state>
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ZopePageTemplate" module="Products.PageTemplates.ZopePageTemplate"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>text/html</string> </value>
</item>
<item>
<key> <string>expand</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>testFastSave</string> </value>
</item>
<item>
<key> <string>output_encoding</string> </key>
<value> <string>utf-8</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <unicode></unicode> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<html xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:metal="http://xml.zope.org/namespaces/metal">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Test ListBox Fast Save</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<thead>
<tr><td rowspan="1" colspan="3">Test fast saving without enough time to render listbox data</td></tr>
</thead><tbody>
<tal:block metal:use-macro="here/PTZuite_CommonTemplate/macros/init" />
<!-- Clean Up -->
<tr><td>open</td>
<td>${base_url}/foo_module/ListBoxZuite_reset</td><td></td></tr>
<tr><td>assertTextPresent</td>
<td>Reset Successfully.</td><td></td></tr>
<!-- Create Foo objects with IDs 0-9 -->
<tr><td>open</td>
<td>${base_url}/foo_module/FooModule_createObjects?start:int=1&amp;num:int=3&amp;create_line:int=1</td><td></td></tr>
<tr><td>assertTextPresent</td>
<td>Created Successfully.</td><td></td></tr>
<tal:block metal:use-macro="here/Zuite_CommonTemplate/macros/wait_for_activities" />
<!-- Shortcut for full renderjs url -->
<tr><td>store</td>
<td>${base_url}/web_site_module/renderjs_runner</td>
<td>renderjs_url</td></tr>
<tr><td>open</td>
<td>${renderjs_url}/#/foo_module/1?editable=1</td><td></td></tr>
<tr><td>waitForElementPresent</td>
<td>//input[@name="field_my_quantity"]</td><td></td></tr>
<tr><td>type</td>
<td>field_my_quantity</td>
<td>1.00</td></tr>
<tr><td>click</td>
<td>//div[@data-gadget-scope='header']//button[@data-i18n='Save']</td><td></td></tr>
<tr><td>waitForElementPresent</td>
<td>//button[text()="Input data has errors."]</td><td></td></tr>
<tr><td>waitForElementPresent</td>
<td>//div[@data-gadget-scope="field_my_quantity"]//span[text()='The number you input has too large precision.']</td><td></td></tr>
<tr><td>type</td>
<td>field_my_quantity</td>
<td>1000000000.0</td></tr>
<tal:block metal:use-macro="here/Zuite_CommonTemplateForRenderjsUi/macros/save" />
<tr><td>verifyValue</td>
<td>field_my_quantity</td>
<td>1000000000.0</td></tr>
</tbody></table>
</body>
</html>
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment