mirror of
https://github.com/Jermolene/TiddlyWiki5
synced 2026-10-17 21:32:28 +00:00
* Fix race condition in plugin library iframe message handling
Introduce a PLUGIN-LIBRARY-READY handshake verb for plugin library iframes.
This ensures that postMessage commands are not prematurely dropped before
the iframe's internal script has fully bound its message listeners.
Retains iframe.onload as a backward-compatible fallback for legacy or
external libraries that do not emit the ready signal.
* add changenote
* feat(plugin-library): add PLUGIN-LIBRARY-READY handshake to plugin library iframe
Add PLUGIN-LIBRARY-READY message emission in libraryserver.js
Signals to parent window when iframe is ready to receive messages
Complements browser-messaging.js PLUGIN-LIBRARY-READY handler
Includes comprehensive tests for callback queueing and handshake flow
Fixes race condition in plugin library iframe message handling
* fix(plugin-library): emit PLUGIN-LIBRARY-READY message on iframe initialization
Emit the PLUGIN-LIBRARY-READY message after the message listener is bound,
signaling to the parent window that the iframe is ready to receive postMessage
commands. This completes the handshake implementation to eliminate the race
condition where messages could be lost before the iframe's script initialization
completes.
* fix indentation
* remove extra space
* remove typo
* refactor(test-browser-messaging): update test descriptions and remove unused code
* lint fix
* fix: correct the update note
201 lines
6.1 KiB
JavaScript
201 lines
6.1 KiB
JavaScript
/*\
|
|
title: $:/core/modules/browser-messaging.js
|
|
type: application/javascript
|
|
module-type: startup
|
|
|
|
Browser message handling
|
|
\*/
|
|
|
|
"use strict";
|
|
|
|
// Export name and synchronous status
|
|
exports.name = "browser-messaging";
|
|
exports.platforms = ["browser"];
|
|
exports.after = ["startup"];
|
|
exports.synchronous = true;
|
|
|
|
/*
|
|
Load a specified url as an iframe and call the callback when it is loaded. If the url is already loaded then the existing iframe instance is used
|
|
*/
|
|
|
|
// Ensure the callback fires once, whether by PLUGIN-LIBRARY-READY or onload (legacy)
|
|
|
|
function flushCallbacks(iframeInfo, err) {
|
|
if(iframeInfo.status !== "loaded") {
|
|
iframeInfo.status = err ? "error" : "loaded";
|
|
saveIFrameInfoTiddler(iframeInfo);
|
|
var cb;
|
|
while((cb = iframeInfo.callbacks.shift())) {
|
|
cb(err, iframeInfo);
|
|
}
|
|
}
|
|
}
|
|
|
|
function loadIFrame(url,callback) {
|
|
// Check if iframe already exists
|
|
var iframeInfo = $tw.browserMessaging.iframeInfoMap[url];
|
|
if(iframeInfo) {
|
|
// We've already got the iframe
|
|
callback(null,iframeInfo);
|
|
} else {
|
|
// Create the iframe and save it in the list
|
|
var iframe = document.createElement("iframe");
|
|
iframeInfo = {
|
|
url: url,
|
|
status: "loading",
|
|
domNode: iframe,
|
|
callbacks: [callback]
|
|
};
|
|
$tw.browserMessaging.iframeInfoMap[url] = iframeInfo;
|
|
saveIFrameInfoTiddler(iframeInfo);
|
|
// Add the iframe to the DOM and hide it
|
|
iframe.style.display = "none";
|
|
iframe.setAttribute("library","true");
|
|
document.body.appendChild(iframe);
|
|
|
|
// Set up onload. Legacy fallback: if PLUGIN-LIBRARY-READY never arrives, onload triggers the flush later
|
|
iframe.onload = function() {
|
|
flushCallbacks(iframeInfo);
|
|
};
|
|
iframe.onerror = function() {
|
|
flushCallbacks(iframeInfo, "Cannot load iframe");
|
|
};
|
|
try {
|
|
iframe.src = url;
|
|
} catch(ex) {
|
|
flushCallbacks(iframeInfo, ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
Unload library iframe for given url
|
|
*/
|
|
function unloadIFrame(url){
|
|
var iframes = document.getElementsByTagName("iframe");
|
|
for(var t = iframes.length - 1; t >= 0; t--) {
|
|
var iframe = iframes[t];
|
|
if(iframe.getAttribute("library") === "true" &&
|
|
iframe.getAttribute("src") === url) {
|
|
iframe.parentNode.removeChild(iframe);
|
|
}
|
|
}
|
|
}
|
|
|
|
function saveIFrameInfoTiddler(iframeInfo) {
|
|
$tw.wiki.addTiddler(new $tw.Tiddler($tw.wiki.getCreationFields(),{
|
|
title: "$:/temp/ServerConnection/" + iframeInfo.url,
|
|
text: iframeInfo.status,
|
|
tags: ["$:/tags/ServerConnection"],
|
|
url: iframeInfo.url
|
|
},$tw.wiki.getModificationFields()));
|
|
}
|
|
|
|
exports.startup = function() {
|
|
// Initialise the store of iframes we've created
|
|
$tw.browserMessaging = {
|
|
iframeInfoMap: {} // Hashmap by URL of {url:,status:"loading/loaded",domNode:}
|
|
};
|
|
// Listen for widget messages to control loading the plugin library
|
|
$tw.rootWidget.addEventListener("tm-load-plugin-library",function(event) {
|
|
var paramObject = event.paramObject || {},
|
|
url = paramObject.url;
|
|
if(url) {
|
|
loadIFrame(url,function(err,iframeInfo) {
|
|
if(err) {
|
|
alert($tw.language.getString("Error/LoadingPluginLibrary") + ": " + url);
|
|
} else {
|
|
iframeInfo.domNode.contentWindow.postMessage({
|
|
verb: "GET",
|
|
url: "recipes/library/tiddlers.json",
|
|
cookies: {
|
|
type: "save-info",
|
|
infoTitlePrefix: paramObject.infoTitlePrefix || "$:/temp/RemoteAssetInfo/",
|
|
url: url
|
|
}
|
|
},"*");
|
|
}
|
|
});
|
|
}
|
|
});
|
|
// Listen for widget messages to control unloading the plugin library
|
|
$tw.rootWidget.addEventListener("tm-unload-plugin-library",function(event) {
|
|
var paramObject = event.paramObject || {},
|
|
url = paramObject.url;
|
|
$tw.browserMessaging.iframeInfoMap[url] = undefined;
|
|
if(url) {
|
|
unloadIFrame(url);
|
|
$tw.utils.each(
|
|
$tw.wiki.filterTiddlers("[[$:/temp/ServerConnection/" + url + "]] [prefix[$:/temp/RemoteAssetInfo/" + url + "/]]"),
|
|
function(title) {
|
|
$tw.wiki.deleteTiddler(title);
|
|
}
|
|
);
|
|
}
|
|
});
|
|
$tw.rootWidget.addEventListener("tm-load-plugin-from-library",function(event) {
|
|
var paramObject = event.paramObject || {},
|
|
url = paramObject.url,
|
|
title = paramObject.title;
|
|
if(url && title) {
|
|
loadIFrame(url,function(err,iframeInfo) {
|
|
if(err) {
|
|
alert($tw.language.getString("Error/LoadingPluginLibrary") + ": " + url);
|
|
} else {
|
|
iframeInfo.domNode.contentWindow.postMessage({
|
|
verb: "GET",
|
|
url: "recipes/library/tiddlers/" + encodeURIComponent(title) + ".json",
|
|
cookies: {
|
|
type: "save-tiddler",
|
|
url: url
|
|
}
|
|
},"*");
|
|
}
|
|
});
|
|
}
|
|
});
|
|
// Listen for window messages from other windows
|
|
window.addEventListener("message",function listener(event){
|
|
// console.log("browser-messaging: ",document.location.toString())
|
|
// console.log("browser-messaging: Received message from",event.origin);
|
|
// console.log("browser-messaging: Message content",event.data);
|
|
switch(event.data.verb) {
|
|
case "PLUGIN-LIBRARY-READY":
|
|
$tw.utils.each($tw.browserMessaging.iframeInfoMap, function(info) {
|
|
if(info && info.domNode && info.domNode.contentWindow === event.source) {
|
|
flushCallbacks(info);
|
|
}
|
|
});
|
|
break;
|
|
case "GET-RESPONSE":
|
|
if(event.data.status.charAt(0) === "2") {
|
|
if(event.data.cookies) {
|
|
if(event.data.cookies.type === "save-info") {
|
|
var tiddlers = $tw.utils.parseJSONSafe(event.data.body);
|
|
$tw.utils.each(tiddlers,function(tiddler) {
|
|
$tw.wiki.addTiddler(new $tw.Tiddler($tw.wiki.getCreationFields(),tiddler,{
|
|
title: event.data.cookies.infoTitlePrefix + event.data.cookies.url + "/" + tiddler.title,
|
|
"original-title": tiddler.title,
|
|
text: "",
|
|
type: "text/vnd.tiddlywiki",
|
|
"original-type": tiddler.type,
|
|
"plugin-type": undefined,
|
|
"original-plugin-type": tiddler["plugin-type"],
|
|
"module-type": undefined,
|
|
"original-module-type": tiddler["module-type"],
|
|
tags: ["$:/tags/RemoteAssetInfo"],
|
|
"original-tags": $tw.utils.stringifyList(tiddler.tags || []),
|
|
"server-url": event.data.cookies.url
|
|
},$tw.wiki.getModificationFields()));
|
|
});
|
|
} else if(event.data.cookies.type === "save-tiddler") {
|
|
var tiddler = $tw.utils.parseJSONSafe(event.data.body);
|
|
$tw.wiki.addTiddler(new $tw.Tiddler(tiddler));
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
},false);
|
|
};
|