import { Version } from "./version.js"; import { VersionRange } from "./version-range.js"; import { RufflePlayer } from "./ruffle-player.js"; /** * Represents the Ruffle public API. * * The public API exists primarily to allow multiple installs of Ruffle on a * page (e.g. an extension install and a local one) to cooperate. In an ideal * situation, all Ruffle sources on the page install themselves into a single * public API, and then the public API picks the newest version by default. * * This API *is* versioned, in case we need to upgrade it. However, it must be * backwards- and forwards-compatible with all known sources. */ export class PublicAPI { /** * Construct the Ruffle public API. * * Do not use this function to negotiate a public API. Instead, use * `public_api` to register your Ruffle source with an existing public API * if it exists. * * Constructing a Public API will also trigger it to initialize Ruffle once * the page loads, if the API has not already been superceded. * * @param {object} prev What used to be in the public API slot. * * This is used to upgrade from a prior version of the public API, or from * a user-defined configuration object placed in the public API slot. */ constructor(prev) { this.sources = {}; this.config = {}; this.invoked = false; this.newest_name = false; if (prev !== undefined) { if (prev.constructor.name === PublicAPI.name) { /// We're upgrading from a previous API to a new one. this.sources = prev.sources; this.config = prev.config; this.invoked = prev.invoked; this.conflict = prev.conflict; this.newest_name = prev.newest_name; prev.superceded(); } else if (prev.constructor === Object && prev.config !== undefined) { /// We're the first, install user configuration this.config = prev.config; } else { /// We're the first, but conflicting with someone else. this.conflict = prev; } } if (document.readyState === "loading") { window.addEventListener("DOMContentLoaded", this.init.bind(this)); } else { window.setTimeout(this.init.bind(this), 0); } } /** * The version of the public API. * * This allows a page with an old version of the Public API to be upgraded * to a new version of the API. The public API is intended to be changed * very infrequently, if at all, but this provides an escape mechanism for * newer Ruffle sources to upgrade older installations. */ get version() { return "0.1.0"; } /** * Register a given source with the Ruffle Public API. * * @param {string} name The name of the source. * @param {object} api The public API object. This must conform to the shape * of `SourceAPI`. */ register_source(name, api) { this.sources[name] = api; } /** * Determine the name of the newest registered source in the Public API. * * @returns {(string|bool)} The name of the source, or `false` if no source * has yet to be registered. */ newest_source_name() { let newest_name = false, newest_version = Version.from_semver("0.0.0"); for (let k in this.sources) { if (this.sources.hasOwnProperty(k)) { let k_version = Version.from_semver(this.sources[k].version); if (k_version.has_precedence_over(newest_version)) { newest_name = k; newest_version = k_version; } } } return newest_name; } /** * Negotiate and start Ruffle. * * This function reads the config parameter to determine which polyfills * should be enabled. If the configuration parameter is missing, then we * use a built-in set of defaults sufficient to fool sites with static * content and weak plugin detection. */ init() { if (!this.invoked) { this.invoked = true; this.newest_name = this.newest_source_name(); if (this.newest_name === false) { throw new Error("No registered Ruffle source!"); } let polyfills = this.config.polyfills; if (polyfills === undefined) { // Default to all polyfills for simplest usage. polyfills = ["plugin-detect", "dynamic-content", "static-content"]; } this.sources[this.newest_name].polyfill(polyfills); } } /** * Look up the newest Ruffle source and return it's API. * * @returns {SourceAPI} An instance of the Source API. */ newest() { return this.sources[this.newest_source_name()]; } /** * Look up a specific Ruffle version (or any version satisfying a given set * of requirements) and return it's API. * * @param {string} ver_requirement A set of semantic version requirement * strings that the player version must satisfy. * * @returns {SourceAPI|null} An instance of the Source API, if one or more * sources satisfied the requirement. */ satisfying(ver_requirement) { let requirement = VersionRange.from_requirement_string(ver_requirement); let valid_source = null; for (let k in this.sources) { if (this.sources.hasOwnProperty(k)) { let version = Version.from_semver(this.sources[k].version); if (requirement.satisfied_by(version)) { valid_source = this.sources[k] } } } return valid_source; } /** * Look up the newest Ruffle version compatible with the `local` source, if * it's installed. Otherwise, use the latest version. */ local_compatible() { if (this.sources.local !== undefined) { return this.satisfying("^" + this.sources.local.version); } else { return this.newest(); } } /** * Look up the newest Ruffle version with the exact same version as the * `local` source, if it's installed. Otherwise, use the latest version. */ local() { if (this.sources.local !== undefined) { return this.satisfying("=" + this.sources.local.version); } else { return this.newest(); } } /** * Indicates that this version of the public API has been superceded by a * newer version. * * This should only be called by a newer version of the Public API. * Identical versions of the Public API should not supercede older versions * of that same API. * * Unfortunately, we can't disable polyfills after-the-fact, so this * only lets you disable the init event... */ superceded() { this.invoked = true; } /** * Join a source into the public API, if it doesn't already exist. * * @param {*} prev_ruffle The previous iteration of the Ruffle API. * * The `prev_ruffle` param lists the previous object in the RufflePlayer * slot. We perform some checks to see if this is a Ruffle public API or a * conflicting object. If this is conflicting, then a new public API will * be constructed (see the constructor information for what happens to * `prev_ruffle`). * * Note that Public API upgrades are deliberately not enabled in this * version of Ruffle, since there is no Public API to upgrade from. * * @param {string|undefined} source_name The name of this particular * Ruffle source. * * @param {object|undefined} source_api The Ruffle source to add. * * If both parameters are provided they will be used to define a new Ruffle * source to register with the public API. * * @returns {object} The Ruffle Public API. */ static negotiate(prev_ruffle, source_name, source_api) { let public_api; if (prev_ruffle !== undefined && prev_ruffle.constructor.name == PublicAPI.name) { public_api = prev_ruffle; } else { public_api = new PublicAPI(prev_ruffle); } if (source_name !== undefined && source_api !== undefined) { public_api.register_source(source_name, source_api); // Install the faux plugin detection immediately. // This is necessary because scripts such as SWFObject check for the // Flash Player immediately when they load. // TODO: Maybe there's a better place for this. let polyfills = public_api.config.polyfills; if (polyfills === undefined || polyfills.includes("plugin-detect")) { source_api.polyfill(["plugin-detect"]); } } return public_api; }; }