Commit 84bcf0e9 authored by Romain Courteaud's avatar Romain Courteaud

Iframe: ensure all channels are OK before calling ready

parent 987147b7
...@@ -139,14 +139,13 @@ ...@@ -139,14 +139,13 @@
javascript_registration_dict = {}, javascript_registration_dict = {},
stylesheet_registration_dict = {}, stylesheet_registration_dict = {},
gadget_loading_klass_list = [], gadget_loading_klass_list = [],
loading_klass_promise,
renderJS, renderJS,
Monitor, Monitor,
scope_increment = 0, scope_increment = 0,
isAbsoluteOrDataURL = new RegExp('^(?:[a-z]+:)?//|data:', 'i'), isAbsoluteOrDataURL = new RegExp('^(?:[a-z]+:)?//|data:', 'i'),
is_page_unloaded = false, is_page_unloaded = false,
error_list = [], error_list = [],
bootstrap_deferred_object = new RSVP.defer(); all_dependency_loaded_deferred;
window.addEventListener('error', function (error) { window.addEventListener('error', function (error) {
error_list.push(error); error_list.push(error);
...@@ -900,7 +899,8 @@ ...@@ -900,7 +899,8 @@
// Create the communication channel with the iframe // Create the communication channel with the iframe
gadget_instance.__chan = Channel.build({ gadget_instance.__chan = Channel.build({
window: iframe.contentWindow, window: iframe.contentWindow,
origin: "*", // origin: (new URL(url, window.location)).origin,
origin: '*',
scope: "renderJS" scope: "renderJS"
}); });
...@@ -915,12 +915,8 @@ ...@@ -915,12 +915,8 @@
params: [ params: [
method_name, method_name,
Array.prototype.slice.call(argument_list, 0)], Array.prototype.slice.call(argument_list, 0)],
success: function (s) { success: resolve,
resolve(s); error: reject
},
error: function (e) {
reject(e);
}
}); });
}); });
return new RSVP.Queue() return new RSVP.Queue()
...@@ -1255,9 +1251,6 @@ ...@@ -1255,9 +1251,6 @@
gadget_model_defer_dict[url] = defer; gadget_model_defer_dict[url] = defer;
// Change the global variable to update the loading queue
loading_klass_promise = defer.promise;
// Fetch the HTML page and parse it // Fetch the HTML page and parse it
return new RSVP.Queue() return new RSVP.Queue()
.push(function () { .push(function () {
...@@ -1370,225 +1363,19 @@ ...@@ -1370,225 +1363,19 @@
// Bootstrap process. Register the self gadget. // Bootstrap process. Register the self gadget.
/////////////////////////////////////////////////// ///////////////////////////////////////////////////
// Detect when all JS dependencies have been loaded
all_dependency_loaded_deferred = new RSVP.defer();
// Manually initializes the self gadget if the DOMContentLoaded event // Manually initializes the self gadget if the DOMContentLoaded event
// is triggered before everything was ready. // is triggered before everything was ready.
// (For instance, the HTML-tag for the self gadget gets inserted after // (For instance, the HTML-tag for the self gadget gets inserted after
// page load) // page load)
renderJS.manualBootstrap = function () { renderJS.manualBootstrap = function () {
bootstrap_deferred_object.resolve(); all_dependency_loaded_deferred.resolve();
};
function bootstrap() {
var url = removeHash(window.location.href),
TmpConstructor,
root_gadget,
loading_gadget_promise = new RSVP.Queue(),
declare_method_count = 0,
embedded_channel,
notifyReady,
notifyDeclareMethod,
gadget_ready = false,
iframe_top_gadget,
last_acquisition_gadget,
declare_method_list_waiting = [],
gadget_failed = false,
gadget_error,
connection_ready = false;
// Create the gadget class for the current url
if (gadget_model_defer_dict.hasOwnProperty(url)) {
throw new Error("bootstrap should not be called twice");
}
loading_klass_promise = new RSVP.Promise(function (resolve, reject) {
last_acquisition_gadget = new RenderJSGadget();
last_acquisition_gadget.__acquired_method_dict = {
reportServiceError: function (param_list) {
letsCrash(param_list[0]);
}
};
// Stop acquisition on the last acquisition gadget
// Do not put this on the klass, as their could be multiple instances
last_acquisition_gadget.__aq_parent = function (method_name) {
throw new renderJS.AcquisitionError(
"No gadget provides " + method_name
);
};
//we need to determine tmp_constructor's value before exit bootstrap
//because of function : renderJS
//but since the channel checking is async,
//we can't use code structure like:
// if channel communication is ok
// tmp_constructor = RenderJSGadget
// else
// tmp_constructor = RenderJSEmbeddedGadget
if (window.self === window.top) {
// XXX Copy/Paste from declareGadgetKlass
TmpConstructor = function () {
RenderJSGadget.call(this);
};
TmpConstructor.declareMethod = RenderJSGadget.declareMethod;
TmpConstructor.declareJob = RenderJSGadget.declareJob;
TmpConstructor.declareAcquiredMethod =
RenderJSGadget.declareAcquiredMethod;
TmpConstructor.allowPublicAcquisition =
RenderJSGadget.allowPublicAcquisition;
TmpConstructor.__ready_list = RenderJSGadget.__ready_list.slice();
TmpConstructor.ready = RenderJSGadget.ready;
TmpConstructor.setState = RenderJSGadget.setState;
TmpConstructor.onStateChange = RenderJSGadget.onStateChange;
TmpConstructor.__service_list = RenderJSGadget.__service_list.slice();
TmpConstructor.declareService =
RenderJSGadget.declareService;
TmpConstructor.onEvent =
RenderJSGadget.onEvent;
TmpConstructor.onLoop =
RenderJSGadget.onLoop;
TmpConstructor.prototype = new RenderJSGadget();
TmpConstructor.prototype.constructor = TmpConstructor;
TmpConstructor.prototype.__path = url;
gadget_model_defer_dict[url] = {
promise: RSVP.resolve(TmpConstructor)
};
// Create the root gadget instance and put it in the loading stack
root_gadget = new TmpConstructor();
setAqParent(root_gadget, last_acquisition_gadget);
} else {
// Create the root gadget instance and put it in the loading stack
TmpConstructor = RenderJSEmbeddedGadget;
TmpConstructor.__ready_list = RenderJSGadget.__ready_list.slice();
TmpConstructor.__service_list = RenderJSGadget.__service_list.slice();
TmpConstructor.prototype.__path = url;
root_gadget = new RenderJSEmbeddedGadget();
setAqParent(root_gadget, last_acquisition_gadget);
// Create the communication channel
embedded_channel = Channel.build({
window: window.parent,
origin: "*",
scope: "renderJS",
onReady: function () {
var k;
iframe_top_gadget = false;
//Default: Define __aq_parent to inform parent window
root_gadget.__aq_parent =
TmpConstructor.prototype.__aq_parent = function (method_name,
argument_list, time_out) {
return new RSVP.Promise(function (resolve, reject) {
embedded_channel.call({
method: "acquire",
params: [
method_name,
argument_list
],
success: function (s) {
resolve(s);
},
error: function (e) {
reject(e);
},
timeout: time_out
});
});
};
// Channel is ready, so now declare Function
notifyDeclareMethod = function (name) {
declare_method_count += 1;
embedded_channel.call({
method: "declareMethod",
params: name,
success: function () {
declare_method_count -= 1;
notifyReady();
},
error: function () {
declare_method_count -= 1;
}
});
};
for (k = 0; k < declare_method_list_waiting.length; k += 1) {
notifyDeclareMethod(declare_method_list_waiting[k]);
}
declare_method_list_waiting = [];
// If Gadget Failed Notify Parent
if (gadget_failed) {
embedded_channel.notify({
method: "failed",
params: gadget_error
});
return;
}
connection_ready = true;
notifyReady();
//the channel is ok
//so bind calls to renderJS method on the instance
embedded_channel.bind("methodCall", function (trans, v) {
root_gadget[v[0]].apply(root_gadget, v[1])
.then(function (g) {
trans.complete(g);
}).fail(function (e) {
trans.error(e.toString());
});
trans.delayReturn(true);
});
}
});
// Notify parent about gadget instanciation
notifyReady = function () {
if ((declare_method_count === 0) && (gadget_ready === true)) {
embedded_channel.notify({method: "ready"});
}
};
// Inform parent gadget about declareMethod calls here.
notifyDeclareMethod = function (name) {
declare_method_list_waiting.push(name);
};
notifyDeclareMethod("getInterfaceList");
notifyDeclareMethod("getRequiredCSSList");
notifyDeclareMethod("getRequiredJSList");
notifyDeclareMethod("getPath");
notifyDeclareMethod("getTitle");
// Surcharge declareMethod to inform parent window
TmpConstructor.declareMethod = function (name, callback) {
var result = RenderJSGadget.declareMethod.apply(
this,
[name, callback]
);
notifyDeclareMethod(name);
return result;
}; };
document.addEventListener('DOMContentLoaded',
all_dependency_loaded_deferred.resolve, false);
TmpConstructor.declareService = function configureMutationObserver(TmpConstructor, url, root_gadget) {
RenderJSGadget.declareService;
TmpConstructor.declareJob =
RenderJSGadget.declareJob;
TmpConstructor.onEvent =
RenderJSGadget.onEvent;
TmpConstructor.onLoop =
RenderJSGadget.onLoop;
TmpConstructor.declareAcquiredMethod =
RenderJSGadget.declareAcquiredMethod;
TmpConstructor.allowPublicAcquisition =
RenderJSGadget.allowPublicAcquisition;
iframe_top_gadget = true;
}
TmpConstructor.prototype.__acquired_method_dict = {};
gadget_loading_klass_list.push(TmpConstructor);
function init() {
// XXX HTML properties can only be set when the DOM is fully loaded // XXX HTML properties can only be set when the DOM is fully loaded
var settings = renderJS.parseGadgetHTMLDocument(document, url), var settings = renderJS.parseGadgetHTMLDocument(document, url),
j, j,
...@@ -1608,7 +1395,7 @@ ...@@ -1608,7 +1395,7 @@
); );
} }
TmpConstructor.__template_element.appendChild(fragment); TmpConstructor.__template_element.appendChild(fragment);
RSVP.all([root_gadget.getRequiredJSList(), return RSVP.all([root_gadget.getRequiredJSList(),
root_gadget.getRequiredCSSList()]) root_gadget.getRequiredCSSList()])
.then(function (all_list) { .then(function (all_list) {
var i, var i,
...@@ -1690,30 +1477,156 @@ ...@@ -1690,30 +1477,156 @@
observer.observe(target, config); observer.observe(target, config);
return root_gadget; return root_gadget;
}).then(resolve, function (e) {
reject(e);
console.error(e);
throw e;
}); });
} }
document.addEventListener('DOMContentLoaded',
bootstrap_deferred_object.resolve, false); function createLastAcquisitionGadget() {
// Return Promies/Queue here instead of directly calling init() var last_acquisition_gadget = new RenderJSGadget();
return new RSVP.Queue() last_acquisition_gadget.__acquired_method_dict = {
reportServiceError: function (param_list) {
letsCrash(param_list[0]);
}
};
// Stop acquisition on the last acquisition gadget
// Do not put this on the klass, as their could be multiple instances
last_acquisition_gadget.__aq_parent = function (method_name) {
throw new renderJS.AcquisitionError(
"No gadget provides " + method_name
);
};
return last_acquisition_gadget;
}
/*
function notifyAllMethodToParent() {
;
}
*/
function createLoadingGadget(url) {
var TmpConstructor,
root_gadget,
embedded_channel,
notifyDeclareMethod,
declare_method_list_waiting,
loading_result,
channel_defer,
real_result_list;
// gadget_failed = false,
// connection_ready = false;
// Create the gadget class for the current url
if (gadget_model_defer_dict.hasOwnProperty(url)) {
throw new Error("bootstrap should not be called twice");
}
// Create the root gadget instance and put it in the loading stack
TmpConstructor = RenderJSEmbeddedGadget;
TmpConstructor.__ready_list = RenderJSGadget.__ready_list.slice();
TmpConstructor.__service_list = RenderJSGadget.__service_list.slice();
TmpConstructor.prototype.__path = url;
root_gadget = new RenderJSEmbeddedGadget();
setAqParent(root_gadget, createLastAcquisitionGadget());
declare_method_list_waiting = [
"getInterfaceList",
"getRequiredCSSList",
"getRequiredJSList",
"getPath",
"getTitle"
];
// Inform parent gadget about declareMethod calls here.
notifyDeclareMethod = function (name) {
declare_method_list_waiting.push(name);
};
real_result_list = [TmpConstructor, root_gadget, embedded_channel,
declare_method_list_waiting];
if (window.self === window.top) {
loading_result = real_result_list;
} else {
channel_defer = RSVP.defer();
loading_result = RSVP.any([
channel_defer.promise,
new RSVP.Queue()
.push(function () { .push(function () {
return bootstrap_deferred_object.promise; // Expect the channel to parent to be usable after 1 second
// If not, consider the gadget as the root
// Drop all iframe channel communication
return RSVP.delay(1000);
}) })
.push(function () { .push(function () {
return init(); real_result_list[2] = undefined;
return real_result_list;
})
]);
// Create the communication channel
embedded_channel = Channel.build({
window: window.parent,
origin: "*",
scope: "renderJS",
onReady: function () {
var k,
len;
// Channel is ready, so now declare all methods
notifyDeclareMethod = function (name) {
declare_method_list_waiting.push(
new RSVP.Promise(function (resolve, reject) {
embedded_channel.call({
method: "declareMethod",
params: name,
success: resolve,
error: reject
}); });
})
);
};
len = declare_method_list_waiting.length;
for (k = 0; k < len; k += 1) {
notifyDeclareMethod(declare_method_list_waiting[k]);
}
channel_defer.resolve(real_result_list);
}
}); });
real_result_list[2] = embedded_channel;
}
loading_gadget_promise // Surcharge declareMethod to inform parent window
.push(function () { TmpConstructor.declareMethod = function (name, callback) {
return loading_klass_promise; var result = RenderJSGadget.declareMethod.apply(
}) this,
.push(function (root_gadget) { [name, callback]
var i; );
notifyDeclareMethod(name);
return result;
};
TmpConstructor.declareService =
RenderJSGadget.declareService;
TmpConstructor.declareJob =
RenderJSGadget.declareJob;
TmpConstructor.onEvent =
RenderJSGadget.onEvent;
TmpConstructor.onLoop =
RenderJSGadget.onLoop;
TmpConstructor.declareAcquiredMethod =
RenderJSGadget.declareAcquiredMethod;
TmpConstructor.allowPublicAcquisition =
RenderJSGadget.allowPublicAcquisition;
TmpConstructor.prototype.__acquired_method_dict = {};
gadget_loading_klass_list.push(TmpConstructor);
return loading_result;
}
function triggerReadyList(TmpConstructor, root_gadget) {
// XXX Probably duplicated
var i,
ready_queue = new RSVP.Queue();
function ready_wrapper() { function ready_wrapper() {
return root_gadget; return root_gadget;
...@@ -1727,45 +1640,108 @@ ...@@ -1727,45 +1640,108 @@
return startService(this); return startService(this);
}); });
loading_gadget_promise.push(ready_wrapper); ready_queue.push(ready_wrapper);
for (i = 0; i < TmpConstructor.__ready_list.length; i += 1) { for (i = 0; i < TmpConstructor.__ready_list.length; i += 1) {
// Put a timeout? // Put a timeout?
loading_gadget_promise ready_queue
.push(ready_executable_wrapper(TmpConstructor.__ready_list[i])) .push(ready_executable_wrapper(TmpConstructor.__ready_list[i]))
// Always return the gadget instance after ready function // Always return the gadget instance after ready function
.push(ready_wrapper); .push(ready_wrapper);
} }
return ready_queue;
}
function finishAqParentConfiguration(TmpConstructor, root_gadget,
embedded_channel) {
// Define __aq_parent to inform parent window
root_gadget.__aq_parent =
TmpConstructor.prototype.__aq_parent = function (method_name,
argument_list,
time_out) {
return new RSVP.Promise(function (resolve, reject) {
embedded_channel.call({
method: "acquire",
params: [
method_name,
argument_list
],
success: function (s) {
resolve(s);
},
error: function (e) {
reject(e);
},
timeout: time_out
}); });
if (window.self === window.top) {
loading_gadget_promise
.fail(function (e) {
letsCrash(e);
throw e;
}); });
} else { };
// Inform parent window that gadget is correctly loaded
loading_gadget_promise // bind calls to renderJS method on the instance
.then(function () { embedded_channel.bind("methodCall", function (trans, v) {
gadget_ready = true; root_gadget[v[0]].apply(root_gadget, v[1])
if (connection_ready) { .push(function (g) {
notifyReady(); trans.complete(g);
}, function (e) {
trans.error(e.toString());
});
trans.delayReturn(true);
});
}
function bootstrap(url) {
// Create the loading gadget
var wait_for_gadget_loaded = createLoadingGadget(url),
TmpConstructor,
root_gadget,
embedded_channel,
declare_method_list_waiting;
return new RSVP.Queue()
.push(function () {
// Wait for the loading gadget to be created
return wait_for_gadget_loaded;
})
.push(function (result_list) {
TmpConstructor = result_list[0];
root_gadget = result_list[1];
embedded_channel = result_list[2];
declare_method_list_waiting = result_list[3];
// Wait for all the gadget dependencies to be loaded
return all_dependency_loaded_deferred.promise;
})
.push(function () {
// Wait for all methods to be correctly declared
return RSVP.all(declare_method_list_waiting);
})
.push(function (result_list) {
if (embedded_channel !== undefined) {
finishAqParentConfiguration(TmpConstructor, root_gadget,
embedded_channel);
} }
// Check all DOM modifications to correctly start/stop services
return configureMutationObserver(TmpConstructor, url, root_gadget);
}) })
.fail(function (e) { .push(function () {
//top gadget in iframe // Trigger all ready functions
if (iframe_top_gadget) { return triggerReadyList(TmpConstructor, root_gadget);
gadget_failed = true; })
gadget_error = e.toString(); .push(function () {
if (embedded_channel !== undefined) {
embedded_channel.notify({method: "ready"});
}
})
.push(undefined, function (e) {
letsCrash(e); letsCrash(e);
} else { if (embedded_channel !== undefined) {
embedded_channel.notify({method: "failed", params: e.toString()}); embedded_channel.notify({method: "failed", params: e.toString()});
} }
throw e; throw e;
}); });
} }
} bootstrap(
bootstrap(); removeHash(window.location.href)
);
}(document, window, RSVP, DOMParser, Channel, MutationObserver, Node, }(document, window, RSVP, DOMParser, Channel, MutationObserver, Node,
FileReader, Blob, navigator, Event, URL)); FileReader, Blob, navigator, Event, URL));
...@@ -15,8 +15,7 @@ ...@@ -15,8 +15,7 @@
(modification_dict.foo === 'bar'); (modification_dict.foo === 'bar');
state_change_count += 1; state_change_count += 1;
}; };
gk.ready(function () {
gk.ready(function (g) {
ready_called = true; ready_called = true;
}) })
.setState(init_state) .setState(init_state)
......
...@@ -4868,7 +4868,8 @@ ...@@ -4868,7 +4868,8 @@
// Check that declare gadget returns the gadget // Check that declare gadget returns the gadget
var gadget = new RenderJSGadget(), var gadget = new RenderJSGadget(),
acquire_called = false, acquire_called = false,
url = "./embedded.html"; url = "./embedded.html",
new_gadget;
gadget.__aq_parent = function (method_name, argument_list) { gadget.__aq_parent = function (method_name, argument_list) {
acquire_called = true; acquire_called = true;
...@@ -4889,9 +4890,11 @@ ...@@ -4889,9 +4890,11 @@
stop(); stop();
gadget.declareGadget(url, { gadget.declareGadget(url, {
sandbox: 'iframe', sandbox: 'iframe',
element: document.getElementById('qunit-fixture') element: document.getElementById('qunit-fixture'),
scope: 'foobar'
}) })
.then(function (new_gadget) { .then(function (sub_gadget) {
new_gadget = sub_gadget;
return new RSVP.Queue() return new RSVP.Queue()
// Method returns an RSVP.Queue // Method returns an RSVP.Queue
...@@ -4901,12 +4904,14 @@ ...@@ -4901,12 +4904,14 @@
result instanceof RSVP.Queue, result instanceof RSVP.Queue,
"iframe method should return Queue" "iframe method should return Queue"
); );
return result;
}) })
/*
// Check that ready function are called // Check that ready function are called
.push(function () { .push(function () {
return new_gadget.wasReadyCalled(); return new_gadget.wasReadyCalled();
}) })
*/
.push(function (result) { .push(function (result) {
equal(result, true); equal(result, true);
}) })
...@@ -5776,7 +5781,7 @@ ...@@ -5776,7 +5781,7 @@
fixture.innerHTML = fixture.innerHTML =
"<iframe id=renderjsIframe src='./not_declared_gadget.html'></iframe>"; "<iframe id=renderjsIframe src='./not_declared_gadget.html'></iframe>";
stop(); stop();
return RSVP.delay(900) return RSVP.delay(1500)
.then(function () { .then(function () {
var iframe = document.getElementById('renderjsIframe'), var iframe = document.getElementById('renderjsIframe'),
acquisition_div = iframe.contentWindow. acquisition_div = iframe.contentWindow.
...@@ -5820,8 +5825,6 @@ ...@@ -5820,8 +5825,6 @@
return; return;
} }
iframe_text = iframe_body.textContent; iframe_text = iframe_body.textContent;
/*global console*/
// console.log(iframe_text);
if (iframe_text.indexOf('Page changed') !== -1) { if (iframe_text.indexOf('Page changed') !== -1) {
// Final page // Final page
ok(true, iframe_text); ok(true, iframe_text);
...@@ -5872,6 +5875,9 @@ ...@@ -5872,6 +5875,9 @@
iframe.addEventListener("load", function (evt) { iframe.addEventListener("load", function (evt) {
resolve(evt.target.result); resolve(evt.target.result);
}); });
})
.then(function () {
return RSVP.delay(1100);
}) })
.then(function () { .then(function () {
var iframe_body = iframe.contentWindow.document.body, var iframe_body = iframe.contentWindow.document.body,
...@@ -5946,7 +5952,7 @@ ...@@ -5946,7 +5952,7 @@
// if no event is fired within 500ms, just resolve and fail later // if no event is fired within 500ms, just resolve and fail later
window.setTimeout(function () { window.setTimeout(function () {
reject("Timeout, RenderJS is not Ready"); reject("Timeout, RenderJS is not Ready");
}, 500); }, 3000);
iframe.contentWindow.rJS.manualBootstrap(); iframe.contentWindow.rJS.manualBootstrap();
}); });
}) })
......
...@@ -6,9 +6,6 @@ ...@@ -6,9 +6,6 @@
rJS(window) rJS(window)
.ready(function (gadget) { .ready(function (gadget) {
return gadget.getElement() return gadget.element.dispatchEvent(new Event("rjsready"));
.push(function (element) {
element.dispatchEvent(new Event("rjsready"));
});
}); });
}(window, rJS)); }(window, rJS));
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