refactor(iframe): [PKG-IFRAME-03] scope requests to active documents

This commit is contained in:
Harvey Zhao committed 2026-09-13 01:07:37 +08:00
1 parent b8cdafedd1
commit 1342de94ef
27 files changed
+10860 -69

No files matched your search

+45 -3
View File
@@ -8,6 +8,11 @@ two lifecycle flags; it does not depend on ArtPlayer or import the entry class.
`src/connection.ts` owns listener acquisition/release and ensures request
cancellation runs even if listener removal throws. `src/protocol.ts` checks the
selected window peer and packet shape before either public receiver processes it.
`src/navigation.ts` owns parent document state, source observation and suspended
request snapshots. `src/child-session.ts` owns the child document marker and its
acknowledged pagehide/pageshow/hashchange notifications. Requests accept an
internal boundary hook rather than importing the connection or navigation modules;
there is no runtime dependency cycle or dependency on a particular core version.
The entry initializes the same seven enumerable, writable fields in the same
order. Internal request ownership lives in a module WeakMap, keeping instance
@@ -44,6 +49,36 @@ 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.
Internal cancellation holds the native rejection separately from public callback
properties. Replacing/deleting a public entry does not make its owned Promise
unreachable during destroy or document departure; ordinary response dispatch
continues to respect the public callbacks.
## Document ownership
New inject packets advertise optional `__artplayerIframe` metadata. A cooperating
parent acknowledges it through tagged `artplayer-tool-iframe:session` traffic;
the public message callback still receives only its original type/data packet.
Ordinary child messages stay unmarked when the parent has not acknowledged the
extension. The document ID correlates work and is not an authentication token.
See [ADR-026](../../refactor/iframe-document-protocol.md) for the complete wire boundary.
Parent source attribute changes begin a provisional navigation: capture the old
pending set and pause new sends. Confirmed pagehide or a different document's
inject cancels the captured set, preserving work queued for the new document.
A hashchange report matching the actual target src completes a same-document
transition without cancellation. This also handles relative/absolute src spelling
and setting an old fragment after the child changed its own hash. Raw attribute
tracking prevents a changed base URL resolution alone from inventing navigation.
Repeated inject for the active document keeps requests; an old inject cannot
reactivate a document while its replacement is pending. Marked replies and
notifications from other documents are discarded; marked old commands are not
executed by a new child. No load-event reset is used, so inject-before-load works.
Connection cleanup disposes the observer and request hook, then cancels all owned
requests. Child listeners belong to the child document, are installed once, and
are rolled back on partial acquisition failure. A destroyed parent ignores late
private notifications. The existing API adds no static child destroy method.
## Compatibility still under review
@@ -68,9 +103,12 @@ sandbox or a parent-origin allowlist. Upgrading one side does not secure the
unchanged historical receiver on the other side. See the independent decision in
[iframe-message-boundary.md](../../refactor/iframe-message-boundary.md).
Navigation/reinjection remains unfinished. A child can inject before iframe load;
blindly resetting state on load would lose a valid handshake. IFRAME-LIFE-01 and
IFRAME-TRUST-01 remain open for complete lifecycle/integration and release review.
Legacy peers keep the ordinary envelope and support observed changes to different
source addresses; their same-address reloads/internal navigation cannot provide
document identity without upgrading the child. No claim is made that a normal
history reload is BFCache: actual persisted restoration, full-player integration,
devices and externally interrupted navigation remain PKG-IFRAME-05/release gates.
IFRAME-LIFE-01 and IFRAME-TRUST-01 remain open for those integration/review scopes.
Public declarations remain in `types/artplayer-tool-iframe.d.ts`; source typing
does not yet establish historical declaration/entry compatibility. PKG-IFRAME-04
@@ -92,6 +130,10 @@ publication is implied by this source migration.
malformed packets, actual HTTP redirects, opaque sandbox and normal new/old
parent-child wire combinations. `ARTPLAYER_IFRAME_BOUNDARIES_ONLY=1` omits the
mixed-version controls when reproducing a candidate boundary failure.
- `yarn test:browser test/browser/iframe-navigation.spec.js`: source/srcdoc, rapid
navigation, self-navigation, load ordering, fragment identity, stale packets,
legacy children and history with actual persisted-state reporting. There is no
implicit request timeout. Handle rejection when a document leaves or is destroyed.
- 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`.
@@ -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?(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;const s=new WeakMap;function o(t){const o=s.get(t);s.delete(t);try{o?.()}finally{!function(t){const s=e.get(t);if(s)for(const e of[...s])e.callbacks.reject(new Error("The instance has been destroyed"))}(t)}}function r(e,t){if(void 0!==e.source&&null!==e.source&&e.source!==t)return!1;const s=e.data;return"object"===typeof s&&null!==s&&"type"in s&&"string"===typeof s.type}class n{static get iframe(){return window.top!==window}static postMessage({type:e,data:t,id:s=0}){if(!n.iframe)throw new Error('The "ArtplayerToolIframe.postMessage" method can only be used in iframe');window.parent.postMessage({type:e,data:t,id:s},"*")}static async onMessage(e){if(!n.iframe)throw new Error('The "ArtplayerToolIframe.onMessage" method can only be used in iframe');if(!r(e,window.parent))return;const{type:t,data:s,id:o}=e.data;if("commit"===t)try{if(s.match(/\bresolve\((.*?)\)/)){const e=`return new Promise(function(resolve){\n${s}\n})`,t=await new Function(e)();n.postMessage({type:"response",data:t,id:o})}else{const e=new Function(s)();n.postMessage({type:"response",data:e,id:o})}}catch(i){throw n.postMessage({type:"error",data:i.message,id:o}),i}}static inject(){if(!n.iframe)throw new Error('The "ArtplayerToolIframe.inject" method can only be used in iframe');n.postMessage({type:"inject"}),window.addEventListener("message",n.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);try{!function(e){const t=window,o=e.onMessage;s.set(e,()=>t.removeEventListener("message",o)),t.addEventListener("message",o),e.$iframe.src=e.url}(this)}catch(r){this.destroyed=!0;try{o(this)}catch{}throw r}}onMessage(e){if(this.destroyed||!r(e,this.$iframe.contentWindow))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((n,i)=>{if(s.destroyed)return void i(new Error("The instance has been destroyed"));const a=e.get(s)||new Set;e.set(s,a);const c=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),d={callbacks:{resove(e){c(d)&&n(e)},reject(e){c(d)&&i(e)}}};a.add(d);const l=()=>{if(d.timer=void 0,a.has(d))if(s.destroyed)d.callbacks.reject(new Error("The instance has been destroyed"));else try{if(s.injected){const e=Math.max(Date.now(),t+1);t=e,d.id=e,s.promises[e]=d.callbacks,s.$iframe.contentWindow.postMessage({type:o,data:r,id:e},"*")}else d.timer=setTimeout(l,200)}catch(e){d.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,o(this)}}return n});
!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="artplayer-tool-iframe:session";function t(e){const t=e.__artplayerIframe;if(1===t?.version&&"string"===typeof t.document&&t.document&&["inject","message","ack","leave","resume","fragment"].includes(t.phase))return t}function n(e,t,n){return{...e,__artplayerIframe:{version:1,document:t,phase:n}}}function r(e,t){if(void 0!==e.source&&null!==e.source&&e.source!==t)return!1;const n=e.data;return"object"===typeof n&&null!==n&&"type"in n&&"string"===typeof n.type}let o;const s=new WeakMap,i=new WeakMap;let a=0;function c(e,t){t?i.set(e,t):i.delete(e)}function d(e,t=new Error("The instance has been destroyed")){const n=s.get(e);if(n)for(const r of[...n])r.cancel(t)}const u=new WeakMap;function f(e){const t=e.$iframe;return t.hasAttribute?.("srcdoc")?`srcdoc:${t.getAttribute("srcdoc")}`:`src:${n=t.src||"",n.split("#")[0]}`;var n}function m(e){const t=e.$iframe;return t.hasAttribute?.("srcdoc")?`srcdoc:${t.getAttribute("srcdoc")}`:`src:${t.getAttribute?.("src")??t.src??""}`}function h(e,t){t.waiting||(t.documentId?t.cancelPrevious=function(e){const t=[...s.get(e)||[]];return e=>{for(const n of t)n.cancel(e)}}(e):d(e,new Error("The iframe document has changed"))),t.waiting=!0,e.injected=!1}function l(e,t,n){const r=m(e);if(!n.length&&r===t.rawSource)return;const o=function(e,t,n){return f(e)!==t.source||!!t.documentId&&(m(e)!==t.rawSource||n.some(t=>"srcdoc"===t.attributeName||"src"===t.attributeName&&!e.$iframe.hasAttribute?.("srcdoc")))}(e,t,n);t.rawSource=r,t.source=f(e),o&&h(e,t)}function w(e){const t=u.get(e);if(!t||e.destroyed)return;const n=t.observer?.takeRecords()||[];l(e,t,n)}function p(t,r,o){r.hasHandshake&&r.documentId!==o&&(r.cancelPrevious?r.cancelPrevious(new Error("The iframe document has changed")):r.waiting||d(t,new Error("The iframe document has changed"))),r.cancelPrevious=void 0,r.documentId=o,r.hasHandshake=!0,r.waiting=!1,t.injected=!0;try{t.$iframe.contentWindow?.postMessage(n({type:e,data:void 0,id:0},o,"ack"),"*")}catch{}}function g(n,r){const o=u.get(n);if(!o)return!1;w(n);const s=t(r);if(s&&r.type===e)return"resume"===s.phase?p(n,o,s.document):s.document===o.documentId&&("leave"===s.phase?function(e,t){t.cancelPrevious?t.cancelPrevious(new Error("The iframe document has changed")):t.waiting||d(e,new Error("The iframe document has changed")),t.cancelPrevious=void 0,t.waiting=!0,e.injected=!1}(n,o):"fragment"===s.phase&&o.cancelPrevious&&!n.$iframe.hasAttribute?.("srcdoc")&&r.data===n.$iframe.src&&p(n,o,s.document)),!0;if("inject"===r.type){if("inject"===s?.phase){if(o.waiting&&s.document===o.documentId)return!0;p(n,o,s.document)}else o.waiting&&(o.cancelPrevious?.(new Error("The iframe document has changed")),o.cancelPrevious=void 0,o.documentId=void 0),o.hasHandshake=!0,o.waiting=!1;return!1}return Boolean(s&&(o.waiting&&!o.cancelPrevious||s.document!==o.documentId))}const y=new WeakMap;function v(e){const t=window,r=e.onMessage,o=[()=>t.removeEventListener("message",r)];y.set(e,()=>{let e,t=!1;for(const r of o)try{r()}catch(n){t||(t=!0,e=n)}if(t)throw e}),t.addEventListener("message",r),e.$iframe.src=e.url;const s=function(e){const t={source:f(e),rawSource:m(e),hasHandshake:!1,waiting:!1};return u.set(e,t),c(e,{prepare:()=>w(e),envelope:e=>t.documentId?n(e,t.documentId,"message"):e}),{start(){"function"===typeof window.MutationObserver&&(t.observer=new window.MutationObserver(n=>{e.destroyed||l(e,t,n)}),t.observer.observe(e.$iframe,{attributes:!0,attributeOldValue:!0,attributeFilter:["src","srcdoc"]}))},dispose(){u.delete(e),c(e,void 0),t.cancelPrevious=void 0,t.observer?.disconnect()}}}(e);o.push(s.dispose),s.start()}function b(e){const t=y.get(e);y.delete(e);try{t?.()}finally{d(e)}}class E{static get iframe(){return window.top!==window}static postMessage({type:e,data:t,id:r=0}){if(!E.iframe)throw new Error('The "ArtplayerToolIframe.postMessage" method can only be used in iframe');var s;window.parent.postMessage((s={type:e,data:t,id:r},o&&("inject"===s.type||o.acknowledged)?n(s,o.id,"inject"===s.type?"inject":"message"):s),"*")}static async onMessage(n){if(!E.iframe)throw new Error('The "ArtplayerToolIframe.onMessage" method can only be used in iframe');if(!r(n,window.parent)||function(n){const r=t(n);return!!r&&(n.type===e?(o&&r.document===o.id&&"ack"===r.phase&&(o.acknowledged=!0),!0):!o||r.document!==o.id)}(n.data))return;const{type:s,data:i,id:a}=n.data;if("commit"===s)try{if(i.match(/\bresolve\((.*?)\)/)){const e=`return new Promise(function(resolve){\n${i}\n})`,t=await new Function(e)();E.postMessage({type:"response",data:t,id:a})}else{const e=new Function(i)();E.postMessage({type:"response",data:e,id:a})}}catch(c){throw E.postMessage({type:"error",data:c.message,Line truncated
@@ -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?(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;const s=new WeakMap;function o(t){const o=s.get(t);s.delete(t);try{null==o||o()}finally{!function(t){const s=e.get(t);if(s)for(const e of[...s])e.callbacks.reject(new Error("The instance has been destroyed"))}(t)}}function r(e,t){if(void 0!==e.source&&null!==e.source&&e.source!==t)return!1;const s=e.data;return"object"===typeof s&&null!==s&&"type"in s&&"string"===typeof s.type}class n{static get iframe(){return window.top!==window}static postMessage({type:e,data:t,id:s=0}){if(!n.iframe)throw new Error('The "ArtplayerToolIframe.postMessage" method can only be used in iframe');window.parent.postMessage({type:e,data:t,id:s},"*")}static onMessage(e){return t=this,s=null,o=function*(){if(!n.iframe)throw new Error('The "ArtplayerToolIframe.onMessage" method can only be used in iframe');if(!r(e,window.parent))return;const{type:t,data:s,id:o}=e.data;if("commit"===t)try{if(s.match(/\bresolve\((.*?)\)/)){const e=`return new Promise(function(resolve){\n${s}\n})`,t=yield new Function(e)();n.postMessage({type:"response",data:t,id:o})}else{const e=new Function(s)();n.postMessage({type:"response",data:e,id:o})}}catch(i){throw n.postMessage({type:"error",data:i.message,id:o}),i}},new Promise((e,r)=>{var n=e=>{try{a(o.next(e))}catch(t){r(t)}},i=e=>{try{a(o.throw(e))}catch(t){r(t)}},a=t=>t.done?e(t.value):Promise.resolve(t.value).then(n,i);a((o=o.apply(t,s)).next())});var t,s,o}static inject(){if(!n.iframe)throw new Error('The "ArtplayerToolIframe.inject" method can only be used in iframe');n.postMessage({type:"inject"}),window.addEventListener("message",n.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);try{!function(e){const t=window,o=e.onMessage;s.set(e,()=>t.removeEventListener("message",o)),t.addEventListener("message",o),e.$iframe.src=e.url}(this)}catch(r){this.destroyed=!0;try{o(this)}catch(n){}throw r}}onMessage(e){if(this.destroyed||!r(e,this.$iframe.contentWindow))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((n,i)=>{if(s.destroyed)return void i(new Error("The instance has been destroyed"));const a=e.get(s)||new Set;e.set(s,a);const c=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),d={callbacks:{resove(e){c(d)&&n(e)},reject(e){c(d)&&i(e)}}};a.add(d);const l=()=>{if(d.timer=void 0,a.has(d))if(s.destroyed)d.callbacks.reject(new Error("The instance has been destroyed"));else try{if(s.injected){const e=Math.max(Date.now(),t+1);t=e,d.id=e,s.promises[e]=d.callbacks,s.$iframe.contentWindow.postMessage({type:o,data:r,id:e},"*")}else d.timer=setTimeout(l,200)}catch(e){d.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,o(this)}}return n});
!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";var e=Object.defineProperty,t=Object.defineProperties,n=Object.getOwnPropertyDescriptors,r=Object.getOwnPropertySymbols,o=Object.prototype.hasOwnProperty,s=Object.prototype.propertyIsEnumerable,i=(t,n,r)=>n in t?e(t,n,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[n]=r;const a="artplayer-tool-iframe:session";function c(e){const t=e.__artplayerIframe;if(1===(null==t?void 0:t.version)&&"string"===typeof t.document&&t.document&&["inject","message","ack","leave","resume","fragment"].includes(t.phase))return t}function d(e,a,c){return d=((e,t)=>{for(var n in t||(t={}))o.call(t,n)&&i(e,n,t[n]);if(r)for(var n of r(t))s.call(t,n)&&i(e,n,t[n]);return e})({},e),t(d,n({__artplayerIframe:{version:1,document:a,phase:c}}));var d}function u(e,t){if(void 0!==e.source&&null!==e.source&&e.source!==t)return!1;const n=e.data;return"object"===typeof n&&null!==n&&"type"in n&&"string"===typeof n.type}let l;const f=new WeakMap,m=new WeakMap;let h=0;function p(e,t){t?m.set(e,t):m.delete(e)}function w(e,t=new Error("The instance has been destroyed")){const n=f.get(e);if(n)for(const r of[...n])r.cancel(t)}const v=new WeakMap;function y(e){var t;const n=e.$iframe;return(null==(t=n.hasAttribute)?void 0:t.call(n,"srcdoc"))?`srcdoc:${n.getAttribute("srcdoc")}`:`src:${r=n.src||"",r.split("#")[0]}`;var r}function g(e){var t,n,r,o;const s=e.$iframe;return(null==(t=s.hasAttribute)?void 0:t.call(s,"srcdoc"))?`srcdoc:${s.getAttribute("srcdoc")}`:`src:${null!=(o=null!=(r=null==(n=s.getAttribute)?void 0:n.call(s,"src"))?r:s.src)?o:""}`}function b(e,t){t.waiting||(t.documentId?t.cancelPrevious=function(e){const t=[...f.get(e)||[]];return e=>{for(const n of t)n.cancel(e)}}(e):w(e,new Error("The iframe document has changed"))),t.waiting=!0,e.injected=!1}function E(e,t,n){const r=g(e);if(!n.length&&r===t.rawSource)return;const o=function(e,t,n){return y(e)!==t.source||!!t.documentId&&(g(e)!==t.rawSource||n.some(t=>{var n,r;return"srcdoc"===t.attributeName||"src"===t.attributeName&&!(null==(r=(n=e.$iframe).hasAttribute)?void 0:r.call(n,"srcdoc"))}))}(e,t,n);t.rawSource=r,t.source=y(e),o&&b(e,t)}function M(e){var t;const n=v.get(e);if(!n||e.destroyed)return;E(e,n,(null==(t=n.observer)?void 0:t.takeRecords())||[])}function j(e,t,n){var r;t.hasHandshake&&t.documentId!==n&&(t.cancelPrevious?t.cancelPrevious(new Error("The iframe document has changed")):t.waiting||w(e,new Error("The iframe document has changed"))),t.cancelPrevious=void 0,t.documentId=n,t.hasHandshake=!0,t.waiting=!1,e.injected=!0;try{null==(r=e.$iframe.contentWindow)||r.postMessage(d({type:a,data:void 0,id:0},n,"ack"),"*")}catch(o){}}function T(e,t){var n,r,o;const s=v.get(e);if(!s)return!1;M(e);const i=c(t);if(i&&t.type===a)return"resume"===i.phase?j(e,s,i.document):i.document===s.documentId&&("leave"===i.phase?function(e,t){t.cancelPrevious?t.cancelPrevious(new Error("The iframe document has changed")):t.waiting||w(e,new Error("The iframe document has changed")),t.cancelPrevious=void 0,t.waiting=!0,e.injected=!1}(e,s):"fragment"===i.phase&&s.cancelPrevious&&!(null==(r=(n=e.$iframe).hasAttribute)?void 0:r.call(n,"srcdoc"))&&t.data===e.$iframe.src&&j(e,s,i.document)),!0;if("inject"===t.type){if("inject"===(null==i?void 0:i.phase)){if(s.waiting&&i.document===s.documentId)return!0;j(e,s,i.document)}else s.waiting&&(null==(o=s.cancelPrevious)||o.call(s,new Error("The iframe document has changed")),s.cancelPrevious=void 0,s.documentId=void 0),s.hasHandshake=!0,s.waiting=!1;return!1}return Boolean(i&&(s.waiting&&!s.cancelPrevious||i.document!==s.documentId))}const k=new WeakMap;function P(e){const t=window,n=e.onMessage,r=[()=>t.removeEventListener("message",n)];k.set(e,()=>{let e,t=!1;for(const o of r)try{o()}catch(n){t||(t=!0,e=n)}if(t)throw e}),t.addEventListener("message",n),e.$iframe.src=e.url;const o=function(e){const t={source:y(e),rawSource:g(e),hasHandshake:!1,waiting:!1};return v.set(e,t),p(e,{prepare:()=>M(e),envelope:e=>t.documentId?d(e,t.documentId,"message"):e}),{start(){"function"===typeof window.MutationObserver&&(t.observer=new window.MutationObserver(n=>{e.destroyed||E(e,t,n)}),t.observer.observe(e.$iframe,{attributes:!0,attributeOldValue:!0,attributeFilter:["src","srcdoc"]}))},dispose(){var n;v.delete(e),p(e,void 0),t.cancelPrevious=void 0,null==(n=t.observer)||n.disconnect()}}}(e);r.push(o.dispose),o.start()}function I(e){const t=k.get(e);k.delete(e);try{null==t||t()}finally{w(e)}}class ${static get iframe(){return window.top!==window}static postMessage({type:e,data:t,id:n=0}){if(!$.iframe)throw new Error('The "ArtplayerToolIframe.postMessage" method can only be used in iframe');var r;window.parent.postMessage((r={type:e,data:t,id:n},l&&("inject"===r.type||lLine truncated
+282 -17
View File
@@ -4,8 +4,104 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
const sessionType = "artplayer-tool-iframe:session";
function sessionMetadata(packet) {
const value = packet.__artplayerIframe;
if (value?.version === 1 && typeof value.document === "string" && value.document && ["inject", "message", "ack", "leave", "resume", "fragment"].includes(value.phase))
return value;
}
function withSession(packet, documentId, phase) {
return { ...packet, __artplayerIframe: { version: 1, document: documentId, phase } };
}
function acceptsMessage(event, peer) {
if (event.source !== void 0 && event.source !== null && event.source !== peer)
return false;
const data = event.data;
return typeof data === "object" && data !== null && "type" in data && typeof data.type === "string";
}
let session;
function prepareChildSession() {
if (session?.owner === window.document && session)
return;
const random = new Uint32Array(4);
if (window.crypto?.getRandomValues)
window.crypto.getRandomValues(random);
else
random.forEach((_, index) => random[index] = Math.floor(Math.random() * 4294967296));
session = { owner: window.document, id: `${Date.now()}-${Array.from(random).join("-")}`, acknowledged: false, lifecycle: false, receivers: /* @__PURE__ */ new Set() };
}
function childEnvelope(packet) {
if (session && (packet.type === "inject" || session.acknowledged))
return withSession(packet, session.id, packet.type === "inject" ? "inject" : "message");
return packet;
}
function consumeChildSession(packet) {
const metadata = sessionMetadata(packet);
if (!metadata)
return false;
if (packet.type === sessionType) {
if (session && metadata.document === session.id && metadata.phase === "ack")
session.acknowledged = true;
return true;
}
return !session || metadata.document !== session.id;
}
function connectChildSession(receiver) {
const current = session;
const added = !current.receivers.has(receiver);
const installing = !current.lifecycle;
const notify = (phase, data) => {
if (!current.acknowledged)
return;
try {
window.parent.postMessage(withSession({ type: sessionType, data, id: 0 }, current.id, phase), "*");
} catch {
}
};
const leave = () => notify("leave");
const fragment = () => notify("fragment", window.location.href);
const resume = (event) => {
if (event.persisted)
notify("resume");
};
try {
window.addEventListener("message", receiver);
current.receivers.add(receiver);
if (!installing)
return;
window.addEventListener("pagehide", leave);
window.addEventListener("pageshow", resume);
window.addEventListener("hashchange", fragment);
current.lifecycle = true;
} catch (error) {
const cleanups = [];
if (installing) {
cleanups.push(() => window.removeEventListener("pagehide", leave));
cleanups.push(() => window.removeEventListener("pageshow", resume));
cleanups.push(() => window.removeEventListener("hashchange", fragment));
}
if (added)
cleanups.push(() => window.removeEventListener("message", receiver));
for (const cleanup of cleanups) {
try {
cleanup();
} catch {
}
}
if (added)
current.receivers.delete(receiver);
throw error;
}
}
const requests = /* @__PURE__ */ new WeakMap();
const boundaries = /* @__PURE__ */ new WeakMap();
let lastId = 0;
function setRequestBoundary(host, boundary) {
if (boundary)
boundaries.set(host, boundary);
else
boundaries.delete(host);
}
function postRequest(host, { type, data }) {
return new Promise((resolve, reject) => {
if (host.destroyed) {
@@ -35,6 +131,10 @@ function postRequest(host, { type, data }) {
if (finish(request))
reject(error);
}
},
cancel(error) {
if (finish(request))
reject(error);
}
};
pending.add(request);
@@ -43,40 +143,209 @@ function postRequest(host, { type, data }) {
if (!pending.has(request))
return;
if (host.destroyed) {
request.callbacks.reject(new Error("The instance has been destroyed"));
request.cancel(new Error("The instance has been destroyed"));
return;
}
try {
boundaries.get(host)?.prepare();
if (!pending.has(request))
return;
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 }, "*");
const packet = { type, data, id };
host.$iframe.contentWindow.postMessage(boundaries.get(host)?.envelope(packet) || packet, "*");
} else {
request.timer = setTimeout(loop, 200);
}
} catch (error) {
request.callbacks.reject(error);
request.cancel(error);
}
};
loop();
});
}
function cancelRequests(host) {
function cancelRequests(host, reason = new Error("The instance has been destroyed")) {
const pending = requests.get(host);
if (!pending)
return;
for (const request of [...pending])
request.callbacks.reject(new Error("The instance has been destroyed"));
request.cancel(reason);
}
function captureRequests(host) {
const captured = [...requests.get(host) || []];
return (reason) => {
for (const request of captured)
request.cancel(reason);
};
}
const navigations = /* @__PURE__ */ new WeakMap();
const withoutHash = (value) => value.split("#")[0];
function sourceOf(host) {
const frame = host.$iframe;
return frame.hasAttribute?.("srcdoc") ? `srcdoc:${frame.getAttribute("srcdoc")}` : `src:${withoutHash(frame.src || "")}`;
}
function rawSourceOf(host) {
const frame = host.$iframe;
return frame.hasAttribute?.("srcdoc") ? `srcdoc:${frame.getAttribute("srcdoc")}` : `src:${frame.getAttribute?.("src") ?? frame.src ?? ""}`;
}
function invalidate(host, state) {
if (!state.waiting) {
if (state.documentId)
state.cancelPrevious = captureRequests(host);
else
cancelRequests(host, new Error("The iframe document has changed"));
}
state.waiting = true;
host.injected = false;
}
function leaveDocument(host, state) {
if (state.cancelPrevious)
state.cancelPrevious(new Error("The iframe document has changed"));
else if (!state.waiting)
cancelRequests(host, new Error("The iframe document has changed"));
state.cancelPrevious = void 0;
state.waiting = true;
host.injected = false;
}
function sourceChanged(host, state, records) {
if (sourceOf(host) !== state.source)
return true;
if (!state.documentId)
return false;
if (rawSourceOf(host) !== state.rawSource)
return true;
return records.some((record) => {
if (record.attributeName === "srcdoc")
return true;
if (record.attributeName !== "src" || host.$iframe.hasAttribute?.("srcdoc"))
return false;
return true;
});
}
function observeSource(host, state, records) {
const rawSource = rawSourceOf(host);
if (!records.length && rawSource === state.rawSource)
return;
const changed = sourceChanged(host, state, records);
state.rawSource = rawSource;
state.source = sourceOf(host);
if (changed)
invalidate(host, state);
}
function prepareNavigation(host) {
const state = navigations.get(host);
if (!state || host.destroyed)
return;
const records = state.observer?.takeRecords() || [];
observeSource(host, state, records);
}
function createNavigation(host) {
const state = { source: sourceOf(host), rawSource: rawSourceOf(host), hasHandshake: false, waiting: false };
navigations.set(host, state);
setRequestBoundary(host, {
prepare: () => prepareNavigation(host),
envelope: (packet) => state.documentId ? withSession(packet, state.documentId, "message") : packet
});
return {
start() {
if (typeof window.MutationObserver !== "function")
return;
state.observer = new window.MutationObserver((records) => {
if (host.destroyed)
return;
observeSource(host, state, records);
});
state.observer.observe(host.$iframe, { attributes: true, attributeOldValue: true, attributeFilter: ["src", "srcdoc"] });
},
dispose() {
navigations.delete(host);
setRequestBoundary(host, void 0);
state.cancelPrevious = void 0;
state.observer?.disconnect();
}
};
}
function activate(host, state, documentId) {
if (state.hasHandshake && state.documentId !== documentId) {
if (state.cancelPrevious)
state.cancelPrevious(new Error("The iframe document has changed"));
else if (!state.waiting)
cancelRequests(host, new Error("The iframe document has changed"));
}
state.cancelPrevious = void 0;
state.documentId = documentId;
state.hasHandshake = true;
state.waiting = false;
host.injected = true;
try {
host.$iframe.contentWindow?.postMessage(withSession({ type: sessionType, data: void 0, id: 0 }, documentId, "ack"), "*");
} catch {
}
}
function consumeNavigation(host, packet) {
const state = navigations.get(host);
if (!state)
return false;
prepareNavigation(host);
const metadata = sessionMetadata(packet);
if (metadata && packet.type === sessionType) {
if (metadata.phase === "resume") {
activate(host, state, metadata.document);
} else if (metadata.document === state.documentId) {
if (metadata.phase === "leave")
leaveDocument(host, state);
else if (metadata.phase === "fragment" && state.cancelPrevious && !host.$iframe.hasAttribute?.("srcdoc") && packet.data === host.$iframe.src)
activate(host, state, metadata.document);
}
return true;
}
if (packet.type === "inject") {
if (metadata?.phase === "inject") {
if (state.waiting && metadata.document === state.documentId)
return true;
activate(host, state, metadata.document);
} else {
if (state.waiting) {
state.cancelPrevious?.(new Error("The iframe document has changed"));
state.cancelPrevious = void 0;
state.documentId = void 0;
}
state.hasHandshake = true;
state.waiting = false;
}
return false;
}
return Boolean(metadata && (state.waiting && !state.cancelPrevious || metadata.document !== state.documentId));
}
const connections = /* @__PURE__ */ new WeakMap();
function connect(host) {
const owner = window;
const receiver = host.onMessage;
connections.set(host, () => owner.removeEventListener("message", receiver));
const cleanups = [() => owner.removeEventListener("message", receiver)];
connections.set(host, () => {
let failed = false;
let failure;
for (const cleanup of cleanups) {
try {
cleanup();
} catch (error) {
if (!failed) {
failed = true;
failure = error;
}
}
}
if (failed)
throw failure;
});
owner.addEventListener("message", receiver);
host.$iframe.src = host.url;
const navigation = createNavigation(host);
cleanups.push(navigation.dispose);
navigation.start();
}
function releaseConnection(host) {
const disconnect = connections.get(host);
@@ -87,12 +356,6 @@ function releaseConnection(host) {
cancelRequests(host);
}
}
function acceptsMessage(event, peer) {
if (event.source !== void 0 && event.source !== null && event.source !== peer)
return false;
const data = event.data;
return typeof data === "object" && data !== null && "type" in data && typeof data.type === "string";
}
class ArtplayerToolIframe {
static get iframe() {
return window.top !== window;
@@ -102,11 +365,11 @@ class ArtplayerToolIframe {
throw new Error('The "ArtplayerToolIframe.postMessage" method can only be used in iframe');
}
window.parent.postMessage(
{
childEnvelope({
type,
data,
id
},
}),
"*"
);
}
@@ -114,7 +377,7 @@ class ArtplayerToolIframe {
if (!ArtplayerToolIframe.iframe) {
throw new Error('The "ArtplayerToolIframe.onMessage" method can only be used in iframe');
}
if (!acceptsMessage(event, window.parent))
if (!acceptsMessage(event, window.parent) || consumeChildSession(event.data))
return;
const { type, data, id } = event.data;
switch (type) {
@@ -141,8 +404,9 @@ ${data}
if (!ArtplayerToolIframe.iframe) {
throw new Error('The "ArtplayerToolIframe.inject" method can only be used in iframe');
}
prepareChildSession();
ArtplayerToolIframe.postMessage({ type: "inject" });
window.addEventListener("message", ArtplayerToolIframe.onMessage);
connectChildSession(ArtplayerToolIframe.onMessage);
}
constructor({ iframe, url }) {
if (iframe instanceof HTMLIFrameElement === false) {
@@ -170,7 +434,7 @@ ${data}
}
}
onMessage(event) {
if (this.destroyed || !acceptsMessage(event, this.$iframe.contentWindow))
if (this.destroyed || !acceptsMessage(event, this.$iframe.contentWindow) || consumeNavigation(this, event.data))
return;
const { type, data, id } = event.data;
switch (type) {
@@ -191,6 +455,7 @@ ${data}
}
}
postMessage(message) {
prepareNavigation(this);
return postRequest(this, message);
}
commit(callback) {
@@ -0,0 +1,92 @@
import type { Packet } from './requests'
import { sessionMetadata, sessionType, withSession } from './protocol'
interface ChildSession {
owner: Document
id: string
acknowledged: boolean
lifecycle: boolean
receivers: Set<(event: MessageEvent<Packet>) => void>
}
let session: ChildSession | undefined
export function prepareChildSession(): void {
if (session?.owner === window.document && session)
return
const random = new Uint32Array(4)
if (window.crypto?.getRandomValues)
window.crypto.getRandomValues(random)
else
random.forEach((_, index) => random[index] = Math.floor(Math.random() * 0x100000000))
session = { owner: window.document, id: `${Date.now()}-${Array.from(random).join('-')}`, acknowledged: false, lifecycle: false, receivers: new Set() }
}
export function childEnvelope(packet: Packet): Packet {
if (session && (packet.type === 'inject' || session.acknowledged))
return withSession(packet, session.id, packet.type === 'inject' ? 'inject' : 'message')
return packet
}
export function consumeChildSession(packet: Packet): boolean {
const metadata = sessionMetadata(packet)
if (!metadata)
return false
if (packet.type === sessionType) {
if (session && metadata.document === session.id && metadata.phase === 'ack')
session.acknowledged = true
return true
}
return !session || metadata.document !== session.id
}
export function connectChildSession(receiver: (event: MessageEvent<Packet>) => void): void {
const current = session!
const added = !current.receivers.has(receiver)
const installing = !current.lifecycle
const notify = (phase: 'leave' | 'resume' | 'fragment', data?: unknown) => {
if (!current.acknowledged)
return
try {
window.parent.postMessage(withSession({ type: sessionType, data, id: 0 }, current.id, phase), '*')
}
catch {
// The parent may have disappeared; this private lifecycle notification is best effort.
}
}
const leave = () => notify('leave')
const fragment = () => notify('fragment', window.location.href)
const resume = (event: PageTransitionEvent) => {
if (event.persisted)
notify('resume')
}
try {
window.addEventListener('message', receiver)
current.receivers.add(receiver)
if (!installing)
return
window.addEventListener('pagehide', leave)
window.addEventListener('pageshow', resume)
window.addEventListener('hashchange', fragment)
current.lifecycle = true
}
catch (error) {
const cleanups: (() => void)[] = []
if (installing) {
cleanups.push(() => window.removeEventListener('pagehide', leave))
cleanups.push(() => window.removeEventListener('pageshow', resume))
cleanups.push(() => window.removeEventListener('hashchange', fragment))
}
if (added)
cleanups.push(() => window.removeEventListener('message', receiver))
for (const cleanup of cleanups) {
try {
cleanup()
}
catch { /* Preserve the acquisition error while releasing the other listeners. */ }
}
if (added)
current.receivers.delete(receiver)
throw error
}
}
@@ -1,4 +1,5 @@
import type { Packet, RequestHost } from './requests'
import { createNavigation } from './navigation'
import { cancelRequests } from './requests'
interface ConnectionHost extends RequestHost {
@@ -11,9 +12,29 @@ const connections = new WeakMap<ConnectionHost, () => void>()
export function connect(host: ConnectionHost): void {
const owner = window
const receiver = host.onMessage
connections.set(host, () => owner.removeEventListener('message', receiver))
const cleanups = [() => owner.removeEventListener('message', receiver)]
connections.set(host, () => {
let failed = false
let failure: unknown
for (const cleanup of cleanups) {
try {
cleanup()
}
catch (error) {
if (!failed) {
failed = true
failure = error
}
}
}
if (failed)
throw failure
})
owner.addEventListener('message', receiver)
host.$iframe.src = host.url
const navigation = createNavigation(host)
cleanups.push(navigation.dispose)
navigation.start()
}
export function releaseConnection(host: ConnectionHost): void {
+9 -5
View File
@@ -1,5 +1,7 @@
import type { Callbacks, Packet } from './requests'
import { childEnvelope, connectChildSession, consumeChildSession, prepareChildSession } from './child-session'
import { connect, releaseConnection } from './connection'
import { consumeNavigation, prepareNavigation } from './navigation'
import { acceptsMessage } from './protocol'
import { postRequest } from './requests'
@@ -21,11 +23,11 @@ export default class ArtplayerToolIframe {
}
window.parent.postMessage(
{
childEnvelope({
type,
data,
id,
},
}),
'*',
)
}
@@ -35,7 +37,7 @@ export default class ArtplayerToolIframe {
throw new Error('The "ArtplayerToolIframe.onMessage" method can only be used in iframe')
}
if (!acceptsMessage(event, window.parent))
if (!acceptsMessage(event, window.parent) || consumeChildSession(event.data))
return
const { type, data, id } = event.data
@@ -69,8 +71,9 @@ export default class ArtplayerToolIframe {
throw new Error('The "ArtplayerToolIframe.inject" method can only be used in iframe')
}
prepareChildSession()
ArtplayerToolIframe.postMessage({ type: 'inject' })
window.addEventListener('message', ArtplayerToolIframe.onMessage)
connectChildSession(ArtplayerToolIframe.onMessage)
}
constructor({ iframe, url }: { iframe: HTMLIFrameElement, url: string }) {
@@ -105,7 +108,7 @@ export default class ArtplayerToolIframe {
}
onMessage(event: MessageEvent<Packet>) {
if (this.destroyed || !acceptsMessage(event, this.$iframe.contentWindow))
if (this.destroyed || !acceptsMessage(event, this.$iframe.contentWindow) || consumeNavigation(this, event.data))
return
const { type, data, id } = event.data
@@ -134,6 +137,7 @@ export default class ArtplayerToolIframe {
}
postMessage(message: Packet): Promise<any> {
prepareNavigation(this)
return postRequest(this, message)
}
@@ -0,0 +1,166 @@
import type { Packet, RequestHost } from './requests'
import { sessionMetadata, sessionType, withSession } from './protocol'
import { cancelRequests, captureRequests, setRequestBoundary } from './requests'
interface Navigation {
source: string
rawSource: string
documentId?: string
hasHandshake: boolean
waiting: boolean
observer?: MutationObserver
cancelPrevious?: (reason: unknown) => void
}
const navigations = new WeakMap<RequestHost, Navigation>()
const withoutHash = (value: string) => value.split('#')[0]!
function sourceOf(host: RequestHost): string {
const frame = host.$iframe
return frame.hasAttribute?.('srcdoc') ? `srcdoc:${frame.getAttribute('srcdoc')}` : `src:${withoutHash(frame.src || '')}`
}
function rawSourceOf(host: RequestHost): string {
const frame = host.$iframe
return frame.hasAttribute?.('srcdoc') ? `srcdoc:${frame.getAttribute('srcdoc')}` : `src:${frame.getAttribute?.('src') ?? frame.src ?? ''}`
}
function invalidate(host: RequestHost, state: Navigation): void {
if (!state.waiting) {
if (state.documentId)
state.cancelPrevious = captureRequests(host)
else
cancelRequests(host, new Error('The iframe document has changed'))
}
state.waiting = true
host.injected = false
}
function leaveDocument(host: RequestHost, state: Navigation): void {
if (state.cancelPrevious)
state.cancelPrevious(new Error('The iframe document has changed'))
else if (!state.waiting)
cancelRequests(host, new Error('The iframe document has changed'))
state.cancelPrevious = undefined
state.waiting = true
host.injected = false
}
function sourceChanged(host: RequestHost, state: Navigation, records: MutationRecord[]): boolean {
if (sourceOf(host) !== state.source)
return true
if (!state.documentId)
return false
if (rawSourceOf(host) !== state.rawSource)
return true
return records.some((record) => {
if (record.attributeName === 'srcdoc')
return true
if (record.attributeName !== 'src' || host.$iframe.hasAttribute?.('srcdoc'))
return false
return true
})
}
function observeSource(host: RequestHost, state: Navigation, records: MutationRecord[]): void {
const rawSource = rawSourceOf(host)
if (!records.length && rawSource === state.rawSource)
return
const changed = sourceChanged(host, state, records)
state.rawSource = rawSource
state.source = sourceOf(host)
if (changed)
invalidate(host, state)
}
export function prepareNavigation(host: RequestHost): void {
const state = navigations.get(host)
if (!state || host.destroyed)
return
const records = state.observer?.takeRecords() || []
observeSource(host, state, records)
}
export function createNavigation(host: RequestHost): { start: () => void, dispose: () => void } {
const state: Navigation = { source: sourceOf(host), rawSource: rawSourceOf(host), hasHandshake: false, waiting: false }
navigations.set(host, state)
setRequestBoundary(host, {
prepare: () => prepareNavigation(host),
envelope: packet => state.documentId ? withSession(packet, state.documentId, 'message') : packet,
})
return {
start() {
if (typeof window.MutationObserver !== 'function')
return
state.observer = new window.MutationObserver((records) => {
if (host.destroyed)
return
observeSource(host, state, records)
})
state.observer.observe(host.$iframe, { attributes: true, attributeOldValue: true, attributeFilter: ['src', 'srcdoc'] })
},
dispose() {
navigations.delete(host)
setRequestBoundary(host, undefined)
state.cancelPrevious = undefined
state.observer?.disconnect()
},
}
}
function activate(host: RequestHost, state: Navigation, documentId: string): void {
if (state.hasHandshake && state.documentId !== documentId) {
if (state.cancelPrevious)
state.cancelPrevious(new Error('The iframe document has changed'))
else if (!state.waiting)
cancelRequests(host, new Error('The iframe document has changed'))
}
state.cancelPrevious = undefined
state.documentId = documentId
state.hasHandshake = true
state.waiting = false
host.injected = true
try {
host.$iframe.contentWindow?.postMessage(withSession({ type: sessionType, data: undefined, id: 0 }, documentId, 'ack'), '*')
}
catch {
// A failed private acknowledgement does not replace the public inject callback.
}
}
export function consumeNavigation(host: RequestHost, packet: Packet): boolean {
const state = navigations.get(host)
if (!state)
return false
prepareNavigation(host)
const metadata = sessionMetadata(packet)
if (metadata && packet.type === sessionType) {
if (metadata.phase === 'resume') {
activate(host, state, metadata.document)
}
else if (metadata.document === state.documentId) {
if (metadata.phase === 'leave')
leaveDocument(host, state)
else if (metadata.phase === 'fragment' && state.cancelPrevious && !host.$iframe.hasAttribute?.('srcdoc') && packet.data === host.$iframe.src)
activate(host, state, metadata.document)
}
return true
}
if (packet.type === 'inject') {
if (metadata?.phase === 'inject') {
if (state.waiting && metadata.document === state.documentId)
return true
activate(host, state, metadata.document)
}
else {
if (state.waiting) {
state.cancelPrevious?.(new Error('The iframe document has changed'))
state.cancelPrevious = undefined
state.documentId = undefined
}
state.hasHandshake = true
state.waiting = false
}
return false
}
return Boolean(metadata && ((state.waiting && !state.cancelPrevious) || metadata.document !== state.documentId))
}
@@ -1,5 +1,27 @@
import type { Packet } from './requests'
export const sessionType = 'artplayer-tool-iframe:session'
export type SessionPhase = 'inject' | 'message' | 'ack' | 'leave' | 'resume' | 'fragment'
export interface SessionMetadata {
version: 1
document: string
phase: SessionPhase
}
interface SessionPacket extends Packet {
__artplayerIframe: SessionMetadata
}
export function sessionMetadata(packet: Packet): SessionMetadata | undefined {
const value = (packet as Partial<SessionPacket>).__artplayerIframe
if (value?.version === 1 && typeof value.document === 'string' && value.document && ['inject', 'message', 'ack', 'leave', 'resume', 'fragment'].includes(value.phase))
return value
}
export function withSession(packet: Packet, documentId: string, phase: SessionPhase): SessionPacket {
return { ...packet, __artplayerIframe: { version: 1, document: documentId, phase } }
}
export function acceptsMessage(event: MessageEvent<unknown>, peer: Window | null): event is MessageEvent<Packet> {
if (event.source !== undefined && event.source !== null && event.source !== peer)
return false
+34 -5
View File
@@ -20,11 +20,24 @@ interface Pending {
id?: number
timer?: ReturnType<typeof setTimeout>
callbacks: Callbacks
cancel: (reason: unknown) => void
}
const requests = new WeakMap<RequestHost, Set<Pending>>()
interface RequestBoundary {
prepare: () => void
envelope: (packet: Packet) => Packet
}
const boundaries = new WeakMap<RequestHost, RequestBoundary>()
let lastId = 0
export function setRequestBoundary(host: RequestHost, boundary: RequestBoundary | undefined): void {
if (boundary)
boundaries.set(host, boundary)
else
boundaries.delete(host)
}
export function postRequest(host: RequestHost, { type, data }: Packet): Promise<any> {
return new Promise((resolve, reject) => {
if (host.destroyed) {
@@ -55,6 +68,10 @@ export function postRequest(host: RequestHost, { type, data }: Packet): Promise<
reject(error)
},
},
cancel(error) {
if (finish(request))
reject(error)
},
}
pending.add(request)
const loop = () => {
@@ -62,33 +79,45 @@ export function postRequest(host: RequestHost, { type, data }: Packet): Promise<
if (!pending.has(request))
return
if (host.destroyed) {
request.callbacks.reject(new Error('The instance has been destroyed'))
request.cancel(new Error('The instance has been destroyed'))
return
}
try {
boundaries.get(host)?.prepare()
if (!pending.has(request))
return
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 }, '*')
const packet = { type, data, id }
host.$iframe.contentWindow!.postMessage(boundaries.get(host)?.envelope(packet) || packet, '*')
}
else {
request.timer = setTimeout(loop, 200)
}
}
catch (error) {
request.callbacks.reject(error)
request.cancel(error)
}
}
loop()
})
}
export function cancelRequests(host: RequestHost): void {
export function cancelRequests(host: RequestHost, reason = new Error('The instance has been destroyed')): void {
const pending = requests.get(host)
if (!pending)
return
for (const request of [...pending])
request.callbacks.reject(new Error('The instance has been destroyed'))
request.cancel(reason)
}
export function captureRequests(host: RequestHost): (reason: unknown) => void {
const captured = [...(requests.get(host) || [])]
return (reason) => {
for (const request of captured)
request.cancel(reason)
}
}