p2p-media-loader-core
Version:
P2P Media Loader core functionality
1,441 lines • 171 kB
JavaScript
//#region \0rolldown/runtime.js
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
var __exportAll = (all, no_symbols) => {
let target = {};
for (var name in all) __defProp(target, name, {
get: all[name],
enumerable: true
});
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
return target;
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
key = keys[i];
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
get: ((k) => from[k]).bind(null, key),
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
});
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
value: mod,
enumerable: true
}) : target, mod));
//#endregion
//#region src/types.ts
/**
* Base class for domain-specific errors carrying a machine-readable type discriminator.
* @internal
*/
var TypedError = class extends Error {
type;
cause;
constructor(type, message, cause) {
super(message);
this.type = type;
this.cause = cause;
}
};
/** Represents an error that occurred during a peer connection. */
var PeerError = class extends TypedError {
name = "PeerError";
};
/** Represents a warning that occurred during a peer connection. */
var PeerWarning = class extends TypedError {
name = "PeerWarning";
};
/** Represents an error that occurred during a tracker request. */
var TrackerError = class extends TypedError {
name = "TrackerError";
};
/** Represents a warning that occurred during a tracker request. */
var TrackerWarning = class extends TypedError {
name = "TrackerWarning";
};
/** Represents an error that occurred while establishing a peer connection. */
var PeerConnectError = class extends TypedError {
name = "PeerConnectError";
};
/**
* Represents an error that can occur during the request process, with a timestamp for when the error occurred.
* @template T - The specific type of request error.
*/
var RequestError = class extends TypedError {
name = "RequestError";
/** Error timestamp. */
timestamp;
/**
* Constructs a new RequestError.
* @param type - The specific error type.
* @param message - Optional message describing the error.
* @param cause - Optional underlying cause of the error.
*/
constructor(type, message, cause) {
super(type, message, cause);
this.timestamp = performance.now();
}
};
/** Custom error class for errors that occur during core network requests. */
var CoreRequestError = class extends TypedError {
name = "CoreRequestError";
};
//#endregion
//#region src/utils/abort-controller.ts
var AbortSignalPolyfill = class {
aborted = false;
#listeners = /* @__PURE__ */ new Set();
addEventListener(_type, listener) {
this.#listeners.add(listener);
}
removeEventListener(_type, listener) {
this.#listeners.delete(listener);
}
dispatchEvent(_type) {
this.aborted = true;
for (const listener of this.#listeners) try {
listener();
} catch {}
this.#listeners.clear();
}
};
var AbortControllerPolyfill = class {
signal = new AbortSignalPolyfill();
abort() {
this.signal.dispatchEvent("abort");
}
};
var isAbortControllerSupported = typeof AbortController !== "undefined";
var SafeAbortController = isAbortControllerSupported ? AbortController : AbortControllerPolyfill;
//#endregion
//#region src/http-loader.ts
var HttpRequestExecutor = class {
request;
httpConfig;
abortController = new SafeAbortController();
expectedBytesLength;
requestByteRange;
onChunkDownloaded;
isAborted() {
return this.abortController.signal.aborted;
}
constructor(request, httpConfig, eventTarget) {
this.request = request;
this.httpConfig = httpConfig;
this.onChunkDownloaded = eventTarget.getEventDispatcher("onChunkDownloaded");
const { byteRange } = this.request.segment;
if (byteRange) this.requestByteRange = { ...byteRange };
}
execute() {
const startControls = {
onAbort: () => this.abortController.abort(),
notReceivingBytesTimeoutMs: this.httpConfig.httpNotReceivingBytesTimeoutMs
};
if (this.request.tryCompleteByLoadedBytes({ downloadSource: "http" }, startControls, this.httpConfig.validateHTTPSegment, "http-segment-validation-failed")) return;
if (this.request.loadedBytes !== 0) {
this.requestByteRange = this.requestByteRange ?? { start: 0 };
this.requestByteRange.start = this.requestByteRange.start + this.request.loadedBytes;
}
if (this.request.totalBytes) this.expectedBytesLength = this.request.totalBytes - this.request.loadedBytes;
const requestControls = this.request.start({ downloadSource: "http" }, startControls);
this.fetch(requestControls);
}
async fetch(requestControls) {
const { segment } = this.request;
let activeReader;
if (this.isAborted()) return;
const onAbort = () => {
try {
activeReader?.cancel().catch(() => {});
} catch {}
};
this.abortController.signal.addEventListener("abort", onAbort);
const abortSignal = isAbortControllerSupported ? this.abortController.signal : void 0;
try {
let request = await this.httpConfig.httpRequestSetup?.(segment.url, segment.byteRange, abortSignal, this.requestByteRange);
if (this.isAborted()) throw new DOMException("Request aborted", "AbortError");
if (!request) {
const headers = new Headers();
if (this.requestByteRange) headers.set("Range", `bytes=${this.requestByteRange.start}-${this.requestByteRange.end ?? ""}`);
const requestOptions = { headers };
if (abortSignal) requestOptions.signal = abortSignal;
request = new Request(segment.url, requestOptions);
}
if (this.isAborted()) throw new DOMException("Request aborted before request fetch", "AbortError");
const response = await window.fetch(request);
if (this.isAborted()) throw new DOMException("Request aborted", "AbortError");
this.handleResponseHeaders(response);
requestControls.firstBytesReceived();
if (!response.body || typeof response.body.getReader !== "function") {
const arrayBuffer = await response.arrayBuffer();
if (this.isAborted()) throw new DOMException("Request aborted", "AbortError");
const value = new Uint8Array(arrayBuffer);
requestControls.addLoadedChunk(value);
this.onChunkDownloaded(value.byteLength, "http", void 0, segment.stream.type, this.request.infoHash);
} else {
const reader = response.body.getReader();
activeReader = reader;
for (;;) {
if (this.isAborted()) throw new DOMException("Request aborted", "AbortError");
const { done, value } = await reader.read();
if (done) break;
if (this.isAborted()) throw new DOMException("Request aborted", "AbortError");
requestControls.addLoadedChunk(value);
this.onChunkDownloaded(value.byteLength, "http", void 0, segment.stream.type, this.request.infoHash);
}
}
if (this.isAborted()) throw new DOMException("Request aborted", "AbortError");
if (this.request.totalBytes !== void 0 && this.request.loadedBytes !== this.request.totalBytes) throw new RequestError("http-bytes-mismatch", `HTTP response truncated: received ${this.request.loadedBytes} of ${this.request.totalBytes} bytes`);
const isValid = await this.request.validateData(this.httpConfig.validateHTTPSegment);
if (this.isAborted()) throw new DOMException("Request aborted", "AbortError");
if (!isValid) {
this.request.clearLoadedBytes();
throw new RequestError("http-segment-validation-failed");
}
requestControls.completeOnSuccess();
} catch (error) {
this.handleError(error, requestControls);
} finally {
this.abortController.signal.removeEventListener("abort", onAbort);
}
}
handleResponseHeaders(response) {
if (!response.ok) if (response.status === 406 || response.status === 416) {
this.request.clearLoadedBytes();
throw new RequestError("http-bytes-mismatch", response.statusText);
} else throw new RequestError("http-error", response.statusText);
const { requestByteRange } = this;
if (requestByteRange) if (response.status === 200) if (this.request.segment.byteRange) throw new RequestError("http-unexpected-status-code");
else this.request.clearLoadedBytes();
else {
if (response.status !== 206) throw new RequestError("http-unexpected-status-code", response.statusText);
const contentLengthHeader = response.headers.get("Content-Length");
if (contentLengthHeader && this.expectedBytesLength !== void 0 && this.expectedBytesLength !== +contentLengthHeader) {
this.request.clearLoadedBytes();
throw new RequestError("http-bytes-mismatch", response.statusText);
}
const contentRangeHeader = response.headers.get("Content-Range");
const contentRange = contentRangeHeader ? parseContentRangeHeader(contentRangeHeader) : void 0;
if (contentRange) {
const { from, to } = contentRange;
const responseExpectedBytesLength = to !== void 0 && from !== void 0 ? to - from + 1 : void 0;
if (responseExpectedBytesLength !== void 0 && this.expectedBytesLength !== responseExpectedBytesLength || from !== void 0 && requestByteRange.start !== from || to !== void 0 && requestByteRange.end !== void 0 && requestByteRange.end !== to) {
this.request.clearLoadedBytes();
throw new RequestError("http-bytes-mismatch", response.statusText);
}
}
}
if (response.status === 200 && this.request.totalBytes === void 0) {
const contentLengthHeader = response.headers.get("Content-Length");
if (contentLengthHeader) this.request.setTotalBytes(+contentLengthHeader);
}
}
handleError(error, requestControls) {
if (this.isAborted()) return;
if (error instanceof Error) {
const httpLoaderError = error instanceof RequestError ? error : new RequestError("http-error", error.message);
requestControls.failWithError(httpLoaderError);
}
}
};
var rangeHeaderRegex = /^bytes (?:(?:(\d+)|)-(?:(\d+)|)|\*)\/(?:(\d+)|\*)$/;
function parseContentRangeHeader(headerValue) {
const match = rangeHeaderRegex.exec(headerValue.trim());
if (!match) return;
const [, from, to, total] = match;
return {
from: from ? parseInt(from) : void 0,
to: to ? parseInt(to) : void 0,
total: total ? parseInt(total) : void 0
};
}
//#endregion
//#region ../../node_modules/.pnpm/ms@2.1.3/node_modules/ms/index.js
var require_ms = /* @__PURE__ */ __commonJSMin(((exports, module) => {
/**
* Helpers.
*/
var s = 1e3;
var m = s * 60;
var h = m * 60;
var d = h * 24;
var w = d * 7;
var y = d * 365.25;
/**
* Parse or format the given `val`.
*
* Options:
*
* - `long` verbose formatting [false]
*
* @param {String|Number} val
* @param {Object} [options]
* @throws {Error} throw an error if val is not a non-empty string or a number
* @return {String|Number}
* @api public
*/
module.exports = function(val, options) {
options = options || {};
var type = typeof val;
if (type === "string" && val.length > 0) return parse(val);
else if (type === "number" && isFinite(val)) return options.long ? fmtLong(val) : fmtShort(val);
throw new Error("val is not a non-empty string or a valid number. val=" + JSON.stringify(val));
};
/**
* Parse the given `str` and return milliseconds.
*
* @param {String} str
* @return {Number}
* @api private
*/
function parse(str) {
str = String(str);
if (str.length > 100) return;
var match = /^(-?(?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec(str);
if (!match) return;
var n = parseFloat(match[1]);
switch ((match[2] || "ms").toLowerCase()) {
case "years":
case "year":
case "yrs":
case "yr":
case "y": return n * y;
case "weeks":
case "week":
case "w": return n * w;
case "days":
case "day":
case "d": return n * d;
case "hours":
case "hour":
case "hrs":
case "hr":
case "h": return n * h;
case "minutes":
case "minute":
case "mins":
case "min":
case "m": return n * m;
case "seconds":
case "second":
case "secs":
case "sec":
case "s": return n * s;
case "milliseconds":
case "millisecond":
case "msecs":
case "msec":
case "ms": return n;
default: return;
}
}
/**
* Short format for `ms`.
*
* @param {Number} ms
* @return {String}
* @api private
*/
function fmtShort(ms) {
var msAbs = Math.abs(ms);
if (msAbs >= d) return Math.round(ms / d) + "d";
if (msAbs >= h) return Math.round(ms / h) + "h";
if (msAbs >= m) return Math.round(ms / m) + "m";
if (msAbs >= s) return Math.round(ms / s) + "s";
return ms + "ms";
}
/**
* Long format for `ms`.
*
* @param {Number} ms
* @return {String}
* @api private
*/
function fmtLong(ms) {
var msAbs = Math.abs(ms);
if (msAbs >= d) return plural(ms, msAbs, d, "day");
if (msAbs >= h) return plural(ms, msAbs, h, "hour");
if (msAbs >= m) return plural(ms, msAbs, m, "minute");
if (msAbs >= s) return plural(ms, msAbs, s, "second");
return ms + " ms";
}
/**
* Pluralization helper.
*/
function plural(ms, msAbs, n, name) {
var isPlural = msAbs >= n * 1.5;
return Math.round(ms / n) + " " + name + (isPlural ? "s" : "");
}
}));
//#endregion
//#region ../../node_modules/.pnpm/debug@4.4.3/node_modules/debug/src/common.js
var require_common = /* @__PURE__ */ __commonJSMin(((exports, module) => {
/**
* This is the common logic for both the Node.js and web browser
* implementations of `debug()`.
*/
function setup(env) {
createDebug.debug = createDebug;
createDebug.default = createDebug;
createDebug.coerce = coerce;
createDebug.disable = disable;
createDebug.enable = enable;
createDebug.enabled = enabled;
createDebug.humanize = require_ms();
createDebug.destroy = destroy;
Object.keys(env).forEach((key) => {
createDebug[key] = env[key];
});
/**
* The currently active debug mode names, and names to skip.
*/
createDebug.names = [];
createDebug.skips = [];
/**
* Map of special "%n" handling functions, for the debug "format" argument.
*
* Valid key names are a single, lower or upper-case letter, i.e. "n" and "N".
*/
createDebug.formatters = {};
/**
* Selects a color for a debug namespace
* @param {String} namespace The namespace string for the debug instance to be colored
* @return {Number|String} An ANSI color code for the given namespace
* @api private
*/
function selectColor(namespace) {
let hash = 0;
for (let i = 0; i < namespace.length; i++) {
hash = (hash << 5) - hash + namespace.charCodeAt(i);
hash |= 0;
}
return createDebug.colors[Math.abs(hash) % createDebug.colors.length];
}
createDebug.selectColor = selectColor;
/**
* Create a debugger with the given `namespace`.
*
* @param {String} namespace
* @return {Function}
* @api public
*/
function createDebug(namespace) {
let prevTime;
let enableOverride = null;
let namespacesCache;
let enabledCache;
function debug(...args) {
if (!debug.enabled) return;
const self = debug;
const curr = Number(/* @__PURE__ */ new Date());
self.diff = curr - (prevTime || curr);
self.prev = prevTime;
self.curr = curr;
prevTime = curr;
args[0] = createDebug.coerce(args[0]);
if (typeof args[0] !== "string") args.unshift("%O");
let index = 0;
args[0] = args[0].replace(/%([a-zA-Z%])/g, (match, format) => {
if (match === "%%") return "%";
index++;
const formatter = createDebug.formatters[format];
if (typeof formatter === "function") {
const val = args[index];
match = formatter.call(self, val);
args.splice(index, 1);
index--;
}
return match;
});
createDebug.formatArgs.call(self, args);
(self.log || createDebug.log).apply(self, args);
}
debug.namespace = namespace;
debug.useColors = createDebug.useColors();
debug.color = createDebug.selectColor(namespace);
debug.extend = extend;
debug.destroy = createDebug.destroy;
Object.defineProperty(debug, "enabled", {
enumerable: true,
configurable: false,
get: () => {
if (enableOverride !== null) return enableOverride;
if (namespacesCache !== createDebug.namespaces) {
namespacesCache = createDebug.namespaces;
enabledCache = createDebug.enabled(namespace);
}
return enabledCache;
},
set: (v) => {
enableOverride = v;
}
});
if (typeof createDebug.init === "function") createDebug.init(debug);
return debug;
}
function extend(namespace, delimiter) {
const newDebug = createDebug(this.namespace + (typeof delimiter === "undefined" ? ":" : delimiter) + namespace);
newDebug.log = this.log;
return newDebug;
}
/**
* Enables a debug mode by namespaces. This can include modes
* separated by a colon and wildcards.
*
* @param {String} namespaces
* @api public
*/
function enable(namespaces) {
createDebug.save(namespaces);
createDebug.namespaces = namespaces;
createDebug.names = [];
createDebug.skips = [];
const split = (typeof namespaces === "string" ? namespaces : "").trim().replace(/\s+/g, ",").split(",").filter(Boolean);
for (const ns of split) if (ns[0] === "-") createDebug.skips.push(ns.slice(1));
else createDebug.names.push(ns);
}
/**
* Checks if the given string matches a namespace template, honoring
* asterisks as wildcards.
*
* @param {String} search
* @param {String} template
* @return {Boolean}
*/
function matchesTemplate(search, template) {
let searchIndex = 0;
let templateIndex = 0;
let starIndex = -1;
let matchIndex = 0;
while (searchIndex < search.length) if (templateIndex < template.length && (template[templateIndex] === search[searchIndex] || template[templateIndex] === "*")) if (template[templateIndex] === "*") {
starIndex = templateIndex;
matchIndex = searchIndex;
templateIndex++;
} else {
searchIndex++;
templateIndex++;
}
else if (starIndex !== -1) {
templateIndex = starIndex + 1;
matchIndex++;
searchIndex = matchIndex;
} else return false;
while (templateIndex < template.length && template[templateIndex] === "*") templateIndex++;
return templateIndex === template.length;
}
/**
* Disable debug output.
*
* @return {String} namespaces
* @api public
*/
function disable() {
const namespaces = [...createDebug.names, ...createDebug.skips.map((namespace) => "-" + namespace)].join(",");
createDebug.enable("");
return namespaces;
}
/**
* Returns true if the given mode name is enabled, false otherwise.
*
* @param {String} name
* @return {Boolean}
* @api public
*/
function enabled(name) {
for (const skip of createDebug.skips) if (matchesTemplate(name, skip)) return false;
for (const ns of createDebug.names) if (matchesTemplate(name, ns)) return true;
return false;
}
/**
* Coerce `val`.
*
* @param {Mixed} val
* @return {Mixed}
* @api private
*/
function coerce(val) {
if (val instanceof Error) return val.stack || val.message;
return val;
}
/**
* XXX DO NOT USE. This is a temporary stub function.
* XXX It WILL be removed in the next major release.
*/
function destroy() {
console.warn("Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.");
}
createDebug.enable(createDebug.load());
return createDebug;
}
module.exports = setup;
}));
//#endregion
//#region src/p2p/commands/types.ts
var import_browser = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
/**
* This is the web browser implementation of `debug()`.
*/
exports.formatArgs = formatArgs;
exports.save = save;
exports.load = load;
exports.useColors = useColors;
exports.storage = localstorage();
exports.destroy = (() => {
let warned = false;
return () => {
if (!warned) {
warned = true;
console.warn("Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.");
}
};
})();
/**
* Colors.
*/
exports.colors = [
"#0000CC",
"#0000FF",
"#0033CC",
"#0033FF",
"#0066CC",
"#0066FF",
"#0099CC",
"#0099FF",
"#00CC00",
"#00CC33",
"#00CC66",
"#00CC99",
"#00CCCC",
"#00CCFF",
"#3300CC",
"#3300FF",
"#3333CC",
"#3333FF",
"#3366CC",
"#3366FF",
"#3399CC",
"#3399FF",
"#33CC00",
"#33CC33",
"#33CC66",
"#33CC99",
"#33CCCC",
"#33CCFF",
"#6600CC",
"#6600FF",
"#6633CC",
"#6633FF",
"#66CC00",
"#66CC33",
"#9900CC",
"#9900FF",
"#9933CC",
"#9933FF",
"#99CC00",
"#99CC33",
"#CC0000",
"#CC0033",
"#CC0066",
"#CC0099",
"#CC00CC",
"#CC00FF",
"#CC3300",
"#CC3333",
"#CC3366",
"#CC3399",
"#CC33CC",
"#CC33FF",
"#CC6600",
"#CC6633",
"#CC9900",
"#CC9933",
"#CCCC00",
"#CCCC33",
"#FF0000",
"#FF0033",
"#FF0066",
"#FF0099",
"#FF00CC",
"#FF00FF",
"#FF3300",
"#FF3333",
"#FF3366",
"#FF3399",
"#FF33CC",
"#FF33FF",
"#FF6600",
"#FF6633",
"#FF9900",
"#FF9933",
"#FFCC00",
"#FFCC33"
];
/**
* Currently only WebKit-based Web Inspectors, Firefox >= v31,
* and the Firebug extension (any Firefox version) are known
* to support "%c" CSS customizations.
*
* TODO: add a `localStorage` variable to explicitly enable/disable colors
*/
function useColors() {
if (typeof window !== "undefined" && window.process && (window.process.type === "renderer" || window.process.__nwjs)) return true;
if (typeof navigator !== "undefined" && navigator.userAgent && navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/)) return false;
let m;
return typeof document !== "undefined" && document.documentElement && document.documentElement.style && document.documentElement.style.WebkitAppearance || typeof window !== "undefined" && window.console && (window.console.firebug || window.console.exception && window.console.table) || typeof navigator !== "undefined" && navigator.userAgent && (m = navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/)) && parseInt(m[1], 10) >= 31 || typeof navigator !== "undefined" && navigator.userAgent && navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/);
}
/**
* Colorize log arguments if enabled.
*
* @api public
*/
function formatArgs(args) {
args[0] = (this.useColors ? "%c" : "") + this.namespace + (this.useColors ? " %c" : " ") + args[0] + (this.useColors ? "%c " : " ") + "+" + module.exports.humanize(this.diff);
if (!this.useColors) return;
const c = "color: " + this.color;
args.splice(1, 0, c, "color: inherit");
let index = 0;
let lastC = 0;
args[0].replace(/%[a-zA-Z%]/g, (match) => {
if (match === "%%") return;
index++;
if (match === "%c") lastC = index;
});
args.splice(lastC, 0, c);
}
/**
* Invokes `console.debug()` when available.
* No-op when `console.debug` is not a "function".
* If `console.debug` is not available, falls back
* to `console.log`.
*
* @api public
*/
exports.log = console.debug || console.log || (() => {});
/**
* Save `namespaces`.
*
* @param {String} namespaces
* @api private
*/
function save(namespaces) {
try {
if (namespaces) exports.storage.setItem("debug", namespaces);
else exports.storage.removeItem("debug");
} catch (error) {}
}
/**
* Load `namespaces`.
*
* @return {String} returns the previously persisted debug modes
* @api private
*/
function load() {
let r;
try {
r = exports.storage.getItem("debug") || exports.storage.getItem("DEBUG");
} catch (error) {}
if (!r && typeof process !== "undefined" && "env" in process) r = process.env.DEBUG;
return r;
}
/**
* Localstorage attempts to return the localstorage.
*
* This is necessary because safari throws
* when a user disables cookies/localstorage
* and you attempt to access it.
*
* @return {LocalStorage}
* @api private
*/
function localstorage() {
try {
return localStorage;
} catch (error) {}
}
module.exports = require_common()(exports);
var { formatters } = module.exports;
/**
* Map %j to `JSON.stringify()`, since no Web Inspectors do that by default.
*/
formatters.j = function(v) {
try {
return JSON.stringify(v);
} catch (error) {
return "[UnexpectedJSONParseError]: " + error.message;
}
};
})))(), 1);
var PeerCommandType$1 = /* @__PURE__ */ function(PeerCommandType) {
PeerCommandType[PeerCommandType["SegmentsAnnouncement"] = 0] = "SegmentsAnnouncement";
PeerCommandType[PeerCommandType["SegmentRequest"] = 1] = "SegmentRequest";
PeerCommandType[PeerCommandType["SegmentData"] = 2] = "SegmentData";
PeerCommandType[PeerCommandType["SegmentDataSendingCompleted"] = 3] = "SegmentDataSendingCompleted";
PeerCommandType[PeerCommandType["SegmentAbsent"] = 4] = "SegmentAbsent";
PeerCommandType[PeerCommandType["CancelSegmentRequest"] = 5] = "CancelSegmentRequest";
return PeerCommandType;
}({});
//#endregion
//#region src/utils/utils.ts
function getPromiseWithResolvers() {
let resolve;
let reject;
return {
promise: new Promise((res, rej) => {
resolve = res;
reject = rej;
}),
resolve,
reject
};
}
function queueMicrotask(fn) {
Promise.resolve().then(fn);
}
function joinChunks(chunks, totalBytes) {
totalBytes ??= chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
const buffer = new Uint8Array(totalBytes);
let offset = 0;
for (const chunk of chunks) {
buffer.set(chunk, offset);
offset += chunk.byteLength;
}
return buffer;
}
function getRandomItem(items) {
return items[Math.floor(Math.random() * items.length)];
}
function getWeightedRandomItem(items, weightAccessor) {
if (items.length === 0) throw new Error("Cannot get item from empty array");
if (items.length === 1) return items[0];
let totalWeight = 0;
const weights = items.map((item) => {
const weight = weightAccessor(item);
totalWeight += weight;
return weight;
});
let randomWeight = Math.random() * totalWeight;
for (let i = 0; i < items.length; i++) {
randomWeight -= weights[i];
if (randomWeight <= 0) return items[i];
}
return items[items.length - 1];
}
function utf8ToUintArray(utf8String) {
return new TextEncoder().encode(utf8String);
}
function* arrayBackwards(arr) {
for (let i = arr.length - 1; i >= 0; i--) yield arr[i];
}
function isObject(item) {
return !!item && typeof item === "object" && !Array.isArray(item);
}
function isArray(item) {
return Array.isArray(item);
}
function filterUndefinedProps(obj) {
function filter(obj) {
if (isObject(obj)) {
const result = {};
Object.keys(obj).forEach((key) => {
if (obj[key] !== void 0) {
const value = filter(obj[key]);
if (value !== void 0) result[key] = value;
}
});
return result;
} else return obj;
}
return filter(obj);
}
function deepCopy(item) {
if (isArray(item)) return item.map((element) => deepCopy(element));
else if (isObject(item)) {
const copy = {};
for (const key of Object.keys(item)) copy[key] = deepCopy(item[key]);
return copy;
} else return item;
}
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
function overrideConfig(target, updates, defaults = {}) {
if (typeof target !== "object" || target === null || typeof updates !== "object" || updates === null) return target;
Object.keys(updates).forEach((key) => {
const keyStr = typeof key === "symbol" ? key.toString() : String(key);
if (key === "__proto__" || key === "constructor" || key === "prototype") throw new Error(`Attempt to modify restricted property '${keyStr}'`);
const updateValue = updates[key];
const defaultValue = defaults[key];
if (key in target) if (updateValue === void 0) target[key] = defaultValue === void 0 ? void 0 : defaultValue;
else target[key] = updateValue;
});
return target;
}
function mergeAndFilterConfig(options) {
const { defaultConfig, baseConfig = {}, specificStreamConfig = {} } = options;
const mergedConfig = deepCopy({
...defaultConfig,
...baseConfig,
...specificStreamConfig
});
const keysOfT = Object.keys(defaultConfig);
const filteredConfig = {};
keysOfT.forEach((key) => {
if (key in mergedConfig) filteredConfig[key] = mergedConfig[key];
});
return filteredConfig;
}
//#endregion
//#region src/p2p/commands/binary-serialization.ts
var textEncoder = new TextEncoder();
var textDecoder = new TextDecoder("utf8");
var SerializedItem = /* @__PURE__ */ function(SerializedItem) {
SerializedItem[SerializedItem["Min"] = -1] = "Min";
SerializedItem[SerializedItem["Int"] = 0] = "Int";
SerializedItem[SerializedItem["SimilarIntArray"] = 1] = "SimilarIntArray";
SerializedItem[SerializedItem["String"] = 2] = "String";
SerializedItem[SerializedItem["Max"] = 3] = "Max";
return SerializedItem;
}({});
function getRequiredBytesForInt(num) {
if (num === 0) return 1;
const necessaryBits = Math.floor(Math.log2(Math.abs(num))) + 2;
return Math.ceil(necessaryBits / 8);
}
function intToBytes(num) {
const isNegative = num < 0;
const bytesAmountNumber = getRequiredBytesForInt(num);
const bytes = new Uint8Array(bytesAmountNumber);
num = Math.abs(num);
for (let i = 0; i < bytesAmountNumber; i++) {
const shift = 8 * (bytesAmountNumber - 1 - i);
bytes[i] = Math.floor(num / Math.pow(2, shift)) & 255;
}
if (isNegative) bytes[0] = bytes[0] | 128;
return bytes;
}
function bytesToInt(bytes) {
const byteLength = bytes.length;
const getNumberPart = (byte, i) => {
const shift = 8 * (byteLength - 1 - i);
return byte * Math.pow(2, shift);
};
let number = getNumberPart(bytes[0] & 127, 0);
for (let i = 1; i < byteLength; i++) number += getNumberPart(bytes[i], i);
if ((bytes[0] & 128) >> 7 !== 0) number = -number;
return number;
}
function serializeInt(num) {
const numBytes = intToBytes(num);
const numberMetadata = 0 | numBytes.length;
return new Uint8Array([numberMetadata, ...numBytes]);
}
function deserializeInt(bytes) {
if (bytes.length === 0) throw new Error("Buffer is too short");
const metadata = bytes[0];
if (metadata >> 4 !== 0) throw new Error("Trying to deserialize integer with invalid serialized item code");
const numberBytesLength = metadata & 15;
if (numberBytesLength === 0) throw new Error("Invalid integer: zero byte length");
if (numberBytesLength > 7) throw new Error("Invalid integer: byte length exceeds safe integer limit");
const start = 1;
const end = start + numberBytesLength;
if (bytes.length < end) throw new Error("Buffer is too short");
return {
number: bytesToInt(bytes.subarray(start, end)),
byteLength: numberBytesLength + 1
};
}
function serializeUniqueSimilarIntArray(numbers) {
const commonPartNumbersMap = /* @__PURE__ */ new Map();
for (const number of numbers) {
const diffByte = number & 255;
const common = number - diffByte;
const bytes = commonPartNumbersMap.get(common) ?? new ResizableUint8Array();
if (!bytes.length) commonPartNumbersMap.set(common, bytes);
bytes.push(diffByte);
}
const result = new ResizableUint8Array();
result.push([16, commonPartNumbersMap.size]);
for (const [commonPart, binaryArray] of commonPartNumbersMap) {
const { length } = binaryArray;
const commonPartWithLength = commonPart + (length & 255);
binaryArray.unshift(serializeInt(commonPartWithLength));
result.push(binaryArray.getBuffer());
}
return result.getBuffer();
}
function deserializeUniqueSimilarIntArray(bytes) {
if (bytes.length < 2) throw new Error("Buffer is too short");
const [codeByte, commonPartArraysAmount] = bytes;
if (codeByte >> 4 !== 1) throw new Error("Trying to deserialize similar int array with invalid serialized item code");
let offset = 2;
const originalIntArr = [];
for (let i = 0; i < commonPartArraysAmount; i++) {
const { number: commonPartWithLength, byteLength } = deserializeInt(bytes.subarray(offset));
offset += byteLength;
const arrayLength = commonPartWithLength & 255;
const actualLength = arrayLength === 0 ? 256 : arrayLength;
const commonPart = commonPartWithLength - arrayLength;
if (offset + actualLength > bytes.length) throw new Error("Malformed similar int array: buffer too short");
for (let j = 0; j < actualLength; j++) {
const diffPart = bytes[offset];
originalIntArr.push(commonPart + diffPart);
offset++;
}
}
return {
numbers: originalIntArr,
byteLength: offset
};
}
function serializeString(string) {
const encoded = textEncoder.encode(string);
const { length } = encoded;
if (length > 4095) throw new Error("String exceeds maximum length of 4095 bytes");
const bytes = new ResizableUint8Array();
bytes.push([32 | length >> 8 & 15, length & 255]);
bytes.push(encoded);
return bytes.getBuffer();
}
function deserializeString(bytes) {
if (bytes.length < 2) throw new Error("Buffer is too short");
const [codeByte, lengthByte] = bytes;
if (codeByte >> 4 !== 2) throw new Error("Trying to deserialize bytes (sting) with invalid serialized item code.");
const length = (codeByte & 15) << 8 | lengthByte;
if (bytes.length < length + 2) throw new Error("Malformed string: buffer too short");
const stringBytes = bytes.subarray(2, length + 2);
return {
string: textDecoder.decode(stringBytes),
byteLength: length + 2
};
}
var ResizableUint8Array = class {
#bytes = [];
#length = 0;
push(bytes) {
this.#addBytes(bytes, "end");
}
unshift(bytes) {
this.#addBytes(bytes, "start");
}
#addBytes(bytes, position) {
let bytesToAdd;
if (bytes instanceof Uint8Array) bytesToAdd = bytes;
else if (Array.isArray(bytes)) bytesToAdd = new Uint8Array(bytes);
else bytesToAdd = new Uint8Array([bytes]);
this.#length += bytesToAdd.length;
this.#bytes[position === "start" ? "unshift" : "push"](bytesToAdd);
}
getBytesChunks() {
return this.#bytes;
}
getBuffer() {
return joinChunks(this.#bytes, this.#length);
}
get length() {
return this.#length;
}
};
//#endregion
//#region src/p2p/commands/binary-command-creator.ts
var FRAME_PART_LENGTH = 4;
var commandFrameStart = stringToUtf8CodesBuffer("cstr", FRAME_PART_LENGTH);
var commandFrameEnd = stringToUtf8CodesBuffer("cend", FRAME_PART_LENGTH);
var commandDivFrameStart = stringToUtf8CodesBuffer("dstr", FRAME_PART_LENGTH);
var commandDivFrameEnd = stringToUtf8CodesBuffer("dend", FRAME_PART_LENGTH);
var startFrames = [commandFrameStart, commandDivFrameStart];
var endFrames = [commandFrameEnd, commandDivFrameEnd];
var commandFramesLength = commandFrameStart.length + commandFrameEnd.length;
function isCommandChunk(buffer) {
if (buffer.length < commandFramesLength) return false;
const { length } = commandFrameStart;
const bufferEndingToCompare = buffer.subarray(-length);
return startFrames.some((frame) => areBuffersEqual(buffer, frame, FRAME_PART_LENGTH)) && endFrames.some((frame) => areBuffersEqual(bufferEndingToCompare, frame, FRAME_PART_LENGTH));
}
function isFirstCommandChunk(buffer) {
if (buffer.length < commandFramesLength) return false;
return areBuffersEqual(buffer, commandFrameStart, FRAME_PART_LENGTH);
}
function isLastCommandChunk(buffer) {
if (buffer.length < commandFramesLength) return false;
return areBuffersEqual(buffer.subarray(-4), commandFrameEnd, FRAME_PART_LENGTH);
}
var BinaryCommandJoiningError = class extends Error {
type;
constructor(type) {
super();
this.type = type;
}
};
var BinaryCommandChunksJoiner = class {
#chunks = new ResizableUint8Array();
#status = "joining";
#onComplete;
constructor(onComplete) {
this.#onComplete = onComplete;
}
addCommandChunk(chunk) {
if (this.#status === "completed") return;
const isFirstChunk = isFirstCommandChunk(chunk);
if (!this.#chunks.length && !isFirstChunk) throw new BinaryCommandJoiningError("no-first-chunk");
if (this.#chunks.length && isFirstChunk) throw new BinaryCommandJoiningError("incomplete-joining");
this.#chunks.push(this.#unframeCommandChunk(chunk));
if (!isLastCommandChunk(chunk)) return;
this.#status = "completed";
this.#onComplete(this.#chunks.getBuffer());
}
#unframeCommandChunk(chunk) {
if (chunk.length < commandFramesLength) throw new Error("Command chunk is too short to unframe");
return chunk.subarray(FRAME_PART_LENGTH, chunk.length - FRAME_PART_LENGTH);
}
};
var BinaryCommandCreator = class {
#bytes = new ResizableUint8Array();
#resultBuffers = [];
#status = "creating";
#maxChunkLength;
constructor(commandType, maxChunkLength) {
this.#maxChunkLength = maxChunkLength;
this.#bytes.push(commandType);
}
addInteger(name, value) {
this.#bytes.push(name.charCodeAt(0));
const bytes = serializeInt(value);
this.#bytes.push(bytes);
}
addUniqueSimilarIntArr(name, arr) {
this.#bytes.push(name.charCodeAt(0));
const bytes = serializeUniqueSimilarIntArray(arr);
this.#bytes.push(bytes);
}
addString(name, string) {
this.#bytes.push(name.charCodeAt(0));
const bytes = serializeString(string);
this.#bytes.push(bytes);
}
complete() {
if (!this.#bytes.length) throw new Error("Buffer is empty");
if (this.#status === "completed") return;
this.#status = "completed";
const unframedBuffer = this.#bytes.getBuffer();
if (unframedBuffer.length + commandFramesLength <= this.#maxChunkLength) {
this.#resultBuffers.push(frameBuffer(unframedBuffer, commandFrameStart, commandFrameEnd));
return;
}
let chunksCount = Math.ceil(unframedBuffer.length / this.#maxChunkLength);
if (Math.ceil(unframedBuffer.length / chunksCount) + commandFramesLength > this.#maxChunkLength) chunksCount++;
for (const [i, chunk] of splitBufferToEqualChunks(unframedBuffer, chunksCount)) if (i === 0) this.#resultBuffers.push(frameBuffer(chunk, commandFrameStart, commandDivFrameEnd));
else if (i === chunksCount - 1) this.#resultBuffers.push(frameBuffer(chunk, commandDivFrameStart, commandFrameEnd));
else this.#resultBuffers.push(frameBuffer(chunk, commandDivFrameStart, commandDivFrameEnd));
}
getResultBuffers() {
if (this.#status === "creating" || !this.#resultBuffers.length) throw new Error("Command is not complete.");
return this.#resultBuffers;
}
};
function deserializeCommand(bytes) {
const [commandCode] = bytes;
const deserializedCommand = { c: commandCode };
let offset = 1;
while (offset < bytes.length) {
if (offset + 1 >= bytes.length) throw new Error("Malformed command buffer: truncated name/type header");
const name = String.fromCharCode(bytes[offset]);
offset++;
switch (getDataTypeFromByte(bytes[offset])) {
case SerializedItem.Int:
{
const { number, byteLength } = deserializeInt(bytes.subarray(offset));
deserializedCommand[name] = number;
offset += byteLength;
}
break;
case SerializedItem.SimilarIntArray:
{
const { numbers, byteLength } = deserializeUniqueSimilarIntArray(bytes.subarray(offset));
deserializedCommand[name] = numbers;
offset += byteLength;
}
break;
case SerializedItem.String:
{
const { string, byteLength } = deserializeString(bytes.subarray(offset));
deserializedCommand[name] = string;
offset += byteLength;
}
break;
}
}
return validateCommand(deserializedCommand);
}
function getDataTypeFromByte(byte) {
const typeCode = byte >> 4;
if (typeCode <= SerializedItem.Min || typeCode >= SerializedItem.Max) throw new Error("Not existing type");
return typeCode;
}
function stringToUtf8CodesBuffer(string, length) {
if (length && string.length !== length) throw new Error("Wrong string length");
const buffer = new Uint8Array(length ?? string.length);
for (let i = 0; i < string.length; i++) buffer[i] = string.charCodeAt(i);
return buffer;
}
function* splitBufferToEqualChunks(buffer, chunksCount) {
const chunkLength = Math.ceil(buffer.length / chunksCount);
for (let i = 0; i < chunksCount; i++) yield [i, buffer.subarray(i * chunkLength, (i + 1) * chunkLength)];
}
function frameBuffer(buffer, frameStart, frameEnd) {
const result = new Uint8Array(buffer.length + frameStart.length + frameEnd.length);
result.set(frameStart);
result.set(buffer, frameStart.length);
result.set(frameEnd, frameStart.length + buffer.length);
return result;
}
function areBuffersEqual(buffer1, buffer2, length) {
for (let i = 0; i < length; i++) if (buffer1[i] !== buffer2[i]) return false;
return true;
}
function validateCommand(command) {
switch (command.c) {
case PeerCommandType$1.SegmentsAnnouncement: return command;
case PeerCommandType$1.SegmentRequest:
assertNumberFields(command, "i", "r");
return command;
case PeerCommandType$1.SegmentData:
assertNumberFields(command, "i", "r", "s");
return command;
case PeerCommandType$1.SegmentAbsent:
case PeerCommandType$1.CancelSegmentRequest:
case PeerCommandType$1.SegmentDataSendingCompleted:
assertNumberFields(command, "i", "r");
return command;
default: throw new Error(`Unknown peer command type: ${String(command.c)}`);
}
}
function assertNumberFields(obj, ...fields) {
for (const field of fields) if (typeof obj[field] !== "number") throw new Error(`Expected number field "${field}", got ${typeof obj[field]}`);
}
//#endregion
//#region src/p2p/commands/commands.ts
function serializeSegmentAnnouncementCommand(command, maxChunkSize) {
const { c: commandCode, p: loadingByHttp, l: loaded } = command;
const creator = new BinaryCommandCreator(commandCode, maxChunkSize);
if (loaded?.length) creator.addUniqueSimilarIntArr("l", loaded);
if (loadingByHttp?.length) creator.addUniqueSimilarIntArr("p", loadingByHttp);
creator.complete();
return creator.getResultBuffers();
}
function serializePeerSegmentCommand(command, maxChunkSize) {
const creator = new BinaryCommandCreator(command.c, maxChunkSize);
creator.addInteger("i", command.i);
creator.addInteger("r", command.r);
creator.complete();
return creator.getResultBuffers();
}
function serializePeerSendSegmentCommand(command, maxChunkSize) {
const creator = new BinaryCommandCreator(command.c, maxChunkSize);
creator.addInteger("i", command.i);
creator.addInteger("s", command.s);
creator.addInteger("r", command.r);
creator.complete();
return creator.getResultBuffers();
}
function serializePeerSegmentRequestCommand(command, maxChunkSize) {
const creator = new BinaryCommandCreator(command.c, maxChunkSize);
creator.addInteger("i", command.i);
creator.addInteger("r", command.r);
if (command.b) creator.addInteger("b", command.b);
creator.complete();
return creator.getResultBuffers();
}
function serializePeerCommand(command, maxChunkSize) {
switch (command.c) {
case PeerCommandType$1.CancelSegmentRequest:
case PeerCommandType$1.SegmentAbsent:
case PeerCommandType$1.SegmentDataSendingCompleted: return serializePeerSegmentCommand(command, maxChunkSize);
case PeerCommandType$1.SegmentRequest: return serializePeerSegmentRequestCommand(command, maxChunkSize);
case PeerCommandType$1.SegmentsAnnouncement: return serializeSegmentAnnouncementCommand(command, maxChunkSize);
case PeerCommandType$1.SegmentData: return serializePeerSendSegmentCommand(command, maxChunkSize);
}
}
//#endregion
//#region src/p2p/commands/index.ts
var commands_exports = /* @__PURE__ */ __exportAll({
BinaryCommandChunksJoiner: () => BinaryCommandChunksJoiner,
BinaryCommandJoiningError: () => BinaryCommandJoiningError,
PeerCommandType: () => PeerCommandType$1,
deserializeCommand: () => deserializeCommand,
isCommandChunk: () => isCommandChunk,
serializePeerCommand: () => serializePeerCommand
});
//#endregion
//#region src/webtorrent/utils.ts
function getRTCError(event, fallbackMessage = "RTC error") {
const errorEvent = event;
if (errorEvent.error instanceof Error) return errorEvent.error;
const msg = errorEvent.error?.message ?? fallbackMessage;
return new Error(msg);
}
function getRTCErrorMessage(event, fallbackMessage = "RTC error") {
return getRTCError(event, fallbackMessage).message;
}
function isTerminalConnectionState(state) {
return state === "failed" || state === "closed" || state === "disconnected";
}
//#endregion
//#region src/webtorrent/data-channel-sender.ts
var MAX_BUFFERED_AMOUNT = 64 * 1024;
var DataChannelSender = class {
channel;
maxMessageSize;
#currentSendContext;
constructor(channel, maxMessageSize) {
this.channel = channel;
this.maxMessageSize = maxMessageSize;
}
async sendData(data, onChunkSent) {
if (this.#currentSendContext) throw new Error("Already sending data");
if (this.channel.readyState !== "open") throw new Error("Data channel is not open");
this.channel.bufferedAmountLowThreshold = MAX_BUFFERED_AMOUNT;
const { promise, resolve, reject } = getPromiseWithResolvers();
let offset = 0;
let isSettled = false;
const cleanup = () => {
if (isSettled) return false;
isSettled = true;
this.#currentSendContext = void 0;
this.channel.removeEventListener("bufferedamountlow", sendChunks);
this.channel.removeEventListener("closing", onClose);
this.channel.removeEventListener("close", onClose);
this.channel.removeEventListener("error", onError);
return true;
};
this.#currentSendContext = { cancel: () => {
if (cleanup()) reject(/* @__PURE__ */ new Error("Send cancelled"));
} };
const onClose = () => {
if (cleanup()) reject(/* @__PURE__ */ new Error("Data channel closed"));
};
const onError = (event) => {
if (!cleanup()) return;
const message = getRTCErrorMessage(event, "Unknown error");
reject(/* @__PURE__ */ new Error(`Data channel error: ${message}`));
};
const buffer = ArrayBuffer.isView(data) ? data.buffer : data;
const byteOffset = ArrayBuffer.isView(data) ? data.byteOffset : 0;
const sendChunks = () => {
if (isSettled) return;
if (this.channel.readyState !== "open") {
if (cleanup()) reject(/* @__PURE__ */ new Error(`Data channel not open (state: ${this.channel.readyState})`));
return;
}
try {
while (offset < data.byteLength) {
if (this.channel.bufferedAmount > MAX_BUFFERED_AMOUNT) return;
const bytesToSend = Math.min(this.maxMessageSize, data.byteLength - offset);
const chunk = new Uint8Array(buffer, byteOffset + offset, bytesToSend);
this.channel.send(chunk);
offset += bytesToSend;
onChunkSent?.(bytesToSend);
if (!this.#currentSendContext) return;
}
} catch (error) {
if (cleanup()) reject(error instanceof Error ? error : new Error(String(error)));
return;
}
if (cleanup()) resolve();
};
this.channel.addEventListener("bufferedamountlow", sendChunks);
this.channel.addEventListener("closing", onClose);
this.channel.addEventListener("close", onClose);
this.channel.addEventListener("error", onError);
sendChunks();
return promise;
}
cancel() {
this.#currentSendContext?.cancel();
}
};
//#endregion
//#region src/p2p/peer-protocol.ts
var logger = (0, import_browser.default)("p2pml-core:peer-protocol");
var PeerProtocol = class {
#commandChunks;
#dataChannelSender;
#uploadingRequestId;
#onChunkDownloaded;
#onChunkUploaded;
#channel;
#peerConfig;
#eventHandlers;
#peerId;
constructor(channel, peerConfig, eventHandlers, eventTarget, peerId) {
this.#channel = channel;
this.#peerConfig = peerConfig;
this.#eventHandlers = eventHandlers;
this.#peerId = peerId;
this.#dataChannelSender = new DataChannelSender(channel, peerConfig.webRtcMaxMessageSize);
this.#onChunkDownloaded = eventTarget.getEventDispatcher("onChunkDownloaded");
this.#onChunkUploaded = eventTarget.getEventDispatcher("onChunkUploaded");
if (channel.binaryType !== "arraybuffer") throw new Error(`Expected binaryType "arraybuffer", got "${channel.binaryType}"`);
channel.addEventListener("message", this.#onMessageReceived);
}
#onMessageReceived = (event) => {
try {
const data = new Uint8Array(event.data);
if (isCommandChunk(data)) this.#receivingCommandBytes(data);
else {
this.#eventHandlers.onSegmentChunkReceived(data);
this.#onChunkDownloaded(data.byteLength, "p2p", this.#peerId, this.#peerConfig.streamType, this.#peerConfig.infoHash);
}
} catch (err) {
logger("error handling data channel message: %O", err);
this.#eventHandlers.onProtocolError(err);
}
};
sendCommand(command) {
if (this.#channel.readyState !== "open") throw new Error(`cannot send command ${command.c} (channel state: ${this.#channel.readyState})`);
const binaryCommandBuffers = serializePeerCommand(command, this.#peerConfig.webRtcMaxMessageSize);
for (const buffer of binaryCommandBuffers) this.#channel.send(buffer);
}
stopUploadingSegmentData() {
this.#dataChannelSender.cancel();
this.#uploadingRequestId = void 0;
}
getUploadingRequestId() {
return this.#uploadingRequestId;
}
async splitSegmentDataToChunksAndUploadAsync(data, requestId) {
if (this.#uploadingRequestId !== void 0) throw new Error(`Some segment data is already uploading.`);
this.#uploadingRequestId = requestId;
try {
await this.#dataChannelSender.sendData(data, (chunkSize) => {
this.#onChunkUploaded(chunkSize, this.#peerId, this.#peerConfig.streamType, this.#peerConfig.infoHash);
});
} finally {
if (this.#uploadingRequestId === requestId) this.#uploadingRequestId = void 0;
}
}
#receivingCommandBytes(buffer) {
this.#commandChunks ??= new BinaryCommandChunksJoiner((commandBuffer) => {
this.#commandChunks = void 0;
let command;
try {
command = deserializeCommand(commandBuffer);
} catch (err) {
logger("error deserializing command: %O", err);
this.#eventHandlers.onProtocolError(err);
return;
}
this.#eventHandlers.onCommandReceived(command);
});
try {
this.#comm