Files
ArtPlayer/packages/artplayer-tool-iframe/ARCHITECTURE.md

16 KiB

Iframe maintenance map

Package-name rollback must restore the application imports and saved dependency lock together. The old plugin package exposes a CommonJS namespace with .default and distinct helper files; installing its tarball under the current tool dependency key does not preserve the current constructor or /legacy imports. The isolated build/install/type rehearsal is yarn test:rollback:iframe; see rollback maintenance. This does not replace the package's cross-window, playback or device acceptance.

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. 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 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. If constructor setup fails, it marks the partial instance destroyed, releases the acquired listener and cancels reentrant requests, preserving the original setup error even if cleanup also fails. The listener receiver and owning window are captured on acquisition; connection ownership is removed before releasing it.

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. 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 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

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.

Both receivers ignore malformed payloads (a non-null object with a string type is required, including an empty string). The parent accepts native messages only from the configured iframe's contentWindow; the child accepts them only from window.parent. Public direct onMessage calls with a missing/null source remain available to local callers. Same-page script or privileged injection is not isolated by this check. Generic response types and commit payload error handling are unchanged after the peer/envelope check.

The initial URL's origin is deliberately not pinned: redirects and sandboxed opaque-origin children still communicate with their selected parent. The peer's content and the embedding parent must be trusted; executable commit is not a 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.

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 are maintained in types/artplayer-tool-iframe.d.ts. The .d.cts bridge describes the actual CommonJS constructor and its named types; .d.mts routes native ESM to the same class. Keep the root types field and legacy typesVersions entry for TS 4.3 consumers. The runtime has no self-default property. Ordinary default imports use interop in CommonJS consumers; modern TypeScript can use direct import Iframe = require(...) without interop.

The default class intentionally retains required Message.data, void static onMessage, readonly instance fields, non-null callback and legacy commit inference. RuntimeConstructor/RuntimeInstance expose optional outgoing data, async static receiver, nullable callback and application-supplied response types. ResolverInstance offers an explicit result parameter for the old serialized resolve(...) protocol. These are erased type views, not new runtime methods or payload validators. The source class satisfies RuntimeConstructor; its nullable callback intentionally does not satisfy the old non-null class declaration, and tests pin that difference.

Old npm plugin-iframe used Function callbacks and a CommonJS default namespace; those differ from the frozen workspace before this refactor. Installed tests preserve that evidence and the separate helper's static destroy protocol. They do not claim the renamed tool supplies the old package name or helper entrypoints. PKG-IFRAME-06 still owns those distribution decisions and compatibility facades. No version bump or publication is implied by declaration validation.

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, including old extraction and new typed views in TS 5.9 and TS 4.3.
  • yarn test:iframe-types-package: install the real old npm archive, frozen workspace pack and candidate outside the workspace with offline/frozen Yarn. Check declaration resolution, exact bytes, exports and positive/negative uses.
  • yarn build:ts artplayer-tool-iframe: regenerate only this package's standalone editor declaration (plus the shared core declaration). The generator uses the actual uppercase class global and exports named types without external imports.
  • 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.
  • yarn test:browser test/browser/iframe-boundaries.spec.js: native peer rejection, 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. 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. iframe-player.spec.js runs the real parent example, child HTML and stylesheet with core 4.5.9, frozen 5.4.0 and the candidate, all four new/old bridge pairings, and same/cross-origin documents. Historical global-name aliases are test-only. It exercises actual media playback, seek/rate, source switch, fullscreenWeb controls and independent core/tool destruction. Hover the player before clicking controls, as a user does; old cores hide their controls during playback.

iframe-editor.spec.js opens the actual docs index and local Monaco, clicks Run repeatedly, and switches from an iframe example to two parent players and then an empty example. The editor loads the core before Monaco's AMD loader so the UMD core exposes its script global. Before each Run it dispatches the docs-only artplayer:example:cleanup event, then destroys a snapshot of parent instances. The iframe example subscribes once to destroy its tool and remove its frame; its initial commit handles cancellation during cleanup. This is an editor convention, not automatic DOM-removal behavior or a new player API. Standalone integrations remain responsible for calling destroy themselves.

yarn test:iframe-history runs the separate cacheable HTTP fixture and full Chromium channel, removing Playwright's BFCache-disabling default argument. It checks actual whole-page restoration through native persisted events, parent/child witnesses, media and the real commit protocol. Firefox/WebKit reload controls are reported separately and never counted as cached restoration. Do not replace this configuration with the default headless shell or simulated lifecycle events.

An entire cached page also freezes the parent. If the private leave message did not reach it, the original request can remain pending and settle after restoration. If leave was delivered, that request remains rejected; a late result cannot change its outcome. Resume retains the same document ID and emits no duplicate public inject. The test checks actual delivered phases against settlement.

The same command tests native stop, HTTP 204 and a truncated document response. After a failed navigation, new work still waits for actual injection into the target document; it is not sent to the unrelated old document. Navigate the frame to a valid source or destroy the tool to finish/cancel that work. There is no implicit timeout or automatic fallback to an old source.

Physical devices, native Firefox/WebKit cached restoration and final distribution remain separate gates. Desktop evidence does not establish the old helper or package-name migration. See ADR-026 and the task 05 history checkpoint for the exact fixture, automation limitations and source/main/legacy evidence.

Installed browser checks

yarn test:package --browser includes this tool in the shared installed roster. The pack checker verifies the real artplayer-plugin-iframe@1.0.0 archive and the frozen workspace independently. The tool's required dist/type filenames come from its verified workspace manifest and files; the old plugin/helper names are not silently treated as releases of the tool. Publishing or restoring the predecessor package remains a separate distribution decision.

The five test/browser/iframe*.spec.js files use iframeBrowserCandidate() from test/helpers/iframe.js. With an installed map it must load the verified tool UMD, reject an explicit artifact or frozen-workspace override, and record the package/file/archive/source identity for each candidate parent and child. Without a map the same tests retain their source and historical controls. iframeCandidate() keeps its existing unit/history semantics: history may inherit a core-only map, which does not select a tool build. Do not merge these two fixture entrypoints or claim the separate BFCache suite used an installed tool when it used a source/explicit tool. Browser scope configuration rejects diagnostic flags that would remove historical Iframe rows.

The installed tests cover protocol, peer/document boundaries, native navigation, old/new player combinations and the real docs editor. They do not replace the special test:iframe-history browser channel or physical-device evidence. See ../../refactor/changes/2026-09-15-CI-01-iframe-installed.md for measured results.