mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-09 03:46:16 -08:00
fix(iframe): [PKG-IFRAME-03] own pending requests and cancellation
This commit is contained in:
1 parent
b3f22652c2
commit
b8d03e7366
21 files changed
+3826
-107
No files matched your search
@@ -4,4 +4,4 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(t.ArtplayerToolIframe=t(),define(function(){return t.ArtplayerToolIframe})):(e="undefined"!==typeof globalThis?globalThis:e||self).ArtplayerToolIframe=t()}(this,function(){"use strict";class e{static get iframe(){return window.top!==window}static postMessage({type:t,data:s,id:i=0}){if(!e.iframe)throw new Error('The "ArtplayerToolIframe.postMessage" method can only be used in iframe');window.parent.postMessage({type:t,data:s,id:i},"*")}static async onMessage(t){if(!e.iframe)throw new Error('The "ArtplayerToolIframe.onMessage" method can only be used in iframe');const{type:s,data:i,id:o}=t.data;if("commit"===s)try{if(i.match(/\bresolve\((.*?)\)/)){const t=`return new Promise(function(resolve){\n${i}\n})`,s=await new Function(t)();e.postMessage({type:"response",data:s,id:o})}else{const t=new Function(i)();e.postMessage({type:"response",data:t,id:o})}}catch(n){throw e.postMessage({type:"error",data:n.message,id:o}),n}}static inject(){if(!e.iframe)throw new Error('The "ArtplayerToolIframe.inject" method can only be used in iframe');e.postMessage({type:"inject"}),window.addEventListener("message",e.onMessage)}constructor({iframe:e,url:t}){if(e instanceof HTMLIFrameElement===!1)throw new TypeError('"option.iframe" needs to be a HTMLIFrameElement');if("string"!==typeof t)throw new TypeError('"option.url" needs to be a string');this.url=t,this.$iframe=e,this.promises={},this.injected=!1,this.destroyed=!1,this.messageCallback=()=>null,this.onMessage=this.onMessage.bind(this),window.addEventListener("message",this.onMessage),this.$iframe.src=this.url}onMessage(e){const{type:t,data:s,id:i}=e.data;if("inject"===t)this.injected=!0;this.promises[i]&&("error"===t?this.promises[i].reject(new Error(s)):this.promises[i].resove(s),delete this.promises[i]),this.messageCallback&&this.messageCallback({type:t,data:s})}postMessage({type:e,data:t}){return new Promise((s,i)=>{(function o(){if(this.destroyed)i(new Error("The instance has been destroyed"));else if(this.injected){const o=Date.now();this.promises[o]={resove:s,reject:i},this.$iframe.contentWindow.postMessage({type:e,data:t,id:o},"*")}else setTimeout(o.bind(this),200)}).call(this)})}commit(e){if("function"!==typeof e)throw new TypeError('"commit.callback" needs to be a function');const t=e.toString(),s=t.substring(t.indexOf("{")+1,t.lastIndexOf("}"));return this.postMessage({type:"commit",data:s})}message(e){if("function"!==typeof e)throw new TypeError('"message.callback" needs to be a function');this.messageCallback=e}destroy(){this.destroyed=!0,window.removeEventListener("message",this.onMessage)}}return e});
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.ArtplayerToolIframe=t(),define(function(){return e.ArtplayerToolIframe})):(e="undefined"!==typeof globalThis?globalThis:e||self).ArtplayerToolIframe=t()}(this,function(){"use strict";const e=new WeakMap;let t=0;class s{static get iframe(){return window.top!==window}static postMessage({type:e,data:t,id:o=0}){if(!s.iframe)throw new Error('The "ArtplayerToolIframe.postMessage" method can only be used in iframe');window.parent.postMessage({type:e,data:t,id:o},"*")}static async onMessage(e){if(!s.iframe)throw new Error('The "ArtplayerToolIframe.onMessage" method can only be used in iframe');const{type:t,data:o,id:i}=e.data;if("commit"===t)try{if(o.match(/\bresolve\((.*?)\)/)){const e=`return new Promise(function(resolve){\n${o}\n})`,t=await new Function(e)();s.postMessage({type:"response",data:t,id:i})}else{const e=new Function(o)();s.postMessage({type:"response",data:e,id:i})}}catch(r){throw s.postMessage({type:"error",data:r.message,id:i}),r}}static inject(){if(!s.iframe)throw new Error('The "ArtplayerToolIframe.inject" method can only be used in iframe');s.postMessage({type:"inject"}),window.addEventListener("message",s.onMessage)}constructor({iframe:e,url:t}){if(e instanceof HTMLIFrameElement===!1)throw new TypeError('"option.iframe" needs to be a HTMLIFrameElement');if("string"!==typeof t)throw new TypeError('"option.url" needs to be a string');this.url=t,this.$iframe=e,this.promises={},this.injected=!1,this.destroyed=!1,this.messageCallback=()=>null,this.onMessage=this.onMessage.bind(this),window.addEventListener("message",this.onMessage),this.$iframe.src=this.url}onMessage(e){if(this.destroyed)return;const{type:t,data:s,id:o}=e.data;if("inject"===t)this.injected=!0;void 0!==o&&Object.prototype.hasOwnProperty.call(this.promises,o)&&this.promises[o]&&("error"===t?this.promises[o].reject(new Error(s)):this.promises[o].resove(s),delete this.promises[o]),this.messageCallback&&this.messageCallback({type:t,data:s})}postMessage(s){return function(s,{type:o,data:i}){return new Promise((r,n)=>{if(s.destroyed)return void n(new Error("The instance has been destroyed"));const a=e.get(s)||new Set;e.set(s,a);const d=t=>!!a.delete(t)&&(void 0!==t.timer&&clearTimeout(t.timer),void 0!==t.id&&delete s.promises[t.id],a.size||e.delete(s),!0),c={callbacks:{resove(e){d(c)&&r(e)},reject(e){d(c)&&n(e)}}};a.add(c);const l=()=>{if(c.timer=void 0,a.has(c))if(s.destroyed)c.callbacks.reject(new Error("The instance has been destroyed"));else try{if(s.injected){const e=Math.max(Date.now(),t+1);t=e,c.id=e,s.promises[e]=c.callbacks,s.$iframe.contentWindow.postMessage({type:o,data:i,id:e},"*")}else c.timer=setTimeout(l,200)}catch(e){c.callbacks.reject(e)}};l()})}(this,s)}commit(e){if("function"!==typeof e)throw new TypeError('"commit.callback" needs to be a function');const t=e.toString(),s=t.substring(t.indexOf("{")+1,t.lastIndexOf("}"));return this.postMessage({type:"commit",data:s})}message(e){if("function"!==typeof e)throw new TypeError('"message.callback" needs to be a function');this.messageCallback=e}destroy(){this.destroyed=!0,window.removeEventListener("message",this.onMessage),function(t){const s=e.get(t);if(s)for(const e of[...s])e.callbacks.reject(new Error("The instance has been destroyed"))}(this)}}return s});
|
||||
@@ -4,4 +4,4 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(t.ArtplayerToolIframe=t(),define(function(){return t.ArtplayerToolIframe})):(e="undefined"!==typeof globalThis?globalThis:e||self).ArtplayerToolIframe=t()}(this,function(){"use strict";class e{static get iframe(){return window.top!==window}static postMessage({type:t,data:s,id:o=0}){if(!e.iframe)throw new Error('The "ArtplayerToolIframe.postMessage" method can only be used in iframe');window.parent.postMessage({type:t,data:s,id:o},"*")}static onMessage(t){return s=this,o=null,i=function*(){if(!e.iframe)throw new Error('The "ArtplayerToolIframe.onMessage" method can only be used in iframe');const{type:s,data:o,id:i}=t.data;if("commit"===s)try{if(o.match(/\bresolve\((.*?)\)/)){const t=`return new Promise(function(resolve){\n${o}\n})`,s=yield new Function(t)();e.postMessage({type:"response",data:s,id:i})}else{const t=new Function(o)();e.postMessage({type:"response",data:t,id:i})}}catch(n){throw e.postMessage({type:"error",data:n.message,id:i}),n}},new Promise((e,t)=>{var n=e=>{try{a(i.next(e))}catch(s){t(s)}},r=e=>{try{a(i.throw(e))}catch(s){t(s)}},a=t=>t.done?e(t.value):Promise.resolve(t.value).then(n,r);a((i=i.apply(s,o)).next())});var s,o,i}static inject(){if(!e.iframe)throw new Error('The "ArtplayerToolIframe.inject" method can only be used in iframe');e.postMessage({type:"inject"}),window.addEventListener("message",e.onMessage)}constructor({iframe:e,url:t}){if(e instanceof HTMLIFrameElement===!1)throw new TypeError('"option.iframe" needs to be a HTMLIFrameElement');if("string"!==typeof t)throw new TypeError('"option.url" needs to be a string');this.url=t,this.$iframe=e,this.promises={},this.injected=!1,this.destroyed=!1,this.messageCallback=()=>null,this.onMessage=this.onMessage.bind(this),window.addEventListener("message",this.onMessage),this.$iframe.src=this.url}onMessage(e){const{type:t,data:s,id:o}=e.data;if("inject"===t)this.injected=!0;this.promises[o]&&("error"===t?this.promises[o].reject(new Error(s)):this.promises[o].resove(s),delete this.promises[o]),this.messageCallback&&this.messageCallback({type:t,data:s})}postMessage({type:e,data:t}){return new Promise((s,o)=>{(function i(){if(this.destroyed)o(new Error("The instance has been destroyed"));else if(this.injected){const i=Date.now();this.promises[i]={resove:s,reject:o},this.$iframe.contentWindow.postMessage({type:e,data:t,id:i},"*")}else setTimeout(i.bind(this),200)}).call(this)})}commit(e){if("function"!==typeof e)throw new TypeError('"commit.callback" needs to be a function');const t=e.toString(),s=t.substring(t.indexOf("{")+1,t.lastIndexOf("}"));return this.postMessage({type:"commit",data:s})}message(e){if("function"!==typeof e)throw new TypeError('"message.callback" needs to be a function');this.messageCallback=e}destroy(){this.destroyed=!0,window.removeEventListener("message",this.onMessage)}}return e});
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.ArtplayerToolIframe=t(),define(function(){return e.ArtplayerToolIframe})):(e="undefined"!==typeof globalThis?globalThis:e||self).ArtplayerToolIframe=t()}(this,function(){"use strict";const e=new WeakMap;let t=0;class s{static get iframe(){return window.top!==window}static postMessage({type:e,data:t,id:o=0}){if(!s.iframe)throw new Error('The "ArtplayerToolIframe.postMessage" method can only be used in iframe');window.parent.postMessage({type:e,data:t,id:o},"*")}static onMessage(e){return t=this,o=null,r=function*(){if(!s.iframe)throw new Error('The "ArtplayerToolIframe.onMessage" method can only be used in iframe');const{type:t,data:o,id:r}=e.data;if("commit"===t)try{if(o.match(/\bresolve\((.*?)\)/)){const e=`return new Promise(function(resolve){\n${o}\n})`,t=yield new Function(e)();s.postMessage({type:"response",data:t,id:r})}else{const e=new Function(o)();s.postMessage({type:"response",data:e,id:r})}}catch(i){throw s.postMessage({type:"error",data:i.message,id:r}),i}},new Promise((e,s)=>{var i=e=>{try{a(r.next(e))}catch(t){s(t)}},n=e=>{try{a(r.throw(e))}catch(t){s(t)}},a=t=>t.done?e(t.value):Promise.resolve(t.value).then(i,n);a((r=r.apply(t,o)).next())});var t,o,r}static inject(){if(!s.iframe)throw new Error('The "ArtplayerToolIframe.inject" method can only be used in iframe');s.postMessage({type:"inject"}),window.addEventListener("message",s.onMessage)}constructor({iframe:e,url:t}){if(e instanceof HTMLIFrameElement===!1)throw new TypeError('"option.iframe" needs to be a HTMLIFrameElement');if("string"!==typeof t)throw new TypeError('"option.url" needs to be a string');this.url=t,this.$iframe=e,this.promises={},this.injected=!1,this.destroyed=!1,this.messageCallback=()=>null,this.onMessage=this.onMessage.bind(this),window.addEventListener("message",this.onMessage),this.$iframe.src=this.url}onMessage(e){if(this.destroyed)return;const{type:t,data:s,id:o}=e.data;if("inject"===t)this.injected=!0;void 0!==o&&Object.prototype.hasOwnProperty.call(this.promises,o)&&this.promises[o]&&("error"===t?this.promises[o].reject(new Error(s)):this.promises[o].resove(s),delete this.promises[o]),this.messageCallback&&this.messageCallback({type:t,data:s})}postMessage(s){return function(s,{type:o,data:r}){return new Promise((i,n)=>{if(s.destroyed)return void n(new Error("The instance has been destroyed"));const a=e.get(s)||new Set;e.set(s,a);const d=t=>!!a.delete(t)&&(void 0!==t.timer&&clearTimeout(t.timer),void 0!==t.id&&delete s.promises[t.id],a.size||e.delete(s),!0),c={callbacks:{resove(e){d(c)&&i(e)},reject(e){d(c)&&n(e)}}};a.add(c);const l=()=>{if(c.timer=void 0,a.has(c))if(s.destroyed)c.callbacks.reject(new Error("The instance has been destroyed"));else try{if(s.injected){const e=Math.max(Date.now(),t+1);t=e,c.id=e,s.promises[e]=c.callbacks,s.$iframe.contentWindow.postMessage({type:o,data:r,id:e},"*")}else c.timer=setTimeout(l,200)}catch(e){c.callbacks.reject(e)}};l()})}(this,s)}commit(e){if("function"!==typeof e)throw new TypeError('"commit.callback" needs to be a function');const t=e.toString(),s=t.substring(t.indexOf("{")+1,t.lastIndexOf("}"));return this.postMessage({type:"commit",data:s})}message(e){if("function"!==typeof e)throw new TypeError('"message.callback" needs to be a function');this.messageCallback=e}destroy(){this.destroyed=!0,window.removeEventListener("message",this.onMessage),function(t){const s=e.get(t);if(s)for(const e of[...s])e.callbacks.reject(new Error("The instance has been destroyed"))}(this)}}return s});
|
||||
@@ -4,6 +4,72 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
const requests = /* @__PURE__ */ new WeakMap();
|
||||
let lastId = 0;
|
||||
function postRequest(host, { type, data }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (host.destroyed) {
|
||||
reject(new Error("The instance has been destroyed"));
|
||||
return;
|
||||
}
|
||||
const pending = requests.get(host) || /* @__PURE__ */ new Set();
|
||||
requests.set(host, pending);
|
||||
const finish = (request2) => {
|
||||
if (!pending.delete(request2))
|
||||
return false;
|
||||
if (request2.timer !== void 0)
|
||||
clearTimeout(request2.timer);
|
||||
if (request2.id !== void 0)
|
||||
delete host.promises[request2.id];
|
||||
if (!pending.size)
|
||||
requests.delete(host);
|
||||
return true;
|
||||
};
|
||||
const request = {
|
||||
callbacks: {
|
||||
resove(value) {
|
||||
if (finish(request))
|
||||
resolve(value);
|
||||
},
|
||||
reject(error) {
|
||||
if (finish(request))
|
||||
reject(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
pending.add(request);
|
||||
const loop = () => {
|
||||
request.timer = void 0;
|
||||
if (!pending.has(request))
|
||||
return;
|
||||
if (host.destroyed) {
|
||||
request.callbacks.reject(new Error("The instance has been destroyed"));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (host.injected) {
|
||||
const id = Math.max(Date.now(), lastId + 1);
|
||||
lastId = id;
|
||||
request.id = id;
|
||||
host.promises[id] = request.callbacks;
|
||||
host.$iframe.contentWindow.postMessage({ type, data, id }, "*");
|
||||
} else {
|
||||
request.timer = setTimeout(loop, 200);
|
||||
}
|
||||
} catch (error) {
|
||||
request.callbacks.reject(error);
|
||||
}
|
||||
};
|
||||
loop();
|
||||
});
|
||||
}
|
||||
function cancelRequests(host) {
|
||||
const pending = requests.get(host);
|
||||
if (!pending)
|
||||
return;
|
||||
for (const request of [...pending])
|
||||
request.callbacks.reject(new Error("The instance has been destroyed"));
|
||||
}
|
||||
class ArtplayerToolIframe {
|
||||
static get iframe() {
|
||||
return window.top !== window;
|
||||
@@ -71,13 +137,15 @@ ${data}
|
||||
this.$iframe.src = this.url;
|
||||
}
|
||||
onMessage(event) {
|
||||
if (this.destroyed)
|
||||
return;
|
||||
const { type, data, id } = event.data;
|
||||
switch (type) {
|
||||
case "inject":
|
||||
this.injected = true;
|
||||
break;
|
||||
}
|
||||
if (this.promises[id]) {
|
||||
if (id !== void 0 && Object.prototype.hasOwnProperty.call(this.promises, id) && this.promises[id]) {
|
||||
if (type === "error") {
|
||||
this.promises[id].reject(new Error(data));
|
||||
} else {
|
||||
@@ -89,29 +157,8 @@ ${data}
|
||||
this.messageCallback({ type, data });
|
||||
}
|
||||
}
|
||||
postMessage({ type, data }) {
|
||||
return new Promise((resove, reject) => {
|
||||
(function loop() {
|
||||
if (this.destroyed) {
|
||||
reject(new Error("The instance has been destroyed"));
|
||||
} else {
|
||||
if (this.injected) {
|
||||
const id = Date.now();
|
||||
this.promises[id] = { resove, reject };
|
||||
this.$iframe.contentWindow.postMessage(
|
||||
{
|
||||
type,
|
||||
data,
|
||||
id
|
||||
},
|
||||
"*"
|
||||
);
|
||||
} else {
|
||||
setTimeout(loop.bind(this), 200);
|
||||
}
|
||||
}
|
||||
}).call(this);
|
||||
});
|
||||
postMessage(message) {
|
||||
return postRequest(this, message);
|
||||
}
|
||||
commit(callback) {
|
||||
if (typeof callback !== "function") {
|
||||
@@ -130,6 +177,7 @@ ${data}
|
||||
destroy() {
|
||||
this.destroyed = true;
|
||||
window.removeEventListener("message", this.onMessage);
|
||||
cancelRequests(this);
|
||||
}
|
||||
}
|
||||
export {
|
||||
|
||||
+2
-2
@@ -51,7 +51,7 @@
|
||||
"ci:build": "yarn build:types && yarn build all && yarn build:i18n && yarn build:ts && yarn build:docs && yarn test:imports",
|
||||
"test:imports": "node --test test/esm.test.js test/i18n.test.js test/ssr.test.js",
|
||||
"typecheck": "node scripts/typecheck.mjs",
|
||||
"test:unit": "node --test test/iframe.test.js test/mediabunny.test.js test/mediabunny-shim.test.js test/mediabunny-coordination.test.js test/mediabunny-video.test.js test/mediabunny-audio.test.js test/mediabunny-hls.test.js test/mediabunny-entry.test.js test/mediabunny-capability.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js test/dpip.test.js test/dpip-lifecycle.test.js test/canvas.test.js test/canvas-lifecycle.test.js test/ambilight.test.js test/ambilight-lifecycle.test.js test/ambilight-proxy.test.js test/vast.test.js test/vast-lifecycle.test.js test/ads.test.js test/ads-lifecycle.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js",
|
||||
"test:unit": "node --test test/iframe-lifecycle.test.js test/iframe.test.js test/mediabunny.test.js test/mediabunny-shim.test.js test/mediabunny-coordination.test.js test/mediabunny-video.test.js test/mediabunny-audio.test.js test/mediabunny-hls.test.js test/mediabunny-entry.test.js test/mediabunny-capability.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js test/dpip.test.js test/dpip-lifecycle.test.js test/canvas.test.js test/canvas-lifecycle.test.js test/ambilight.test.js test/ambilight-lifecycle.test.js test/ambilight-proxy.test.js test/vast.test.js test/vast-lifecycle.test.js test/ads.test.js test/ads-lifecycle.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js",
|
||||
"test:coverage": "node --test test/coverage.test.js && node scripts/coverage.mjs",
|
||||
"test": "yarn test:node && yarn test:baseline",
|
||||
"test:browser": "playwright test",
|
||||
@@ -80,7 +80,7 @@
|
||||
"test:mediabunny-types-package": "node refactor/scripts/mb-package-types.mjs",
|
||||
"test:mediabunny-soak": "playwright test --config playwright.soak.config.js",
|
||||
"generate:media:hls": "node scripts/generate-hls-fixture.mjs",
|
||||
"test:iframe": "node --test test/iframe.test.js refactor/scripts/iframe-contract.test.mjs"
|
||||
"test:iframe": "node --test test/iframe.test.js test/iframe-lifecycle.test.js refactor/scripts/iframe-contract.test.mjs"
|
||||
},
|
||||
"browserslist": "last 1 Chrome version",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
# Iframe maintenance map
|
||||
|
||||
`src/index.ts` owns the existing class, constructor validation, bound parent
|
||||
message listener, child injection/execution protocol and public callbacks.
|
||||
`src/requests.ts` owns request IDs, pending promises, injection polling and
|
||||
settlement/cancellation. Its host interface uses only the iframe, registry and
|
||||
two lifecycle flags; it does not depend on ArtPlayer or import the entry class.
|
||||
|
||||
The entry initializes the same seven enumerable, writable fields in the same
|
||||
order. Internal request ownership lives in a module WeakMap, keeping instance
|
||||
reflection unchanged. `promises` remains the same ordinary object, exposing the
|
||||
historical `resove` spelling and `reject`. Do not rename `resove` or replace this
|
||||
public object with a Map. The wire envelope remains `{ type, data, id }`, with
|
||||
numeric IDs and wildcard target origin. Non-error responses with a matching ID
|
||||
are still accepted; `message` receives `{ type, data }` with the instance as `this`.
|
||||
|
||||
## Request lifecycle
|
||||
|
||||
Every call owns its promise immediately. Before injection it polls every 200ms,
|
||||
as before; injection does not flush the queue synchronously. On send it allocates
|
||||
a numeric ID from the greater of the current timestamp and the previous ID + 1.
|
||||
The counter is shared by instances in this module so two instances pointing at
|
||||
one child do not settle each other's requests. It also avoids reuse after clock
|
||||
rollback. Independently loaded copies of the library do not share this counter.
|
||||
|
||||
The request is recorded before native postMessage. Success, remote error, native
|
||||
send failure or an explicit public callback settle once and remove ownership,
|
||||
timer and registry entry. Native failures retain their original Error object,
|
||||
including failures reached from polling. `destroy()` first marks the instance
|
||||
destroyed, removes its listener, then immediately rejects both sent and waiting
|
||||
requests with `The instance has been destroyed`. Saved listener/timer callbacks
|
||||
become inert. Repeated destroy is harmless. There is no implicit request timeout.
|
||||
|
||||
These are deliberate defect corrections: callers holding a promise during
|
||||
destroy must handle rejection; previously sent requests remained pending forever.
|
||||
The public ID is a correlation number, and is no longer always equal to Date.now.
|
||||
Public callback invocation now removes its completed registry entry immediately.
|
||||
The own-property guard prevents inherited names such as `toString` being treated
|
||||
as requests. These differences are recorded under PKG-IFRAME-03.
|
||||
|
||||
## Compatibility still under review
|
||||
|
||||
Child `commit` still extracts the function body and evaluates it with the old
|
||||
`resolve(...)` convention. Simple commits post a response synchronously inside
|
||||
the async handler; resolver commits await the result. Execution errors still
|
||||
send an error packet and reject the handler. Expression arrow functions, closure
|
||||
capture and CSP restrictions have not been replaced with a new RPC protocol.
|
||||
|
||||
This checkpoint does not fix navigation/reinjection, malformed message handling
|
||||
or source/origin validation. The executable commit protocol is not a sandbox;
|
||||
the current listener still accepts messages from unrelated windows. IFRAME-LIFE-01
|
||||
and IFRAME-TRUST-01 remain open. Continue in PKG-IFRAME-03/05 before release.
|
||||
|
||||
Public declarations remain in `types/artplayer-tool-iframe.d.ts`; source typing
|
||||
does not yet establish historical declaration/entry compatibility. PKG-IFRAME-04
|
||||
must reconcile the old npm plugin name, namespace/default and extra helper
|
||||
protocol separately. PKG-IFRAME-06 owns final distribution. No version bump or
|
||||
publication is implied by this source migration.
|
||||
|
||||
## Verification
|
||||
|
||||
- `yarn test:iframe`: frozen historical contract/defect assertions and candidate
|
||||
lifecycle assertions. Historical failures are not candidate acceptance.
|
||||
- `yarn typecheck`: strict source and existing consumers; full package declaration
|
||||
consumers are still a separate PKG-IFRAME-04 gate.
|
||||
- `yarn build artplayer-tool-iframe`: normal main/legacy/ESM production output and
|
||||
generated docs copies. Never hand-edit those files.
|
||||
- `yarn test:browser test/browser/iframe.spec.js`: real same/cross-origin windows
|
||||
in Chromium, Firefox and WebKit. This tool-only fixture creates no player.
|
||||
- Set `ARTPLAYER_IFRAME_LIFECYCLE_ONLY=1` for candidate lifecycle browser rows;
|
||||
`ARTPLAYER_IFRAME_ARTIFACT` selects an actual built file. For an unchanged
|
||||
candidate test against the old workspace, set `ARTPLAYER_IFRAME_BASELINE=1`.
|
||||
Expected historical failures must be archived, not suppressed.
|
||||
|
||||
The existing local demo remains at
|
||||
`http://localhost:8082/?libs=./uncompiled/artplayer-tool-iframe/index.js&example=iframe`.
|
||||
Full demo/player integration, native devices and final release checks are later
|
||||
gates; the controlled iframe fixture is not evidence that those have passed.
|
||||
@@ -6,6 +6,11 @@ Iframe tool for ArtPlayer
|
||||
|
||||
[https://artplayer.org](https://artplayer.org/?libs=./uncompiled/artplayer-tool-iframe/index.js&example=iframe)
|
||||
|
||||
## Maintenance
|
||||
|
||||
See [ARCHITECTURE.md](ARCHITECTURE.md) for the TypeScript module map, request
|
||||
cleanup, compatibility constraints, executable checks and remaining migration work.
|
||||
|
||||
## License
|
||||
|
||||
MIT © Harvey Zhao
|
||||
@@ -4,4 +4,4 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(t.ArtplayerToolIframe=t(),define(function(){return t.ArtplayerToolIframe})):(e="undefined"!==typeof globalThis?globalThis:e||self).ArtplayerToolIframe=t()}(this,function(){"use strict";class e{static get iframe(){return window.top!==window}static postMessage({type:t,data:s,id:i=0}){if(!e.iframe)throw new Error('The "ArtplayerToolIframe.postMessage" method can only be used in iframe');window.parent.postMessage({type:t,data:s,id:i},"*")}static async onMessage(t){if(!e.iframe)throw new Error('The "ArtplayerToolIframe.onMessage" method can only be used in iframe');const{type:s,data:i,id:o}=t.data;if("commit"===s)try{if(i.match(/\bresolve\((.*?)\)/)){const t=`return new Promise(function(resolve){\n${i}\n})`,s=await new Function(t)();e.postMessage({type:"response",data:s,id:o})}else{const t=new Function(i)();e.postMessage({type:"response",data:t,id:o})}}catch(n){throw e.postMessage({type:"error",data:n.message,id:o}),n}}static inject(){if(!e.iframe)throw new Error('The "ArtplayerToolIframe.inject" method can only be used in iframe');e.postMessage({type:"inject"}),window.addEventListener("message",e.onMessage)}constructor({iframe:e,url:t}){if(e instanceof HTMLIFrameElement===!1)throw new TypeError('"option.iframe" needs to be a HTMLIFrameElement');if("string"!==typeof t)throw new TypeError('"option.url" needs to be a string');this.url=t,this.$iframe=e,this.promises={},this.injected=!1,this.destroyed=!1,this.messageCallback=()=>null,this.onMessage=this.onMessage.bind(this),window.addEventListener("message",this.onMessage),this.$iframe.src=this.url}onMessage(e){const{type:t,data:s,id:i}=e.data;if("inject"===t)this.injected=!0;this.promises[i]&&("error"===t?this.promises[i].reject(new Error(s)):this.promises[i].resove(s),delete this.promises[i]),this.messageCallback&&this.messageCallback({type:t,data:s})}postMessage({type:e,data:t}){return new Promise((s,i)=>{(function o(){if(this.destroyed)i(new Error("The instance has been destroyed"));else if(this.injected){const o=Date.now();this.promises[o]={resove:s,reject:i},this.$iframe.contentWindow.postMessage({type:e,data:t,id:o},"*")}else setTimeout(o.bind(this),200)}).call(this)})}commit(e){if("function"!==typeof e)throw new TypeError('"commit.callback" needs to be a function');const t=e.toString(),s=t.substring(t.indexOf("{")+1,t.lastIndexOf("}"));return this.postMessage({type:"commit",data:s})}message(e){if("function"!==typeof e)throw new TypeError('"message.callback" needs to be a function');this.messageCallback=e}destroy(){this.destroyed=!0,window.removeEventListener("message",this.onMessage)}}return e});
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.ArtplayerToolIframe=t(),define(function(){return e.ArtplayerToolIframe})):(e="undefined"!==typeof globalThis?globalThis:e||self).ArtplayerToolIframe=t()}(this,function(){"use strict";const e=new WeakMap;let t=0;class s{static get iframe(){return window.top!==window}static postMessage({type:e,data:t,id:o=0}){if(!s.iframe)throw new Error('The "ArtplayerToolIframe.postMessage" method can only be used in iframe');window.parent.postMessage({type:e,data:t,id:o},"*")}static async onMessage(e){if(!s.iframe)throw new Error('The "ArtplayerToolIframe.onMessage" method can only be used in iframe');const{type:t,data:o,id:i}=e.data;if("commit"===t)try{if(o.match(/\bresolve\((.*?)\)/)){const e=`return new Promise(function(resolve){\n${o}\n})`,t=await new Function(e)();s.postMessage({type:"response",data:t,id:i})}else{const e=new Function(o)();s.postMessage({type:"response",data:e,id:i})}}catch(r){throw s.postMessage({type:"error",data:r.message,id:i}),r}}static inject(){if(!s.iframe)throw new Error('The "ArtplayerToolIframe.inject" method can only be used in iframe');s.postMessage({type:"inject"}),window.addEventListener("message",s.onMessage)}constructor({iframe:e,url:t}){if(e instanceof HTMLIFrameElement===!1)throw new TypeError('"option.iframe" needs to be a HTMLIFrameElement');if("string"!==typeof t)throw new TypeError('"option.url" needs to be a string');this.url=t,this.$iframe=e,this.promises={},this.injected=!1,this.destroyed=!1,this.messageCallback=()=>null,this.onMessage=this.onMessage.bind(this),window.addEventListener("message",this.onMessage),this.$iframe.src=this.url}onMessage(e){if(this.destroyed)return;const{type:t,data:s,id:o}=e.data;if("inject"===t)this.injected=!0;void 0!==o&&Object.prototype.hasOwnProperty.call(this.promises,o)&&this.promises[o]&&("error"===t?this.promises[o].reject(new Error(s)):this.promises[o].resove(s),delete this.promises[o]),this.messageCallback&&this.messageCallback({type:t,data:s})}postMessage(s){return function(s,{type:o,data:i}){return new Promise((r,n)=>{if(s.destroyed)return void n(new Error("The instance has been destroyed"));const a=e.get(s)||new Set;e.set(s,a);const d=t=>!!a.delete(t)&&(void 0!==t.timer&&clearTimeout(t.timer),void 0!==t.id&&delete s.promises[t.id],a.size||e.delete(s),!0),c={callbacks:{resove(e){d(c)&&r(e)},reject(e){d(c)&&n(e)}}};a.add(c);const l=()=>{if(c.timer=void 0,a.has(c))if(s.destroyed)c.callbacks.reject(new Error("The instance has been destroyed"));else try{if(s.injected){const e=Math.max(Date.now(),t+1);t=e,c.id=e,s.promises[e]=c.callbacks,s.$iframe.contentWindow.postMessage({type:o,data:i,id:e},"*")}else c.timer=setTimeout(l,200)}catch(e){c.callbacks.reject(e)}};l()})}(this,s)}commit(e){if("function"!==typeof e)throw new TypeError('"commit.callback" needs to be a function');const t=e.toString(),s=t.substring(t.indexOf("{")+1,t.lastIndexOf("}"));return this.postMessage({type:"commit",data:s})}message(e){if("function"!==typeof e)throw new TypeError('"message.callback" needs to be a function');this.messageCallback=e}destroy(){this.destroyed=!0,window.removeEventListener("message",this.onMessage),function(t){const s=e.get(t);if(s)for(const e of[...s])e.callbacks.reject(new Error("The instance has been destroyed"))}(this)}}return s});
|
||||
@@ -4,4 +4,4 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(t.ArtplayerToolIframe=t(),define(function(){return t.ArtplayerToolIframe})):(e="undefined"!==typeof globalThis?globalThis:e||self).ArtplayerToolIframe=t()}(this,function(){"use strict";class e{static get iframe(){return window.top!==window}static postMessage({type:t,data:s,id:o=0}){if(!e.iframe)throw new Error('The "ArtplayerToolIframe.postMessage" method can only be used in iframe');window.parent.postMessage({type:t,data:s,id:o},"*")}static onMessage(t){return s=this,o=null,i=function*(){if(!e.iframe)throw new Error('The "ArtplayerToolIframe.onMessage" method can only be used in iframe');const{type:s,data:o,id:i}=t.data;if("commit"===s)try{if(o.match(/\bresolve\((.*?)\)/)){const t=`return new Promise(function(resolve){\n${o}\n})`,s=yield new Function(t)();e.postMessage({type:"response",data:s,id:i})}else{const t=new Function(o)();e.postMessage({type:"response",data:t,id:i})}}catch(n){throw e.postMessage({type:"error",data:n.message,id:i}),n}},new Promise((e,t)=>{var n=e=>{try{a(i.next(e))}catch(s){t(s)}},r=e=>{try{a(i.throw(e))}catch(s){t(s)}},a=t=>t.done?e(t.value):Promise.resolve(t.value).then(n,r);a((i=i.apply(s,o)).next())});var s,o,i}static inject(){if(!e.iframe)throw new Error('The "ArtplayerToolIframe.inject" method can only be used in iframe');e.postMessage({type:"inject"}),window.addEventListener("message",e.onMessage)}constructor({iframe:e,url:t}){if(e instanceof HTMLIFrameElement===!1)throw new TypeError('"option.iframe" needs to be a HTMLIFrameElement');if("string"!==typeof t)throw new TypeError('"option.url" needs to be a string');this.url=t,this.$iframe=e,this.promises={},this.injected=!1,this.destroyed=!1,this.messageCallback=()=>null,this.onMessage=this.onMessage.bind(this),window.addEventListener("message",this.onMessage),this.$iframe.src=this.url}onMessage(e){const{type:t,data:s,id:o}=e.data;if("inject"===t)this.injected=!0;this.promises[o]&&("error"===t?this.promises[o].reject(new Error(s)):this.promises[o].resove(s),delete this.promises[o]),this.messageCallback&&this.messageCallback({type:t,data:s})}postMessage({type:e,data:t}){return new Promise((s,o)=>{(function i(){if(this.destroyed)o(new Error("The instance has been destroyed"));else if(this.injected){const i=Date.now();this.promises[i]={resove:s,reject:o},this.$iframe.contentWindow.postMessage({type:e,data:t,id:i},"*")}else setTimeout(i.bind(this),200)}).call(this)})}commit(e){if("function"!==typeof e)throw new TypeError('"commit.callback" needs to be a function');const t=e.toString(),s=t.substring(t.indexOf("{")+1,t.lastIndexOf("}"));return this.postMessage({type:"commit",data:s})}message(e){if("function"!==typeof e)throw new TypeError('"message.callback" needs to be a function');this.messageCallback=e}destroy(){this.destroyed=!0,window.removeEventListener("message",this.onMessage)}}return e});
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.ArtplayerToolIframe=t(),define(function(){return e.ArtplayerToolIframe})):(e="undefined"!==typeof globalThis?globalThis:e||self).ArtplayerToolIframe=t()}(this,function(){"use strict";const e=new WeakMap;let t=0;class s{static get iframe(){return window.top!==window}static postMessage({type:e,data:t,id:o=0}){if(!s.iframe)throw new Error('The "ArtplayerToolIframe.postMessage" method can only be used in iframe');window.parent.postMessage({type:e,data:t,id:o},"*")}static onMessage(e){return t=this,o=null,r=function*(){if(!s.iframe)throw new Error('The "ArtplayerToolIframe.onMessage" method can only be used in iframe');const{type:t,data:o,id:r}=e.data;if("commit"===t)try{if(o.match(/\bresolve\((.*?)\)/)){const e=`return new Promise(function(resolve){\n${o}\n})`,t=yield new Function(e)();s.postMessage({type:"response",data:t,id:r})}else{const e=new Function(o)();s.postMessage({type:"response",data:e,id:r})}}catch(i){throw s.postMessage({type:"error",data:i.message,id:r}),i}},new Promise((e,s)=>{var i=e=>{try{a(r.next(e))}catch(t){s(t)}},n=e=>{try{a(r.throw(e))}catch(t){s(t)}},a=t=>t.done?e(t.value):Promise.resolve(t.value).then(i,n);a((r=r.apply(t,o)).next())});var t,o,r}static inject(){if(!s.iframe)throw new Error('The "ArtplayerToolIframe.inject" method can only be used in iframe');s.postMessage({type:"inject"}),window.addEventListener("message",s.onMessage)}constructor({iframe:e,url:t}){if(e instanceof HTMLIFrameElement===!1)throw new TypeError('"option.iframe" needs to be a HTMLIFrameElement');if("string"!==typeof t)throw new TypeError('"option.url" needs to be a string');this.url=t,this.$iframe=e,this.promises={},this.injected=!1,this.destroyed=!1,this.messageCallback=()=>null,this.onMessage=this.onMessage.bind(this),window.addEventListener("message",this.onMessage),this.$iframe.src=this.url}onMessage(e){if(this.destroyed)return;const{type:t,data:s,id:o}=e.data;if("inject"===t)this.injected=!0;void 0!==o&&Object.prototype.hasOwnProperty.call(this.promises,o)&&this.promises[o]&&("error"===t?this.promises[o].reject(new Error(s)):this.promises[o].resove(s),delete this.promises[o]),this.messageCallback&&this.messageCallback({type:t,data:s})}postMessage(s){return function(s,{type:o,data:r}){return new Promise((i,n)=>{if(s.destroyed)return void n(new Error("The instance has been destroyed"));const a=e.get(s)||new Set;e.set(s,a);const d=t=>!!a.delete(t)&&(void 0!==t.timer&&clearTimeout(t.timer),void 0!==t.id&&delete s.promises[t.id],a.size||e.delete(s),!0),c={callbacks:{resove(e){d(c)&&i(e)},reject(e){d(c)&&n(e)}}};a.add(c);const l=()=>{if(c.timer=void 0,a.has(c))if(s.destroyed)c.callbacks.reject(new Error("The instance has been destroyed"));else try{if(s.injected){const e=Math.max(Date.now(),t+1);t=e,c.id=e,s.promises[e]=c.callbacks,s.$iframe.contentWindow.postMessage({type:o,data:r,id:e},"*")}else c.timer=setTimeout(l,200)}catch(e){c.callbacks.reject(e)}};l()})}(this,s)}commit(e){if("function"!==typeof e)throw new TypeError('"commit.callback" needs to be a function');const t=e.toString(),s=t.substring(t.indexOf("{")+1,t.lastIndexOf("}"));return this.postMessage({type:"commit",data:s})}message(e){if("function"!==typeof e)throw new TypeError('"message.callback" needs to be a function');this.messageCallback=e}destroy(){this.destroyed=!0,window.removeEventListener("message",this.onMessage),function(t){const s=e.get(t);if(s)for(const e of[...s])e.callbacks.reject(new Error("The instance has been destroyed"))}(this)}}return s});
|
||||
+72
-24
@@ -4,6 +4,72 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
const requests = /* @__PURE__ */ new WeakMap();
|
||||
let lastId = 0;
|
||||
function postRequest(host, { type, data }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (host.destroyed) {
|
||||
reject(new Error("The instance has been destroyed"));
|
||||
return;
|
||||
}
|
||||
const pending = requests.get(host) || /* @__PURE__ */ new Set();
|
||||
requests.set(host, pending);
|
||||
const finish = (request2) => {
|
||||
if (!pending.delete(request2))
|
||||
return false;
|
||||
if (request2.timer !== void 0)
|
||||
clearTimeout(request2.timer);
|
||||
if (request2.id !== void 0)
|
||||
delete host.promises[request2.id];
|
||||
if (!pending.size)
|
||||
requests.delete(host);
|
||||
return true;
|
||||
};
|
||||
const request = {
|
||||
callbacks: {
|
||||
resove(value) {
|
||||
if (finish(request))
|
||||
resolve(value);
|
||||
},
|
||||
reject(error) {
|
||||
if (finish(request))
|
||||
reject(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
pending.add(request);
|
||||
const loop = () => {
|
||||
request.timer = void 0;
|
||||
if (!pending.has(request))
|
||||
return;
|
||||
if (host.destroyed) {
|
||||
request.callbacks.reject(new Error("The instance has been destroyed"));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (host.injected) {
|
||||
const id = Math.max(Date.now(), lastId + 1);
|
||||
lastId = id;
|
||||
request.id = id;
|
||||
host.promises[id] = request.callbacks;
|
||||
host.$iframe.contentWindow.postMessage({ type, data, id }, "*");
|
||||
} else {
|
||||
request.timer = setTimeout(loop, 200);
|
||||
}
|
||||
} catch (error) {
|
||||
request.callbacks.reject(error);
|
||||
}
|
||||
};
|
||||
loop();
|
||||
});
|
||||
}
|
||||
function cancelRequests(host) {
|
||||
const pending = requests.get(host);
|
||||
if (!pending)
|
||||
return;
|
||||
for (const request of [...pending])
|
||||
request.callbacks.reject(new Error("The instance has been destroyed"));
|
||||
}
|
||||
class ArtplayerToolIframe {
|
||||
static get iframe() {
|
||||
return window.top !== window;
|
||||
@@ -71,13 +137,15 @@ ${data}
|
||||
this.$iframe.src = this.url;
|
||||
}
|
||||
onMessage(event) {
|
||||
if (this.destroyed)
|
||||
return;
|
||||
const { type, data, id } = event.data;
|
||||
switch (type) {
|
||||
case "inject":
|
||||
this.injected = true;
|
||||
break;
|
||||
}
|
||||
if (this.promises[id]) {
|
||||
if (id !== void 0 && Object.prototype.hasOwnProperty.call(this.promises, id) && this.promises[id]) {
|
||||
if (type === "error") {
|
||||
this.promises[id].reject(new Error(data));
|
||||
} else {
|
||||
@@ -89,29 +157,8 @@ ${data}
|
||||
this.messageCallback({ type, data });
|
||||
}
|
||||
}
|
||||
postMessage({ type, data }) {
|
||||
return new Promise((resove, reject) => {
|
||||
(function loop() {
|
||||
if (this.destroyed) {
|
||||
reject(new Error("The instance has been destroyed"));
|
||||
} else {
|
||||
if (this.injected) {
|
||||
const id = Date.now();
|
||||
this.promises[id] = { resove, reject };
|
||||
this.$iframe.contentWindow.postMessage(
|
||||
{
|
||||
type,
|
||||
data,
|
||||
id
|
||||
},
|
||||
"*"
|
||||
);
|
||||
} else {
|
||||
setTimeout(loop.bind(this), 200);
|
||||
}
|
||||
}
|
||||
}).call(this);
|
||||
});
|
||||
postMessage(message) {
|
||||
return postRequest(this, message);
|
||||
}
|
||||
commit(callback) {
|
||||
if (typeof callback !== "function") {
|
||||
@@ -130,6 +177,7 @@ ${data}
|
||||
destroy() {
|
||||
this.destroyed = true;
|
||||
window.removeEventListener("message", this.onMessage);
|
||||
cancelRequests(this);
|
||||
}
|
||||
}
|
||||
export {
|
||||
|
||||
+26
-35
@@ -1,9 +1,19 @@
|
||||
import type { Callbacks, Packet } from './requests'
|
||||
import { cancelRequests, postRequest } from './requests'
|
||||
|
||||
export default class ArtplayerToolIframe {
|
||||
declare url: string
|
||||
declare $iframe: HTMLIFrameElement
|
||||
declare promises: Record<number, Callbacks>
|
||||
declare injected: boolean
|
||||
declare destroyed: boolean
|
||||
declare messageCallback: ((...args: any[]) => any) | null
|
||||
|
||||
static get iframe() {
|
||||
return window.top !== window
|
||||
}
|
||||
|
||||
static postMessage({ type, data, id = 0 }) {
|
||||
static postMessage({ type, data, id = 0 }: Packet) {
|
||||
if (!ArtplayerToolIframe.iframe) {
|
||||
throw new Error('The "ArtplayerToolIframe.postMessage" method can only be used in iframe')
|
||||
}
|
||||
@@ -18,7 +28,7 @@ export default class ArtplayerToolIframe {
|
||||
)
|
||||
}
|
||||
|
||||
static async onMessage(event) {
|
||||
static async onMessage(event: MessageEvent<Packet>) {
|
||||
if (!ArtplayerToolIframe.iframe) {
|
||||
throw new Error('The "ArtplayerToolIframe.onMessage" method can only be used in iframe')
|
||||
}
|
||||
@@ -40,7 +50,7 @@ export default class ArtplayerToolIframe {
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
ArtplayerToolIframe.postMessage({ type: 'error', data: error.message, id })
|
||||
ArtplayerToolIframe.postMessage({ type: 'error', data: (error as { message: string }).message, id })
|
||||
throw error
|
||||
}
|
||||
break
|
||||
@@ -58,7 +68,7 @@ export default class ArtplayerToolIframe {
|
||||
window.addEventListener('message', ArtplayerToolIframe.onMessage)
|
||||
}
|
||||
|
||||
constructor({ iframe, url }) {
|
||||
constructor({ iframe, url }: { iframe: HTMLIFrameElement, url: string }) {
|
||||
if (iframe instanceof HTMLIFrameElement === false) {
|
||||
throw new TypeError('"option.iframe" needs to be a HTMLIFrameElement')
|
||||
}
|
||||
@@ -78,7 +88,10 @@ export default class ArtplayerToolIframe {
|
||||
this.$iframe.src = this.url
|
||||
}
|
||||
|
||||
onMessage(event) {
|
||||
onMessage(event: MessageEvent<Packet>) {
|
||||
if (this.destroyed)
|
||||
return
|
||||
|
||||
const { type, data, id } = event.data
|
||||
|
||||
switch (type) {
|
||||
@@ -89,12 +102,12 @@ export default class ArtplayerToolIframe {
|
||||
break
|
||||
}
|
||||
|
||||
if (this.promises[id]) {
|
||||
if (id !== undefined && Object.prototype.hasOwnProperty.call(this.promises, id) && this.promises[id]) {
|
||||
if (type === 'error') {
|
||||
this.promises[id].reject(new Error(data))
|
||||
this.promises[id]!.reject(new Error(data))
|
||||
}
|
||||
else {
|
||||
this.promises[id].resove(data)
|
||||
this.promises[id]!.resove(data)
|
||||
}
|
||||
delete this.promises[id]
|
||||
}
|
||||
@@ -104,34 +117,11 @@ export default class ArtplayerToolIframe {
|
||||
}
|
||||
}
|
||||
|
||||
postMessage({ type, data }) {
|
||||
return new Promise((resove, reject) => {
|
||||
(function loop() {
|
||||
if (this.destroyed) {
|
||||
reject(new Error('The instance has been destroyed'))
|
||||
}
|
||||
else {
|
||||
if (this.injected) {
|
||||
const id = Date.now()
|
||||
this.promises[id] = { resove, reject }
|
||||
this.$iframe.contentWindow.postMessage(
|
||||
{
|
||||
type,
|
||||
data,
|
||||
id,
|
||||
},
|
||||
'*',
|
||||
)
|
||||
}
|
||||
else {
|
||||
setTimeout(loop.bind(this), 200)
|
||||
}
|
||||
}
|
||||
}).call(this)
|
||||
})
|
||||
postMessage(message: Packet): Promise<any> {
|
||||
return postRequest(this, message)
|
||||
}
|
||||
|
||||
commit(callback) {
|
||||
commit<T extends (...args: any[]) => any>(callback: T): Promise<ReturnType<T>> {
|
||||
if (typeof callback !== 'function') {
|
||||
throw new TypeError('"commit.callback" needs to be a function')
|
||||
}
|
||||
@@ -140,7 +130,7 @@ export default class ArtplayerToolIframe {
|
||||
return this.postMessage({ type: 'commit', data: bodyString })
|
||||
}
|
||||
|
||||
message(callback) {
|
||||
message(callback: (...args: any[]) => any) {
|
||||
if (typeof callback !== 'function') {
|
||||
throw new TypeError('"message.callback" needs to be a function')
|
||||
}
|
||||
@@ -150,5 +140,6 @@ export default class ArtplayerToolIframe {
|
||||
destroy() {
|
||||
this.destroyed = true
|
||||
window.removeEventListener('message', this.onMessage)
|
||||
cancelRequests(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
export interface Packet {
|
||||
type: string
|
||||
data?: any
|
||||
id?: number
|
||||
}
|
||||
|
||||
export interface Callbacks {
|
||||
resove: (value?: any) => void
|
||||
reject: (reason?: any) => void
|
||||
}
|
||||
|
||||
export interface RequestHost {
|
||||
$iframe: HTMLIFrameElement
|
||||
promises: Record<number, Callbacks>
|
||||
injected: boolean
|
||||
destroyed: boolean
|
||||
}
|
||||
|
||||
interface Pending {
|
||||
id?: number
|
||||
timer?: ReturnType<typeof setTimeout>
|
||||
callbacks: Callbacks
|
||||
}
|
||||
|
||||
const requests = new WeakMap<RequestHost, Set<Pending>>()
|
||||
let lastId = 0
|
||||
|
||||
export function postRequest(host: RequestHost, { type, data }: Packet): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (host.destroyed) {
|
||||
reject(new Error('The instance has been destroyed'))
|
||||
return
|
||||
}
|
||||
const pending = requests.get(host) || new Set<Pending>()
|
||||
requests.set(host, pending)
|
||||
const finish = (request: Pending) => {
|
||||
if (!pending.delete(request))
|
||||
return false
|
||||
if (request.timer !== undefined)
|
||||
clearTimeout(request.timer)
|
||||
if (request.id !== undefined)
|
||||
delete host.promises[request.id]
|
||||
if (!pending.size)
|
||||
requests.delete(host)
|
||||
return true
|
||||
}
|
||||
const request: Pending = {
|
||||
callbacks: {
|
||||
resove(value) {
|
||||
if (finish(request))
|
||||
resolve(value)
|
||||
},
|
||||
reject(error) {
|
||||
if (finish(request))
|
||||
reject(error)
|
||||
},
|
||||
},
|
||||
}
|
||||
pending.add(request)
|
||||
const loop = () => {
|
||||
request.timer = undefined
|
||||
if (!pending.has(request))
|
||||
return
|
||||
if (host.destroyed) {
|
||||
request.callbacks.reject(new Error('The instance has been destroyed'))
|
||||
return
|
||||
}
|
||||
try {
|
||||
if (host.injected) {
|
||||
const id = Math.max(Date.now(), lastId + 1)
|
||||
lastId = id
|
||||
request.id = id
|
||||
host.promises[id] = request.callbacks
|
||||
host.$iframe.contentWindow!.postMessage({ type, data, id }, '*')
|
||||
}
|
||||
else {
|
||||
request.timer = setTimeout(loop, 200)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
request.callbacks.reject(error)
|
||||
}
|
||||
}
|
||||
loop()
|
||||
})
|
||||
}
|
||||
|
||||
export function cancelRequests(host: RequestHost): void {
|
||||
const pending = requests.get(host)
|
||||
if (!pending)
|
||||
return
|
||||
for (const request of [...pending])
|
||||
request.callbacks.reject(new Error('The instance has been destroyed'))
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": { "allowJs": false },
|
||||
"include": ["src/**/*.ts", "types/**/*.d.ts"]
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,82 @@
|
||||
# PKG-IFRAME-03 request ownership checkpoint (doing)
|
||||
|
||||
The old single JS file is now a strict TS public entry plus a request ownership
|
||||
module. Entry methods and fields remain in place; the private WeakMap does not
|
||||
add reflective instance keys. Source types are checked by a new package tsconfig,
|
||||
without changing published declarations or installing dependencies. Package
|
||||
ARCHITECTURE.md explains state, ownership, compatibility and follow-up work.
|
||||
|
||||
## Corrections and compatibility
|
||||
|
||||
| Boundary | Previous result | Candidate result |
|
||||
| --- | --- | --- |
|
||||
| Same millisecond / clock rollback | Callbacks overwrite each other or old IDs recur | Module-wide increasing numeric IDs preserve independent responses |
|
||||
| Multiple instances, one frame | Each instance can settle from the same response ID | Instances from the same module allocate distinct IDs |
|
||||
| Destroy sent/waiting request | Sent promise hangs; waiting poll remains until its next tick | Immediate rejection with the existing destroyed Error message, timers/registry released |
|
||||
| Saved callback after destroy | Can settle or notify | Inert |
|
||||
| Native send failure | Immediate rejection leaks record; delayed failure escapes callback | Same original error rejects in both paths, all owned state removed |
|
||||
| Public resove/reject | Promise settles but its registry entry remains | Promise and corresponding owned registry entry released once |
|
||||
| Inherited response ID | `toString` can be mistaken for a request and throws | Only own registry entries participate, generic message callback still runs |
|
||||
|
||||
These are deliberate fixes under API-02/03/04/05/12, not a new wire format.
|
||||
Constructor validation, seven field descriptors/order, bound onMessage, callback
|
||||
receiver/packet shape, numeric envelopes, `resove`, ordinary registry identity,
|
||||
wildcard target origin, 200ms polling, generic non-error responses, function-body
|
||||
commit serialization, child sync/resolve timing and child error channel remain.
|
||||
An ID is now a correlation number rather than an exact clock reading. Callers
|
||||
must handle pending request rejection on destroy; old promises hung indefinitely.
|
||||
No automatic timeout, new public option, SDK or version change is introduced.
|
||||
|
||||
The counter is local to a library module; separately evaluated copies are not
|
||||
coordinated. Source typing uses `any` only for the existing arbitrary message
|
||||
payload/callback/result boundary; ownership and state are explicit. The child
|
||||
error `.message` assertion intentionally retains the old error serialization,
|
||||
including its existing behavior for unusual thrown values. The contentWindow
|
||||
assertion does not add an availability guarantee: a null window throws inside the
|
||||
send boundary and rejects the request rather than leaving it owned.
|
||||
|
||||
## Validation
|
||||
|
||||
The same 16 candidate Node assertions against the frozen workspace pass 5 and
|
||||
fail 11; candidate source passes all 16. The unchanged candidate browser checks
|
||||
against that old artifact pass 2 round trips and fail 8 lifecycle cases across
|
||||
same/cross-origin Chromium. Full failure reports/results/traces are archived in
|
||||
`refactor/.cache/iframe03-before-results`; Node log is `iframe03-before-node.log`.
|
||||
This contrasts candidate acceptance with the separate historical defect assertions.
|
||||
|
||||
During the independent diff review, the new own-property guard was found to
|
||||
throw for consumer-cleared `promises[id] = undefined`, whereas the old workspace
|
||||
ignored it. `iframe03-empty-record-before.log` records that old/current contrast.
|
||||
The guard now retains the old truthiness check as well; the existing regression
|
||||
case covers this edge. Earlier passing CI/browser records predate this fix and
|
||||
remain archived; final candidate builds and checks are rerun against the new bytes.
|
||||
|
||||
Final production/browser/CI results and hashes are recorded in the checkpoint
|
||||
validation file after execution. No player is created by these iframe-only
|
||||
fixtures; they do not establish full demo/core/media integration.
|
||||
|
||||
## Remaining work and rollback
|
||||
|
||||
PKG-IFRAME-03 remains doing. Navigation/reinjection, malformed packets, constructor
|
||||
setup failure cleanup and the independent source/origin policy still need work.
|
||||
IFRAME-LIFE-01, IFRAME-TRUST-01 and IFRAME-DIST-01 remain open. Trust policy has not
|
||||
silently changed; current foreign-message behavior is still a known defect.
|
||||
PKG-IFRAME-04 owns declaration/helper/old-name compatibility and 05/06 own complete
|
||||
integration/distribution. This checkpoint is not release acceptance.
|
||||
|
||||
Rollback by reverting this checkpoint and rebuilding `artplayer-tool-iframe`
|
||||
through the normal script. The frozen artifacts and previous evidence remain
|
||||
immutable. Commit subject: `fix(iframe): [PKG-IFRAME-03] own pending requests and cancellation`.
|
||||
|
||||
Final source/main/legacy Node checks each pass 16. Final main browser matrix
|
||||
passes 174 (144 historical defect assertions + 30 candidate acceptance), and
|
||||
final legacy passes 30 candidate rows. All three engines execute real windows;
|
||||
there are no skips/capability substitutes. CI passes 1505 (1331 unit + 14
|
||||
engineering + 160 baseline), 44 repeated contracts and 326 production TS files;
|
||||
3 import/SSR files pass after the final rebuild. All hashes match archived
|
||||
reports and the final source/artifacts; docs copies are byte-identical.
|
||||
|
||||
One normal build attempt failed copying the legacy file into docs with Windows
|
||||
copyfile UNKNOWN; the cause is not established. The failure log is retained,
|
||||
and a normal build rerun succeeded before final checks. No manual artifact edit
|
||||
or ignored build failure was used. Status stays doing and all three risks open.
|
||||
+3
-2
@@ -4,7 +4,7 @@
|
||||
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 222 项,范围 22 个包及工作区/示例。
|
||||
|
||||
状态:todo 106 / doing 9 / blocked 0 / done 107 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
状态:todo 105 / doing 10 / blocked 0 / done 107 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
|
||||
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
|
||||
|
||||
@@ -346,7 +346,7 @@
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| PKG-IFRAME-01 | artplayer-tool-iframe<br>核对包契约与历史用法 | BASE-05 | constructor/commit/message/inject、postMessage 协议与历史公开拼写 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | H | done |
|
||||
| PKG-IFRAME-02 | artplayer-tool-iframe<br>建立特有行为与错误测试 | PKG-IFRAME-01, ENG-03, ENG-05 | 跨窗口消息、ID 匹配、请求失败、重复 inject、销毁中请求 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done |
|
||||
| PKG-IFRAME-03 | artplayer-tool-iframe<br>整理内部职责与资源 | PKG-IFRAME-02, CORE-02, BASE-07 | 请求注册/响应匹配/监听清理分离;origin/source 安全边界独立决策 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | todo |
|
||||
| PKG-IFRAME-03 | artplayer-tool-iframe<br>整理内部职责与资源 | PKG-IFRAME-02, CORE-02, BASE-07 | 请求注册/响应匹配/监听清理分离;origin/source 安全边界独立决策 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | doing |
|
||||
| PKG-IFRAME-04 | artplayer-tool-iframe<br>迁移自有源码和公开类型 | PKG-IFRAME-03, ENG-04, ENG-06, CORE-07 | 消息联合类型、回调/Promise 推导、旧公开字段兼容;旧npm export=与实际namespace、额外helper协议分别核验 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | todo |
|
||||
| PKG-IFRAME-05 | artplayer-tool-iframe<br>验证新旧核心和组合 | PKG-IFRAME-04, CORE-22 | 真实同源/跨源 iframe、既有 commit 协议;安全变化有独立结论 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
|
||||
| PKG-IFRAME-06 | artplayer-tool-iframe<br>验证分发并同步文档 | PKG-IFRAME-05, ENG-07 | iframe.js、示例集成和原 script/class 导出验证;旧包名/额外helper深入口与新工具名的迁移结论 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | H | todo |
|
||||
@@ -532,4 +532,5 @@
|
||||
- PKG-MB-09: [记录](changes/2026-09-12-PKG-MB-09-native-pip-checkpoint.md) [记录](baselines/mb-native-pip-checkpoint.json) [记录](changes/2026-09-12-PKG-MB-09-sustained-playback-checkpoint.md) [记录](baselines/mb-sustained-validation.json) [记录](baselines/mb-sustained-media.json)
|
||||
- PKG-IFRAME-01: [记录](changes/2026-09-12-PKG-IFRAME-01-contract.md) [记录](baselines/iframe-release.json) [记录](baselines/iframe-contract.md) [记录](baselines/iframe-contract-validation.json)
|
||||
- PKG-IFRAME-02: [记录](changes/2026-09-12-PKG-IFRAME-02-behavior.md) [记录](baselines/iframe-behavior-validation.json)
|
||||
- PKG-IFRAME-03: [记录](changes/2026-09-12-PKG-IFRAME-03-requests-checkpoint.md) [记录](baselines/iframe-requests-checkpoint.json)
|
||||
- PKG-FACTORY-01: [记录](baselines/factory-assignment-gaps.json) [记录](baselines/factory-compatibility-proposals.json) [记录](factory-compatibility-decision.md) [记录](changes/2026-09-12-PKG-FACTORY-01-decision.md)
|
||||
@@ -1,5 +1,18 @@
|
||||
# 进度与证据
|
||||
|
||||
## PKG-IFRAME-03 请求资源与TS入口检查点(doing)
|
||||
|
||||
入口和请求管理拆为2个严格TS模块,保留7个公开字段及resove/回调/消息协议。
|
||||
修复ID冲突/回拨、跨实例串线、销毁悬挂、轮询发送错误和公开回调的资源泄漏。
|
||||
同16断言旧版5通过/11失败,候选源码/main/legacy各16通过;旧版真实Chromium
|
||||
候选断言2通过/8失败。最终main浏览器174(历史144+候选30)、legacy候选30通过,
|
||||
完整CI1505与44重复契约、3导入/SSR文件通过,326生产TS。审查发现并修复空记录
|
||||
兼容回归;一次Windows复制失败经正常重建恢复,全部旧/失败/最终报告保留。
|
||||
见[变更](changes/2026-09-12-PKG-IFRAME-03-requests-checkpoint.md)和
|
||||
[验证](baselines/iframe-requests-checkpoint.json)。222项:107 done、10 doing、105 todo。
|
||||
本地检查点提交;IFRAME-03和三个风险仍未关闭。下一步导航/重新注入、构造失败清理、
|
||||
畸形消息与独立来源信任策略,随后04处理公开声明与旧helper/分发兼容。
|
||||
|
||||
## PKG-IFRAME-02 完成:历史生命周期与真实窗口缺陷复现
|
||||
|
||||
52项Node和144项Chromium/Firefox/WebKit真实窗口检查通过,覆盖4份历史产物、同源/跨源。
|
||||
|
||||
+4
-2
@@ -3775,14 +3775,16 @@
|
||||
],
|
||||
"status": "open",
|
||||
"confirmation": "reproduced",
|
||||
"compatibleResolution": "Reproduce same-tick request collision, pending injection, response errors and destroy behavior before changing internal ownership; preserve public promises[id].resove.",
|
||||
"compatibleResolution": "Request ownership checkpoint fixes timestamp collisions, polling/send failures and terminal destruction while preserving public resove/registry/envelopes. Navigation/reinjection and setup failure cleanup remain for PKG-IFRAME-03; keep open until complete actual iframe lifecycle acceptance.",
|
||||
"closureCriteria": "Old behavior and candidate fixes have deterministic race/resource tests plus actual iframe integration.",
|
||||
"evidence": [
|
||||
"refactor/baselines/iframe-contract.md",
|
||||
"refactor/baselines/iframe-release.json",
|
||||
"refactor/changes/2026-09-12-PKG-IFRAME-01-contract.md",
|
||||
"refactor/baselines/iframe-behavior-validation.json",
|
||||
"refactor/changes/2026-09-12-PKG-IFRAME-02-behavior.md"
|
||||
"refactor/changes/2026-09-12-PKG-IFRAME-02-behavior.md",
|
||||
"refactor/changes/2026-09-12-PKG-IFRAME-03-requests-checkpoint.md",
|
||||
"refactor/baselines/iframe-requests-checkpoint.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
+5
-2
@@ -3656,11 +3656,14 @@
|
||||
"CORE-02",
|
||||
"BASE-07"
|
||||
],
|
||||
"status": "todo",
|
||||
"status": "doing",
|
||||
"risk": "H",
|
||||
"deliverable": "请求注册/响应匹配/监听清理分离;origin/source 安全边界独立决策",
|
||||
"acceptance": "结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过",
|
||||
"evidence": []
|
||||
"evidence": [
|
||||
"changes/2026-09-12-PKG-IFRAME-03-requests-checkpoint.md",
|
||||
"baselines/iframe-requests-checkpoint.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "PKG-IFRAME-04",
|
||||
|
||||
+48
-12
@@ -1,13 +1,20 @@
|
||||
import process from 'node:process'
|
||||
import { hash } from '../../refactor/scripts/releases.mjs'
|
||||
import { iframeHistorical } from '../helpers/iframe.js'
|
||||
import { iframeCandidate, iframeHistorical } from '../helpers/iframe.js'
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
const implementations = (await iframeHistorical()).filter(item => item.global !== 'ArtplayerHelperIframe')
|
||||
const candidate = { ...await iframeCandidate(), lifecycle: true }
|
||||
const implementations = process.env.ARTPLAYER_IFRAME_LIFECYCLE_ONLY === '1'
|
||||
? [candidate]
|
||||
: [...(await iframeHistorical()).filter(item => item.global !== 'ArtplayerHelperIframe'), candidate]
|
||||
|
||||
for (const implementation of implementations) {
|
||||
for (const relation of ['same-origin', 'cross-origin']) {
|
||||
for (const scenario of ['round-trip', 'same-tick', 'destroy-pending', 'foreign-message', 'navigation', 'clone-failure']) {
|
||||
test(`Iframe ${implementation.name}/${relation}: ${scenario} historical behavior`, async ({ page }, testInfo) => {
|
||||
const scenarios = implementation.lifecycle
|
||||
? ['round-trip', 'same-tick', 'destroy-pending', 'destroy-wait', 'clone-failure']
|
||||
: ['round-trip', 'same-tick', 'destroy-pending', 'foreign-message', 'navigation', 'clone-failure']
|
||||
for (const scenario of scenarios) {
|
||||
test(`Iframe ${implementation.name}/${relation}: ${scenario} ${implementation.lifecycle ? 'lifecycle acceptance' : 'historical behavior'}`, async ({ page }, testInfo) => {
|
||||
await page.goto('/test/player.html?core=published')
|
||||
const parentOrigin = new URL(page.url()).origin
|
||||
const alternate = parentOrigin.replace('127.0.0.1', 'localhost')
|
||||
@@ -33,7 +40,7 @@ for (const implementation of implementations) {
|
||||
</script>` }))
|
||||
await page.route('**/iframe-baseline-foreign', route => route.fulfill({ contentType: 'text/html', body: '<!doctype html><script>parent.postMessage({type:"inject",data:"foreign"},"*")</script>' }))
|
||||
await page.addScriptTag({ content: `(() => { const module = { exports: {} }; const exports = module.exports; ${implementation.code}; window.IframeFactory = module.exports.default || module.exports; })();` })
|
||||
const manualInjection = scenario === 'foreign-message' || scenario === 'clone-failure'
|
||||
const manualInjection = scenario === 'foreign-message' || scenario === 'clone-failure' || scenario === 'destroy-wait'
|
||||
const url = `${childOrigin}/iframe-baseline-child?auto=${manualInjection ? 0 : 1}&epoch=1`
|
||||
await page.evaluate((url) => {
|
||||
window.packets = []
|
||||
@@ -90,9 +97,11 @@ for (const implementation of implementations) {
|
||||
}
|
||||
finally { Date.now = now }
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.packets.filter(packet => packet.type === 'response' && packet.id === 1234).length)).toBe(2)
|
||||
await expect.poll(() => page.evaluate(() => window.packets.filter(packet => packet.type === 'response').length)).toBe(2)
|
||||
result = await page.evaluate(() => ({ states: window.states, pending: Object.keys(window.tool.promises).length }))
|
||||
expect(result).toEqual({ states: { first: { status: 'pending' }, second: { status: 'resolved', value: 1 } }, pending: 0 })
|
||||
expect(result).toEqual(implementation.lifecycle
|
||||
? { states: { first: { status: 'resolved', value: 1 }, second: { status: 'resolved', value: 2 } }, pending: 0 }
|
||||
: { states: { first: { status: 'pending' }, second: { status: 'resolved', value: 1 } }, pending: 0 })
|
||||
}
|
||||
else if (scenario === 'destroy-pending') {
|
||||
await page.evaluate(() => window.track('held', window.tool.postMessage({ type: 'hold' })))
|
||||
@@ -102,7 +111,22 @@ for (const implementation of implementations) {
|
||||
await child.evaluate(() => window.Tool.postMessage({ type: 'response', data: 'late', id: window.held.id }))
|
||||
await expect.poll(() => page.evaluate(() => window.packets.some(packet => packet.type === 'response' && packet.data === 'late'))).toBe(true)
|
||||
result = await page.evaluate(() => ({ destroyed: window.tool.destroyed, state: window.states.held, pending: Object.keys(window.tool.promises).length }))
|
||||
expect(result).toEqual({ destroyed: true, state: { status: 'pending' }, pending: 1 })
|
||||
expect(result).toEqual(implementation.lifecycle
|
||||
? { destroyed: true, state: { status: 'rejected', error: 'The instance has been destroyed' }, pending: 0 }
|
||||
: { destroyed: true, state: { status: 'pending' }, pending: 1 })
|
||||
}
|
||||
else if (scenario === 'destroy-wait') {
|
||||
result = await page.evaluate(async () => {
|
||||
window.track('waiting', window.tool.postMessage({ type: 'hold' }))
|
||||
window.tool.destroy()
|
||||
await Promise.resolve()
|
||||
return { state: window.states.waiting, pending: Object.keys(window.tool.promises).length }
|
||||
})
|
||||
expect(result).toEqual({ state: { status: 'rejected', error: 'The instance has been destroyed' }, pending: 0 })
|
||||
const child = page.frames().find(frame => frame.url() === url)
|
||||
await child.evaluate(() => window.Tool.inject())
|
||||
await expect.poll(() => page.evaluate(() => window.packets.some(packet => packet.type === 'inject'))).toBe(true)
|
||||
expect(await page.evaluate(() => window.tool.injected)).toBe(false)
|
||||
}
|
||||
else if (scenario === 'foreign-message') {
|
||||
expect(await page.evaluate(() => window.tool.injected)).toBe(false)
|
||||
@@ -143,9 +167,12 @@ for (const implementation of implementations) {
|
||||
})
|
||||
const child = page.frames().find(frame => frame.url() === url)
|
||||
await child.evaluate(() => window.Tool.inject())
|
||||
await expect.poll(() => page.evaluate(() => window.cloneErrors.length)).toBe(1)
|
||||
if (implementation.lifecycle)
|
||||
await expect.poll(() => page.evaluate(() => window.states.deferredClone.status)).toBe('rejected')
|
||||
else
|
||||
await expect.poll(() => page.evaluate(() => window.cloneErrors.length)).toBe(1)
|
||||
result = await page.evaluate(async () => {
|
||||
const deferredId = Object.keys(window.tool.promises)[0]
|
||||
const deferredId = Object.keys(window.tool.promises)[0] || Date.now()
|
||||
const now = Date.now
|
||||
Date.now = () => Number(deferredId) + 1
|
||||
let failure
|
||||
@@ -156,7 +183,16 @@ for (const implementation of implementations) {
|
||||
finally { Date.now = now }
|
||||
return { failure, deferred: window.states.deferredClone, pending: Object.keys(window.tool.promises).length, uncaught: window.cloneErrors }
|
||||
})
|
||||
expect(result).toEqual({ failure: 'DataCloneError', deferred: { status: 'pending' }, pending: 2, uncaught: ['DataCloneError'] })
|
||||
if (implementation.lifecycle) {
|
||||
expect(result.failure).toBe('DataCloneError')
|
||||
expect(result.deferred.status).toBe('rejected')
|
||||
expect(result.deferred.error).toMatch(/clon/i)
|
||||
expect(result.pending).toBe(0)
|
||||
expect(result.uncaught).toEqual([])
|
||||
}
|
||||
else {
|
||||
expect(result).toEqual({ failure: 'DataCloneError', deferred: { status: 'pending' }, pending: 2, uncaught: ['DataCloneError'] })
|
||||
}
|
||||
}
|
||||
else {
|
||||
const nextUrl = `${childOrigin}/iframe-baseline-child?auto=0&epoch=2`
|
||||
@@ -177,7 +213,7 @@ for (const implementation of implementations) {
|
||||
}
|
||||
const packets = await page.evaluate(() => window.packets)
|
||||
expect(packets.filter(packet => packet.fromChild).every(packet => packet.origin === childOrigin)).toBe(true)
|
||||
await testInfo.attach('iframe-behavior', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, sha256: hash(implementation.code), relation, scenario, parentOrigin, childOrigin, foreignOrigin, result, packets, scope: 'Actual browser iframe WindowProxy/source/origin and postMessage. Local route fulfills immutable library bytes. Date.now is controlled for collision and distinct clone-failure IDs. Only the deliberate child error/DataCloneError is intercepted; known historical defects are asserted, not fixed or waived.' }) })
|
||||
await testInfo.attach('iframe-behavior', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, sha256: hash(implementation.code), relation, scenario, parentOrigin, childOrigin, foreignOrigin, result, packets, lifecycleAcceptance: Boolean(implementation.lifecycle), scope: 'Actual browser iframe WindowProxy/source/origin and postMessage. Local route fulfills immutable library bytes. Date.now is controlled for collision and distinct clone-failure IDs. Only the deliberate child error/DataCloneError is intercepted. Historical defects are asserted for historical rows; lifecycle acceptance requires cleanup and absence of uncaught clone errors. Navigation and trust are not candidate acceptance yet.' }) })
|
||||
}
|
||||
finally {
|
||||
await page.evaluate(() => {
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import fs from 'node:fs'
|
||||
import process from 'node:process'
|
||||
import vm from 'node:vm'
|
||||
import { verifyIframeContract } from '../../refactor/scripts/iframe-contract.mjs'
|
||||
import { readMember } from '../../refactor/scripts/releases.mjs'
|
||||
import { compilePackage } from './load.js'
|
||||
|
||||
export async function iframeCandidate() {
|
||||
if (process.env.ARTPLAYER_IFRAME_BASELINE === '1')
|
||||
return (await iframeHistorical()).find(item => item.name === 'workspace.js')
|
||||
return { name: process.env.ARTPLAYER_IFRAME_ARTIFACT ? 'candidate-artifact' : 'candidate-source', global: 'ArtplayerToolIframe', namespace: false, code: process.env.ARTPLAYER_IFRAME_ARTIFACT ? fs.readFileSync(process.env.ARTPLAYER_IFRAME_ARTIFACT, 'utf8') : await compilePackage('artplayer-tool-iframe', 'umd') }
|
||||
}
|
||||
|
||||
export async function iframeHistorical() {
|
||||
const { baseline, archive, sources } = await verifyIframeContract()
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
import assert from 'node:assert/strict'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Candidate assertions also run unchanged against the frozen workspace.
|
||||
import test from 'node:test'
|
||||
import { iframeCandidate, iframeEnvironment } from './helpers/iframe.js'
|
||||
|
||||
const implementation = await iframeCandidate()
|
||||
const flush = () => new Promise(resolve => setImmediate(resolve))
|
||||
function observe(promise) {
|
||||
const state = { status: 'pending' }
|
||||
promise.then(value => Object.assign(state, { status: 'resolved', value }), error => Object.assign(state, { status: 'rejected', error: error.message }))
|
||||
return state
|
||||
}
|
||||
function setup(injected = true) {
|
||||
const env = iframeEnvironment(implementation)
|
||||
const instance = new env.Factory({ iframe: new env.Frame(), url: '/first' })
|
||||
if (injected)
|
||||
env.dispatch({ type: 'inject' })
|
||||
return { ...env, instance }
|
||||
}
|
||||
function tick(env) {
|
||||
for (const [id, timer] of [...env.timers]) {
|
||||
assert.equal(timer.delay, 200)
|
||||
env.timers.delete(id)
|
||||
timer.callback()
|
||||
}
|
||||
}
|
||||
|
||||
test('Iframe candidate preserves instance shape, descriptors, binding and listener-before-src order', () => {
|
||||
const env = setup(false)
|
||||
const instance = env.instance
|
||||
assert.deepEqual(Object.getOwnPropertyNames(env.Factory.prototype), ['constructor', 'onMessage', 'postMessage', 'commit', 'message', 'destroy'])
|
||||
assert.deepEqual(Object.keys(instance), ['url', '$iframe', 'promises', 'injected', 'destroyed', 'messageCallback', 'onMessage'])
|
||||
for (const key of Object.keys(instance)) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(instance, key)
|
||||
assert(descriptor.writable && descriptor.enumerable && descriptor.configurable)
|
||||
}
|
||||
assert.deepEqual(env.order, ['listen:message', 'src'])
|
||||
assert.equal(instance.messageCallback(), null)
|
||||
const receiver = instance.onMessage
|
||||
receiver({ data: { type: 'inject' } })
|
||||
assert.equal(instance.injected, true)
|
||||
assert.equal(instance.destroy(), undefined)
|
||||
})
|
||||
|
||||
test('Iframe candidate preserves numeric envelopes, resove spelling and generic response callback receiver', async () => {
|
||||
const env = setup()
|
||||
const registry = env.instance.promises
|
||||
const seen = []
|
||||
env.instance.message(function (packet) {
|
||||
seen.push({ receiver: this === env.instance, ...packet })
|
||||
})
|
||||
const pending = env.instance.postMessage({ type: 'custom-query', data: 3, id: 900 })
|
||||
assert.deepEqual(Object.keys(registry[1234]), ['resove', 'reject'])
|
||||
assert.deepEqual(JSON.parse(JSON.stringify(env.sent)), [{ packet: { type: 'custom-query', data: 3, id: 1234 }, origin: '*' }])
|
||||
env.dispatch({ type: 'custom-result', data: 4, id: 1234 })
|
||||
assert.equal(await pending, 4)
|
||||
assert.equal(registry, env.instance.promises)
|
||||
assert.deepEqual(Object.keys(registry), [])
|
||||
assert.deepEqual(seen, [{ receiver: true, type: 'custom-result', data: 4 }])
|
||||
env.instance.destroy()
|
||||
})
|
||||
|
||||
test('Iframe candidate matches simultaneous requests independently and out of order', async () => {
|
||||
const env = setup()
|
||||
const first = observe(env.instance.postMessage({ type: 'one' }))
|
||||
const second = observe(env.instance.postMessage({ type: 'two' }))
|
||||
assert.deepEqual(env.sent.map(item => item.packet.id), [1234, 1235])
|
||||
env.dispatch({ type: 'response', data: 2, id: 1235 })
|
||||
env.dispatch({ type: 'response', data: 1, id: 1234 })
|
||||
await flush()
|
||||
assert.deepEqual([first, second], [{ status: 'resolved', value: 1 }, { status: 'resolved', value: 2 }])
|
||||
assert.equal(Object.keys(env.instance.promises).length, 0)
|
||||
env.instance.destroy()
|
||||
})
|
||||
|
||||
test('Iframe candidate never reuses a settled ID when the clock moves backwards', async () => {
|
||||
const env = setup()
|
||||
const first = env.instance.postMessage({ type: 'one' })
|
||||
env.dispatch({ type: 'response', data: 1, id: 1234 })
|
||||
assert.equal(await first, 1)
|
||||
env.now(1000)
|
||||
const second = observe(env.instance.postMessage({ type: 'two' }))
|
||||
assert.equal(env.sent.at(-1).packet.id, 1235)
|
||||
env.dispatch({ type: 'response', data: 'duplicate', id: 1234 })
|
||||
await flush()
|
||||
assert.equal(second.status, 'pending')
|
||||
env.dispatch({ type: 'response', data: 2, id: 1235 })
|
||||
await flush()
|
||||
assert.deepEqual(second, { status: 'resolved', value: 2 })
|
||||
env.instance.destroy()
|
||||
})
|
||||
|
||||
test('Iframe candidate separates two instances sharing the same iframe window', async () => {
|
||||
const env = setup()
|
||||
const second = new env.Factory({ iframe: env.instance.$iframe, url: '/first' })
|
||||
env.dispatch({ type: 'inject' })
|
||||
const one = observe(env.instance.postMessage({ type: 'one' }))
|
||||
const two = observe(second.postMessage({ type: 'two' }))
|
||||
assert.deepEqual(env.sent.map(item => item.packet.id), [1234, 1235])
|
||||
env.dispatch({ type: 'response', data: 1, id: 1234 })
|
||||
env.dispatch({ type: 'response', data: 2, id: 1235 })
|
||||
await flush()
|
||||
assert.deepEqual([one, two], [{ status: 'resolved', value: 1 }, { status: 'resolved', value: 2 }])
|
||||
env.instance.destroy()
|
||||
second.destroy()
|
||||
})
|
||||
|
||||
test('Iframe candidate destroys sent requests immediately and ignores saved receivers', async () => {
|
||||
const env = setup()
|
||||
const state = observe(env.instance.postMessage({ type: 'one' }))
|
||||
const receiver = env.instance.onMessage
|
||||
let calls = 0
|
||||
env.instance.message(() => calls++)
|
||||
env.instance.destroy()
|
||||
receiver({ data: { type: 'response', data: 'late', id: 1234 } })
|
||||
await flush()
|
||||
assert.deepEqual(state, { status: 'rejected', error: 'The instance has been destroyed' })
|
||||
assert.equal(Object.keys(env.instance.promises).length, 0)
|
||||
assert.equal(calls, 0)
|
||||
assert.equal(env.handlers.get('message').size, 0)
|
||||
assert.equal(env.instance.destroy(), undefined)
|
||||
})
|
||||
|
||||
test('Iframe candidate destroys waiting requests immediately and cancels every polling timer', async () => {
|
||||
const env = setup(false)
|
||||
const states = Array.from({ length: 3 }, () => observe(env.instance.postMessage({ type: 'wait' })))
|
||||
const late = [...env.timers.values()].map(timer => timer.callback)
|
||||
env.instance.destroy()
|
||||
await flush()
|
||||
assert.equal(env.timers.size, 0)
|
||||
assert(states.every(state => state.status === 'rejected' && state.error === 'The instance has been destroyed'))
|
||||
late.forEach(callback => callback())
|
||||
assert.equal(env.timers.size, 0)
|
||||
assert.equal(env.sent.length, 0)
|
||||
})
|
||||
|
||||
test('Iframe candidate preserves 200ms injection waiting and allocates unique IDs after handshake', async () => {
|
||||
const env = setup(false)
|
||||
const one = observe(env.instance.postMessage({ type: 'one' }))
|
||||
const two = observe(env.instance.postMessage({ type: 'two' }))
|
||||
tick(env)
|
||||
assert.equal(env.sent.length, 0)
|
||||
assert.equal(env.timers.size, 2)
|
||||
env.dispatch({ type: 'inject' })
|
||||
assert.equal(env.sent.length, 0)
|
||||
tick(env)
|
||||
assert.equal(env.timers.size, 0)
|
||||
assert.deepEqual(env.sent.map(item => item.packet.id), [1234, 1235])
|
||||
env.dispatch({ type: 'response', data: 1, id: 1234 })
|
||||
env.dispatch({ type: 'error', data: 'remote failure', id: 1235 })
|
||||
await flush()
|
||||
assert.deepEqual([one, two], [{ status: 'resolved', value: 1 }, { status: 'rejected', error: 'remote failure' }])
|
||||
env.instance.destroy()
|
||||
})
|
||||
|
||||
for (const deferred of [false, true]) {
|
||||
test(`Iframe candidate clears ${deferred ? 'deferred' : 'immediate'} send failure and preserves the exact rejection`, async () => {
|
||||
const env = setup(!deferred)
|
||||
const failure = new Error('clone failure')
|
||||
env.instance.$iframe.contentWindow.postMessage = () => {
|
||||
throw failure
|
||||
}
|
||||
const promise = env.instance.postMessage({ type: 'query' })
|
||||
const rejected = assert.rejects(promise, error => error === failure)
|
||||
if (deferred) {
|
||||
env.dispatch({ type: 'inject' })
|
||||
tick(env)
|
||||
}
|
||||
await rejected
|
||||
assert.equal(Object.keys(env.instance.promises).length, 0)
|
||||
assert.equal(env.timers.size, 0)
|
||||
env.instance.destroy()
|
||||
})
|
||||
}
|
||||
|
||||
test('Iframe candidate rejects after destroy without allocating a request or timer', async () => {
|
||||
const env = setup(false)
|
||||
env.instance.destroy()
|
||||
await assert.rejects(env.instance.postMessage({ type: 'late' }), /The instance has been destroyed/)
|
||||
assert.equal(env.timers.size, 0)
|
||||
assert.equal(env.sent.length, 0)
|
||||
assert.equal(Object.keys(env.instance.promises).length, 0)
|
||||
})
|
||||
|
||||
test('Iframe candidate permits public resove/reject callbacks and releases their owned requests', async () => {
|
||||
const env = setup()
|
||||
const first = env.instance.postMessage({ type: 'one' })
|
||||
env.instance.promises[1234].resove(5)
|
||||
assert.equal(await first, 5)
|
||||
assert.equal(Object.keys(env.instance.promises).length, 0)
|
||||
const second = env.instance.postMessage({ type: 'two' })
|
||||
const rejected = assert.rejects(second, /external rejection/)
|
||||
env.instance.promises[env.sent.at(-1).packet.id].reject(new Error('external rejection'))
|
||||
await rejected
|
||||
assert.equal(Object.keys(env.instance.promises).length, 0)
|
||||
env.instance.destroy()
|
||||
})
|
||||
|
||||
test('Iframe candidate cancels owned promises even when public records were removed by a consumer', async () => {
|
||||
const env = setup()
|
||||
const state = observe(env.instance.postMessage({ type: 'one' }))
|
||||
delete env.instance.promises[1234]
|
||||
env.instance.destroy()
|
||||
await flush()
|
||||
assert.deepEqual(state, { status: 'rejected', error: 'The instance has been destroyed' })
|
||||
})
|
||||
|
||||
test('Iframe candidate ignores inherited request keys without changing generic message callbacks', () => {
|
||||
const env = setup()
|
||||
const seen = []
|
||||
env.instance.message(packet => seen.push(packet.type))
|
||||
assert.doesNotThrow(() => env.dispatch({ type: 'custom', id: 'toString' }))
|
||||
assert.deepEqual(seen, ['custom'])
|
||||
env.instance.promises[7] = undefined
|
||||
assert.doesNotThrow(() => env.dispatch({ type: 'empty', id: 7 }))
|
||||
assert.deepEqual(seen, ['custom', 'empty'])
|
||||
env.instance.destroy()
|
||||
})
|
||||
|
||||
test('Iframe candidate preserves commit serialization, synchronous response and async resolver behavior', async () => {
|
||||
const parent = setup()
|
||||
const promise = parent.instance.commit(() => {
|
||||
return 7
|
||||
})
|
||||
const packet = parent.sent[0].packet
|
||||
assert.equal(packet.data.trim(), 'return 7')
|
||||
const child = iframeEnvironment(implementation, true)
|
||||
const handling = child.Factory.onMessage({ data: packet })
|
||||
assert.equal(child.sent[0].packet.data, 7)
|
||||
await handling
|
||||
parent.dispatch(child.sent[0].packet)
|
||||
assert.equal(await promise, 7)
|
||||
const async = child.Factory.onMessage({ data: { type: 'commit', data: 'resolve(8)', id: 18 } })
|
||||
assert.equal(child.sent.length, 1)
|
||||
await async
|
||||
assert.equal(child.sent[1].packet.data, 8)
|
||||
parent.instance.destroy()
|
||||
})
|
||||
|
||||
test('Iframe candidate preserves child errors, injection repetition and argument validation', async () => {
|
||||
const parent = setup()
|
||||
assert.throws(() => new parent.Factory({ iframe: {}, url: '/' }), /needs to be a HTMLIFrameElement/)
|
||||
assert.throws(() => parent.instance.commit(null), /needs to be a function/)
|
||||
assert.throws(() => parent.instance.message(null), /needs to be a function/)
|
||||
assert.throws(() => parent.Factory.inject(), /can only be used in iframe/)
|
||||
const child = iframeEnvironment(implementation, true)
|
||||
child.Factory.inject()
|
||||
child.Factory.inject()
|
||||
assert.equal(child.handlers.get('message').size, 1)
|
||||
assert.deepEqual(child.sent.map(item => item.packet.id), [0, 0])
|
||||
await assert.rejects(child.Factory.onMessage({ data: { type: 'commit', data: 'throw new Error("child failure")', id: 19 } }), /child failure/)
|
||||
assert.equal(child.sent.at(-1).packet.type, 'error')
|
||||
assert.equal(child.sent.at(-1).packet.data, 'child failure')
|
||||
parent.instance.destroy()
|
||||
})
|
||||
Reference in new issue
Block a user