fix(iframe): [PKG-IFRAME-03] own pending requests and cancellation

This commit is contained in:
Harvey Zhao committed 2026-09-12 23:41:48 +08:00
1 parent b3f22652c2
commit b8d03e7366
21 files changed
+3826 -107

No files matched your search

@@ -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.
+5
View File
@@ -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
View File
@@ -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 {
@@ -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"]
}