{"version":3,"sources":["jsdelivr-header.js","/gh/m-lab/ndt7-js@main/src/ndt7.js"],"names":[],"mappings":"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","file":"/gh/m-lab/ndt7-js@main/src/ndt7.js","sourceRoot":"","sourcesContent":["/**\n * Minified by jsDelivr using Terser v5.48.0.\n * Original file: /gh/m-lab/ndt7-js@main/src/ndt7.js\n *\n * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files\n */\n","// ndt7 contains the core ndt7 client functionality. Please, refer\n// to the ndt7 spec available at the following URL:\n//\n// https://github.com/m-lab/ndt-server/blob/master/spec/ndt7-protocol.md\n//\n// This implementation uses v0.9.0 of the spec.\n\n// Wrap everything in a closure to ensure that local definitions don't\n// permanently shadow global definitions.\n(function() {\n  'use strict';\n\n  /**\n   * @name ndt7\n   * @namespace ndt7\n   */\n  const ndt7 = (function() {\n    const staticMetadata = {\n      'client_library_name': 'ndt7-js',\n      'client_library_version': '0.1.5',\n    };\n    // cb creates a default-empty callback function, allowing library users to\n    // only need to specify callback functions for the events they care about.\n    //\n    // This function is not exported.\n    const cb = function(name, callbacks, defaultFn) {\n      if (typeof(callbacks) !== 'undefined' && name in callbacks) {\n        return callbacks[name];\n      } else if (typeof defaultFn !== 'undefined') {\n        return defaultFn;\n      } else {\n        // If no default function is provided, use the empty function.\n        return function() {};\n      }\n    };\n\n    // The default response to an error is to throw an exception.\n    const defaultErrCallback = function(err) {\n      throw new Error(err);\n    };\n\n    /**\n     * discoverServerURLs contacts a web service (likely the Measurement Lab\n     * locate service, but not necessarily) and gets URLs with access tokens in\n     * them for the client. It can be short-circuted if config.server exists,\n     * which is useful for clients served from the webserver of an NDT server.\n     *\n     * @param {Object} config - An associative array of configuration options.\n     * @param {string} [config.server] - Optional server hostname to connect to\n     *     directly, bypassing the locate service. Useful for testing against a\n     *     specific NDT server.\n     * @param {string} [config.protocol='wss'] - WebSocket protocol to use.\n     *     Either 'wss' (secure WebSocket) or 'ws' (insecure). Defaults to 'wss'.\n     * @param {Object} [config.metadata] - Optional metadata to identify your\n     *     application. Recommended fields: `client_name` (your application name,\n     *     e.g., 'my-speed-test') and `client_version` (your application version,\n     *     e.g., '2.1.0'). These are sent as query parameters to help distinguish\n     *     different integrations. The library automatically includes\n     *     `client_library_name` ('ndt7-js') and `client_library_version`.\n     * @param {string} [config.loadbalancer] - Optional custom locate service\n     *     URL to use instead of the default M-Lab locate service.\n     * @param {string} [config.clientRegistrationToken] - Optional JWT token for\n     *     registered integrator access. When provided, identifies that tests are\n     *     being run through a registered client integration, enabling access to\n     *     the priority endpoint (v2/priority/nearest) with higher rate limits.\n     *     The token should be obtained from your integrator backend that securely\n     *     manages API credentials with the M-Lab token exchange service. This\n     *     registers your client implementation, not individual end users.\n     * @param {Object} userCallbacks - An associative array of user callbacks.\n     * @param {Function} [userCallbacks.error] - Called when an error occurs.\n     *     Receives an error message string. If not provided, errors throw.\n     * @param {Function} [userCallbacks.serverDiscovery] - Called when server\n     *     discovery starts. Receives `{loadbalancer: URL}` where URL is the\n     *     locate service URL being queried.\n     * @param {Function} [userCallbacks.serverChosen] - Called when a server\n     *     is selected. Receives the server object from locate results.\n     *\n     * @name ndt7.discoverServerURLS\n     * @public\n     */\n    async function discoverServerURLs(config, userCallbacks) {\n      config.metadata = Object.assign({}, config.metadata);\n      config.metadata = Object.assign(config.metadata, staticMetadata);\n      const callbacks = {\n        error: cb('error', userCallbacks, defaultErrCallback),\n        serverDiscovery: cb('serverDiscovery', userCallbacks),\n        serverChosen: cb('serverChosen', userCallbacks),\n      };\n      let protocol = 'wss';\n      if (config && config.protocol) {\n        protocol = config.protocol;\n      }\n\n      const metadata = new URLSearchParams(config.metadata);\n      // If a server was specified, use it.\n      if (config && config.server) {\n        // Add metadata as querystring parameters.\n        const downloadURL = new URL(protocol + '://' + config.server + '/ndt/v7/download');\n        const uploadURL = new URL(protocol + '://' + config.server + '/ndt/v7/upload');\n        downloadURL.search = metadata;\n        uploadURL.search = metadata;\n        return {\n          '///ndt/v7/download': downloadURL.toString(),\n          '///ndt/v7/upload': uploadURL.toString(),\n        };\n      }\n\n      // If no server was specified then use a loadbalancer. If no loadbalancer\n      // is specified, use the locate service from Measurement Lab.\n      //\n      // When a clientRegistrationToken is provided and no custom loadbalancer\n      // is specified, use the priority endpoint.\n      let lbURL;\n      if (config && config.loadbalancer) {\n        // Use custom loadbalancer if specified\n        lbURL = new URL(config.loadbalancer);\n      } else if (config && config.clientRegistrationToken) {\n        // Use priority endpoint for authenticated requests\n        lbURL = new URL('https://locate.measurementlab.net/v2/priority/nearest/ndt/ndt7');\n      } else {\n        // Use regular endpoint for unauthenticated requests\n        lbURL = new URL('https://locate.measurementlab.net/v2/nearest/ndt/ndt7');\n      }\n      lbURL.search = metadata;\n      callbacks.serverDiscovery({loadbalancer: lbURL});\n\n      // Prepare fetch options with Authorization header if token is present\n      const fetchOptions = {};\n      if (config && config.clientRegistrationToken) {\n        fetchOptions.headers = {\n          'Authorization': `Bearer ${config.clientRegistrationToken}`,\n        };\n      }\n\n      // Perform the HTTP round trip with the load balancer\n      const response = await fetch(lbURL, fetchOptions).catch((err) => {\n        throw new Error(err);\n      });\n      const js = await response.json();\n      if (! ('results' in js) ) {\n        callbacks.error(`Could not understand response from ${lbURL}: ${js}`);\n        return {};\n      }\n\n      // TODO: do not discard unused results. If the first server is unavailable\n      // the client should quickly try the next server.\n      //\n      // Choose the first result sent by the load balancer. This ensures that\n      // in cases where we have a single pod in a metro, that pod is used to\n      // run the measurement. When there are multiple pods in the same metro,\n      // they are randomized by the load balancer already.\n      const choice = js.results[0];\n      callbacks.serverChosen(choice);\n\n      return {\n        '///ndt/v7/download': choice.urls[protocol + ':///ndt/v7/download'],\n        '///ndt/v7/upload': choice.urls[protocol + ':///ndt/v7/upload'],\n      };\n    }\n\n    /*\n     * runNDT7Worker is a helper function that runs a webworker. It uses the\n     * callback functions `error`, `start`, `measurement`, and `complete`. It\n     * returns a c-style return code. 0 is success, non-zero is some kind of\n     * failure.\n     *\n     * @private\n     */\n    const runNDT7Worker = async function(\n        config, callbacks, urlPromise, filename, testType) {\n      if (config.userAcceptedDataPolicy !== true &&\n          config.mlabDataPolicyInapplicable !== true) {\n        callbacks.error('The M-Lab data policy is applicable and the user ' +\n                        'has not explicitly accepted that data policy.');\n        return 1;\n      }\n\n      let clientMeasurement;\n      let serverMeasurement;\n\n      // This makes the worker. The worker won't actually start until it\n      // receives a message.\n      const worker = new Worker(filename);\n\n      // When the workerPromise gets resolved it will terminate the worker.\n      // Workers are resolved with c-style return codes. 0 for success,\n      // non-zero for failure.\n      const workerPromise = new Promise((resolve) => {\n        worker.resolve = function(returnCode) {\n          if (returnCode == 0) {\n            callbacks.complete({\n              LastClientMeasurement: clientMeasurement,\n              LastServerMeasurement: serverMeasurement,\n            });\n          }\n          worker.terminate();\n          resolve(returnCode);\n        };\n      });\n\n      // Connection timeout: if the WebSocket never opens (and never\n      // fires onerror/onclose), resolve after 10s to prevent the\n      // promise from hanging indefinitely.\n      const connTimeout = setTimeout(() => worker.resolve(0), 10000);\n\n      // The test duration timeout is set when the worker reports\n      // 'start' (i.e. sock.onopen), so that WS+TLS handshake time\n      // does not eat into the test duration.\n      let workerTimeout;\n\n      // This is how the worker communicates back to the main thread of\n      // execution.  The MsgTpe of `ev` determines which callback the message\n      // gets forwarded to.\n      worker.onmessage = function(ev) {\n        if (!ev.data || !ev.data.MsgType || ev.data.MsgType === 'error') {\n          clearTimeout(connTimeout);\n          clearTimeout(workerTimeout);\n          worker.resolve(1);\n          const msg = (!ev.data) ? `${testType} error` : ev.data.Error;\n          callbacks.error(msg);\n        } else if (ev.data.MsgType === 'start') {\n          clearTimeout(connTimeout);\n          // Safety timeout anchored to connect, not worker creation.\n          // 12s gives the server's 10s duration time to finish plus\n          // margin for the upload buffer to drain.\n          workerTimeout = setTimeout(() => worker.resolve(0), 12000);\n          callbacks.start(ev.data.Data);\n        } else if (ev.data.MsgType == 'measurement') {\n          // For performance reasons, we parse the JSON outside of the thread\n          // doing the downloading or uploading.\n          if (ev.data.Source == 'server') {\n            serverMeasurement = JSON.parse(ev.data.ServerMessage);\n            callbacks.measurement({\n              Source: ev.data.Source,\n              Data: serverMeasurement,\n            });\n          } else {\n            clientMeasurement = ev.data.ClientData;\n            callbacks.measurement({\n              Source: ev.data.Source,\n              Data: ev.data.ClientData,\n            });\n          }\n        } else if (ev.data.MsgType == 'complete') {\n          clearTimeout(workerTimeout);\n          worker.resolve(0);\n        }\n      };\n\n      // We can't start the worker until we know the right server, so we wait\n      // here to find that out.\n      const urls = await urlPromise.catch((err) => {\n        // Clear timers, terminate the worker and rethrow the error.\n        clearTimeout(connTimeout);\n        clearTimeout(workerTimeout);\n        worker.resolve(2);\n        throw err;\n      });\n\n      // Start the worker.\n      worker.postMessage(urls);\n\n      // Await the resolution of the workerPromise.\n      return await workerPromise;\n\n      // Liveness guarantee - once the promise is resolved, .terminate() has\n      // been called and the webworker will be terminated or in the process of\n      // being terminated.\n    };\n\n    /**\n     * downloadTest runs just the NDT7 download test.\n     * @param {Object} config - An associative array of configuration strings\n     * @param {Object} userCallbacks\n     * @param {Object} urlPromise - A promise that will resolve to urls.\n     *\n     * @return {number} Zero on success, and non-zero error code on failure.\n     *\n     * @name ndt7.downloadTest\n     * @public\n     */\n    async function downloadTest(config, userCallbacks, urlPromise) {\n      const callbacks = {\n        error: cb('error', userCallbacks, defaultErrCallback),\n        start: cb('downloadStart', userCallbacks),\n        measurement: cb('downloadMeasurement', userCallbacks),\n        complete: cb('downloadComplete', userCallbacks),\n      };\n      const workerfile = config.downloadworkerfile || 'ndt7-download-worker.js';\n      return await runNDT7Worker(\n          config, callbacks, urlPromise, workerfile, 'download')\n          .catch((err) => {\n            callbacks.error(err);\n          });\n    }\n\n    /**\n     * uploadTest runs just the NDT7 download test.\n     * @param {Object} config - An associative array of configuration strings\n     * @param {Object} userCallbacks\n     * @param {Object} urlPromise - A promise that will resolve to urls.\n     *\n     * @return {number} Zero on success, and non-zero error code on failure.\n     *\n     * @name ndt7.uploadTest\n     * @public\n     */\n    async function uploadTest(config, userCallbacks, urlPromise) {\n      const callbacks = {\n        error: cb('error', userCallbacks, defaultErrCallback),\n        start: cb('uploadStart', userCallbacks),\n        measurement: cb('uploadMeasurement', userCallbacks),\n        complete: cb('uploadComplete', userCallbacks),\n      };\n      const workerfile = config.uploadworkerfile || 'ndt7-upload-worker.js';\n      const rv = await runNDT7Worker(\n          config, callbacks, urlPromise, workerfile, 'upload')\n          .catch((err) => {\n            callbacks.error(err);\n          });\n      return rv << 4;\n    }\n\n    /**\n     * test discovers a server to run against and then runs a download test\n     * followed by an upload test.\n     *\n     * @param {Object} config - An associative array of configuration options.\n     * @param {string} [config.server] - Optional server hostname to connect to\n     *     directly, bypassing the locate service. Useful for testing against a\n     *     specific NDT server.\n     * @param {string} [config.protocol='wss'] - WebSocket protocol to use.\n     *     Either 'wss' (secure WebSocket) or 'ws' (insecure). Defaults to 'wss'.\n     * @param {Object} [config.metadata] - Optional metadata to identify your\n     *     application. Recommended fields: `client_name` (your application name,\n     *     e.g., 'my-speed-test') and `client_version` (your application version,\n     *     e.g., '2.1.0'). These are sent as query parameters to help distinguish\n     *     different integrations. The library automatically includes\n     *     `client_library_name` ('ndt7-js') and `client_library_version`.\n     * @param {string} [config.loadbalancer] - Optional custom locate service\n     *     URL to use instead of the default M-Lab locate service.\n     * @param {string} [config.clientRegistrationToken] - Optional JWT token for\n     *     registered integrator access. When provided, identifies that tests are\n     *     being run through a registered client integration, enabling access to\n     *     the priority endpoint (v2/priority/nearest) with higher rate limits.\n     *     The token should be obtained from your integrator backend that securely\n     *     manages API credentials with the M-Lab token exchange service. This\n     *     registers your client implementation, not individual end users.\n     * @param {boolean} [config.userAcceptedDataPolicy] - Must be set to true\n     *     to indicate the user has accepted M-Lab's data policy. Required unless\n     *     mlabDataPolicyInapplicable is true.\n     * @param {boolean} [config.mlabDataPolicyInapplicable] - Set to true if\n     *     M-Lab's data policy does not apply to your use case.\n     * @param {Object} userCallbacks - An associative array of user callbacks.\n     * @param {Function} [userCallbacks.error] - Called when an error occurs.\n     *     Receives an error message string. If not provided, errors throw.\n     * @param {Function} [userCallbacks.serverDiscovery] - Called when server\n     *     discovery starts. Receives `{loadbalancer: URL}` where URL is the\n     *     locate service URL being queried.\n     * @param {Function} [userCallbacks.serverChosen] - Called when a server\n     *     is selected. Receives the server object from locate results.\n     * @param {Function} [userCallbacks.downloadStart] - Called when download\n     *     test starts. Receives start event data.\n     * @param {Function} [userCallbacks.downloadMeasurement] - Called during\n     *     download test. Receives `{Source, Data}` where Source is 'client'\n     *     or 'server' and Data contains measurement values.\n     * @param {Function} [userCallbacks.downloadComplete] - Called when download\n     *     completes. Receives `{LastClientMeasurement, LastServerMeasurement}`.\n     * @param {Function} [userCallbacks.uploadStart] - Called when upload\n     *     test starts. Receives start event data.\n     * @param {Function} [userCallbacks.uploadMeasurement] - Called during\n     *     upload test. Receives `{Source, Data}` where Source is 'client'\n     *     or 'server' and Data contains measurement values.\n     * @param {Function} [userCallbacks.uploadComplete] - Called when upload\n     *     completes. Receives `{LastClientMeasurement, LastServerMeasurement}`.\n     *\n     * @return {number} Zero on success, non-zero on failure.\n     *\n     * @name ndt7.test\n     * @public\n     */\n    async function test(config, userCallbacks) {\n      // Starts the asynchronous process of server discovery, allowing other\n      // stuff to proceed in the background.\n      const urlPromise = discoverServerURLs(config, userCallbacks);\n      const downloadSuccess = await downloadTest(\n          config, userCallbacks, urlPromise);\n      const uploadSuccess = await uploadTest(\n          config, userCallbacks, urlPromise);\n      return downloadSuccess + uploadSuccess;\n    }\n\n    return {\n      discoverServerURLs: discoverServerURLs,\n      downloadTest: downloadTest,\n      uploadTest: uploadTest,\n      test: test,\n    };\n  })();\n\n  // Modules are used by `require`, if this file is included on a web page, then\n  // module will be undefined and we use the window.ndt7 piece.\n  if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {\n    module.exports = ndt7;\n  } else {\n    window.ndt7 = ndt7;\n  }\n})();\n"]}