Files
gopeed/pkg/download/engine/inject/stream/stream.js
T

965 lines
28 KiB
JavaScript

(function () {
const createBlobObjectURL = globalThis.__gopeed_create_blob_object_url;
const revokeObjectURL = globalThis.__gopeed_revoke_object_url;
const fetchOpen = globalThis.__gopeed_fetch_open;
const fetchRead = globalThis.__gopeed_fetch_read;
const fetchClose = globalThis.__gopeed_fetch_close;
const fetchAbort = globalThis.__gopeed_fetch_abort;
if (typeof globalThis.ReadableStream === "undefined") {
class ReadableStreamDefaultController {
constructor(stream) {
this._stream = stream;
}
enqueue(chunk) {
this._stream._enqueue(chunk);
}
close() {
this._stream._close();
}
error(err) {
this._stream._error(err);
}
}
class ReadableStreamDefaultReader {
constructor(stream) {
this._stream = stream;
}
read() {
return this._stream._read();
}
cancel(reason) {
if (!this._stream) {
return Promise.resolve();
}
return this._stream.cancel(reason);
}
releaseLock() {
if (this._stream) {
this._stream._reader = null;
this._stream.locked = false;
this._stream = null;
}
}
}
class ReadableStream {
constructor(source = {}) {
this._source = source;
this._queue = [];
this._waiters = [];
this._closed = false;
this._errored = null;
this._reader = null;
this._pulling = false;
this.locked = false;
this._controller = new ReadableStreamDefaultController(this);
if (typeof source.start === "function") {
source.start(this._controller);
}
}
_enqueue(chunk) {
if (this._closed || this._errored) {
return;
}
if (this._waiters.length > 0) {
const waiter = this._waiters.shift();
waiter.resolve({ done: false, value: chunk });
return;
}
this._queue.push(chunk);
}
_close() {
this._closed = true;
while (this._waiters.length > 0) {
this._waiters.shift().resolve({ done: true, value: undefined });
}
}
_error(err) {
this._errored = err || new Error("ReadableStream error");
while (this._waiters.length > 0) {
this._waiters.shift().reject(this._errored);
}
}
_read() {
if (this._errored) {
return Promise.reject(this._errored);
}
this._markBodyUsed();
if (this._queue.length > 0) {
return Promise.resolve({ done: false, value: this._queue.shift() });
}
if (this._closed) {
return Promise.resolve({ done: true, value: undefined });
}
this._maybePull();
if (this._queue.length > 0) {
return Promise.resolve({ done: false, value: this._queue.shift() });
}
if (this._closed) {
return Promise.resolve({ done: true, value: undefined });
}
return new Promise((resolve, reject) => {
this._waiters.push({ resolve, reject });
this._maybePull();
});
}
_maybePull() {
if (this._closed || this._errored || this._pulling) {
return;
}
if (this._queue.length > 0) {
return;
}
if (!this._source || typeof this._source.pull !== "function") {
return;
}
this._pulling = true;
Promise.resolve(this._source.pull(this._controller))
.catch((err) => {
this._error(err);
})
.finally(() => {
this._pulling = false;
if (!this._closed && !this._errored && this._queue.length === 0 && this._waiters.length > 0) {
this._maybePull();
}
});
}
_markBodyUsed() {
if (typeof this.__gopeedMarkBodyUsed === "function") {
this.__gopeedMarkBodyUsed();
}
}
getReader() {
if (this.locked) {
throw new TypeError("ReadableStream is locked");
}
this.locked = true;
this._reader = new ReadableStreamDefaultReader(this);
return this._reader;
}
cancel(reason) {
this._markBodyUsed();
if (this._source && typeof this._source.cancel === "function") {
return Promise.resolve(this._source.cancel(reason)).then(() => {
this._close();
});
}
this._close();
return Promise.resolve();
}
async pipeTo(dest) {
const reader = this.getReader();
const writer = dest.getWriter();
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
await writer.close();
break;
}
await writer.write(value);
}
} catch (err) {
await writer.abort(err);
throw err;
} finally {
reader.releaseLock();
writer.releaseLock();
}
}
}
globalThis.ReadableStream = ReadableStream;
}
if (typeof globalThis.WritableStream === "undefined") {
class WritableStreamDefaultWriter {
constructor(stream) {
if (stream.locked) {
throw new TypeError("WritableStream is locked");
}
this._stream = stream;
this._stream.locked = true;
}
write(chunk) {
return this._stream._write(chunk);
}
close() {
return this._stream._close();
}
abort(reason) {
return this._stream._abort(reason);
}
releaseLock() {
if (this._stream) {
this._stream.locked = false;
this._stream = null;
}
}
}
class WritableStream {
constructor(sink = {}) {
this._sink = sink;
this._observers = [];
this._state = "writable";
this.locked = false;
}
_targets() {
return [this._sink, ...this._observers];
}
_write(chunk) {
if (this._state !== "writable") {
return Promise.reject(new TypeError("WritableStream is not writable"));
}
let chain = Promise.resolve();
for (const target of this._targets()) {
if (target && typeof target.write === "function") {
chain = chain.then(() => target.write(chunk));
}
}
return chain;
}
_close() {
if (this._state !== "writable") {
return Promise.reject(new TypeError("WritableStream is not writable"));
}
this._state = "closed";
let chain = Promise.resolve();
for (const target of this._targets()) {
if (target && typeof target.close === "function") {
chain = chain.then(() => target.close());
}
}
return chain;
}
_abort(reason) {
if (this._state === "errored") {
return Promise.resolve();
}
this._state = "errored";
let chain = Promise.resolve();
for (const target of this._targets()) {
if (target && typeof target.abort === "function") {
chain = chain.then(() => target.abort(reason));
}
}
return chain;
}
_addObserver(observer) {
this._observers.push(observer);
}
getWriter() {
return new WritableStreamDefaultWriter(this);
}
}
globalThis.WritableStream = WritableStream;
}
if (typeof globalThis.TransformStream === "undefined") {
class TransformStream {
constructor(transformer = {}) {
let readableController;
this.readable = new ReadableStream({
start(controller) {
readableController = controller;
}
});
this.writable = new WritableStream({
async write(chunk) {
if (typeof transformer.transform === "function") {
await transformer.transform(chunk, readableController);
return;
}
readableController.enqueue(chunk);
},
async close() {
if (typeof transformer.flush === "function") {
await transformer.flush(readableController);
}
readableController.close();
},
async abort(reason) {
if (typeof transformer.abort === "function") {
await transformer.abort(reason);
}
readableController.error(reason || new Error("TransformStream aborted"));
}
});
}
}
globalThis.TransformStream = TransformStream;
}
if (typeof globalThis.ByteLengthQueuingStrategy === "undefined") {
globalThis.ByteLengthQueuingStrategy = class ByteLengthQueuingStrategy {
constructor({ highWaterMark }) {
this.highWaterMark = highWaterMark;
}
size(chunk) {
if (typeof chunk === "string") {
return chunk.length;
}
if (chunk && typeof chunk.byteLength === "number") {
return chunk.byteLength;
}
return 1;
}
};
}
if (typeof globalThis.CountQueuingStrategy === "undefined") {
globalThis.CountQueuingStrategy = class CountQueuingStrategy {
constructor({ highWaterMark }) {
this.highWaterMark = highWaterMark;
}
size() {
return 1;
}
};
}
function toUint8Array(chunk) {
if (chunk == null) {
return new Uint8Array(0);
}
if (chunk instanceof Uint8Array) {
return chunk;
}
if (typeof chunk === "string") {
return new TextEncoder().encode(chunk);
}
if (chunk instanceof ArrayBuffer) {
return new Uint8Array(chunk);
}
if (typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView && ArrayBuffer.isView(chunk)) {
return new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
}
if (typeof Blob !== "undefined" && chunk instanceof Blob) {
if (chunk._buffer instanceof Uint8Array) {
return chunk._buffer;
}
}
return new Uint8Array(0);
}
function createBodyReadableStream(owner) {
return new ReadableStream({
start(controller) {
Promise.resolve().then(async () => {
owner.bodyUsed = true;
if (owner._noBody) {
controller.close();
return;
}
if (owner._bodyArrayBuffer) {
controller.enqueue(toUint8Array(owner._bodyArrayBuffer));
controller.close();
return;
}
if (owner._bodyBlob) {
const data = await owner._bodyBlob.arrayBuffer();
controller.enqueue(new Uint8Array(data));
controller.close();
return;
}
if (owner._bodyText != null) {
controller.enqueue(toUint8Array(owner._bodyText));
controller.close();
return;
}
if (owner._bodyInit != null) {
controller.enqueue(toUint8Array(owner._bodyInit));
}
controller.close();
}).catch((err) => {
controller.error(err);
});
}
});
}
async function readAllFromStream(stream, asText) {
if (!stream) {
return asText ? "" : new Uint8Array(0);
}
const reader = stream.getReader();
const chunks = [];
let total = 0;
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
const chunk = toUint8Array(value);
chunks.push(chunk);
total += chunk.byteLength;
}
} finally {
reader.releaseLock();
}
const merged = new Uint8Array(total);
let offset = 0;
for (const chunk of chunks) {
merged.set(chunk, offset);
offset += chunk.byteLength;
}
if (asText) {
return new TextDecoder().decode(merged);
}
return merged;
}
function attachResponseStreaming(response, stream) {
response.__gopeedBodyStream = stream;
response.__gopeedBodyConsumed = false;
const ensureUnused = () => {
if (response.__gopeedBodyConsumed) {
throw new TypeError("Already read");
}
};
const markBodyUsed = () => {
ensureUnused();
response.__gopeedBodyConsumed = true;
response.bodyUsed = true;
};
if (stream) {
stream.__gopeedMarkBodyUsed = () => {
if (!response.__gopeedBodyConsumed) {
response.__gopeedBodyConsumed = true;
response.bodyUsed = true;
}
};
}
response.text = async function () {
ensureUnused();
markBodyUsed();
return readAllFromStream(stream, true);
};
response.arrayBuffer = async function () {
ensureUnused();
markBodyUsed();
const bytes = await readAllFromStream(stream, false);
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
};
response.blob = async function () {
ensureUnused();
markBodyUsed();
const contentType = this.headers && this.headers.get ? (this.headers.get("content-type") || "") : "";
// Response.blob() is a materializing body consumer. Keeping the live
// response stream behind an empty one-shot Blob makes retry, range and a
// second object-URL reader silently produce an empty file.
const bytes = await readAllFromStream(stream, false);
return new Blob([bytes], { type: contentType });
};
response.json = async function () {
const text = await this.text();
return JSON.parse(text);
};
return response;
}
if (typeof globalThis.Response === "function") {
const responseProto = globalThis.Response.prototype;
const bodyDescriptor = Object.getOwnPropertyDescriptor(responseProto, "body");
if (!bodyDescriptor || typeof bodyDescriptor.get !== "function") {
Object.defineProperty(responseProto, "body", {
configurable: true,
enumerable: true,
get() {
if (this.__gopeedBodyStream) {
return this.__gopeedBodyStream;
}
if (!this.__gopeedBodyStream) {
this.__gopeedBodyStream = createBodyReadableStream(this);
}
return this.__gopeedBodyStream;
}
});
}
}
const blobReaders = new Map();
function getValueTypeName(value) {
if (value === null) {
return "null";
}
if (value === undefined) {
return "undefined";
}
if (value && value.constructor && typeof value.constructor.name === "string" && value.constructor.name) {
return value.constructor.name;
}
return typeof value;
}
function toReadableStreamReader(value, sourceLabel) {
if (value && typeof value.getReader === "function") {
return value.getReader();
}
throw new TypeError(sourceLabel + " must return a ReadableStream, got " + getValueTypeName(value));
}
async function openBlobReadable(blob, request) {
const { offset, end } = request;
const sliced = blob.slice(offset, end >= offset ? end + 1 : undefined);
if (sliced && typeof sliced.stream === "function") {
try {
const stream = sliced.stream();
if (stream && typeof stream.getReader === "function") {
return stream;
}
} catch (_) {
}
}
if (sliced && typeof sliced.arrayBuffer === "function") {
const buffer = await sliced.arrayBuffer();
return new ReadableStream({
start(controller) {
controller.enqueue(new Uint8Array(buffer));
controller.close();
},
});
}
throw new TypeError("Blob source cannot be converted to a ReadableStream");
}
function describeObjectURLValue(value) {
if (value instanceof Blob) {
return {
kind: "blob",
value,
};
}
if (typeof value === "function") {
return {
kind: "opener",
openReadable: value,
sourceLabel: "gopeed.runtime.blob.createObjectURL opener function",
};
}
return {
kind: "other",
value,
};
}
function releaseReader(reader, reason) {
if (!reader) {
return;
}
try {
if (reason !== undefined && typeof reader.cancel === "function") {
const result = reader.cancel(reason);
if (result && typeof result.catch === "function") {
result.catch(function () {});
}
}
} catch (_) {
}
if (typeof reader.releaseLock === "function") {
try {
reader.releaseLock();
} catch (_) {
}
}
}
function normalizeBlobObjectURLOptions(value, options, defaultRange) {
const normalized = {
contentType: "",
size: 0,
range: !!defaultRange,
};
if (options && typeof options === "object") {
if (typeof options.contentType === "string") {
normalized.contentType = options.contentType;
}
if (Number.isFinite(Number(options.size)) && Number(options.size) > 0) {
normalized.size = Number(options.size);
}
if (typeof options.range === "boolean") {
normalized.range = options.range;
}
}
if (!normalized.contentType && value && typeof value.type === "string") {
normalized.contentType = value.type;
}
if (!normalized.size && value) {
if (Number.isFinite(Number(value.size)) && Number(value.size) > 0) {
normalized.size = Number(value.size);
} else if (value._buffer && Number.isFinite(Number(value._buffer.byteLength))) {
normalized.size = Number(value._buffer.byteLength);
}
}
return normalized;
}
function validateBlobObjectURLOptions(options) {
if (options.range && !(Number.isFinite(Number(options.size)) && Number(options.size) > 0)) {
throw new TypeError("gopeed.runtime.blob.createObjectURL options.range requires a positive options.size");
}
}
function normalizeOpenRequest(request) {
const offset = Math.max(0, Number(request && request.offset) || 0);
const endValue = Number(request && request.end);
const end = Number.isFinite(endValue) ? endValue : -1;
return { offset, end };
}
function yieldBlobPipeTask() {
return new Promise((resolve) => {
if (typeof setTimeout === "function") {
setTimeout(resolve, 0);
} else {
Promise.resolve().then(resolve);
}
});
}
function createPendingBlobReader(openReadable, request) {
let activeReader;
let ready;
function ensureReader() {
if (ready) {
return ready;
}
ready = (async function () {
const source = await openReadable(normalizeOpenRequest(request));
activeReader = toReadableStreamReader(source, "gopeed.runtime.blob.createObjectURL opener function");
return activeReader;
})();
return ready;
}
return {
async read() {
const reader = await ensureReader();
return reader.read();
},
cancel(reason) {
return ensureReader().then(function (reader) {
return reader.cancel(reason);
}, function () {});
},
releaseLock() {
if (!ready) {
return;
}
return ready.then(function (reader) {
return reader.releaseLock();
}, function () {});
}
};
}
globalThis.__gopeed_blob_open_source = function (openReadable, request) {
if (typeof openReadable !== "function") {
throw new TypeError("blob opener must be callable");
}
const id = crypto.randomUUID ? crypto.randomUUID() : String(Date.now()) + "-" + String(Math.random());
blobReaders.set(id, createPendingBlobReader(openReadable, request));
return id;
};
globalThis.__gopeed_blob_read_source = async function (id, chunkSize) {
const reader = blobReaders.get(id);
if (!reader) {
return null;
}
const { done, value } = await reader.read();
if (done) {
blobReaders.delete(id);
releaseReader(reader);
return null;
}
const chunk = value instanceof Uint8Array ? value : new Uint8Array(value);
const size = Math.max(1, Number(chunkSize) || chunk.byteLength);
if (chunk.byteLength <= size) {
return chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength);
}
const head = chunk.slice(0, size);
let offset = size;
blobReaders.set(id, {
read() {
if (offset >= chunk.byteLength) {
return reader.read();
}
const next = chunk.slice(offset, Math.min(offset + size, chunk.byteLength));
offset += next.byteLength;
return Promise.resolve({ done: false, value: next });
},
cancel(reason) {
return reader.cancel(reason);
},
releaseLock() {
return reader.releaseLock();
}
});
return head.buffer.slice(head.byteOffset, head.byteOffset + head.byteLength);
};
globalThis.__gopeed_blob_close_source = function (id) {
const reader = blobReaders.get(id);
if (!reader) {
return;
}
blobReaders.delete(id);
releaseReader(reader, "blob request closed");
};
const blobPipeReaderStates = new Map();
function releaseBlobPipeReader(state) {
if (!state || state.released) {
return;
}
state.released = true;
if (state.reader && typeof state.reader.releaseLock === "function") {
try {
state.reader.releaseLock();
} catch (_) {
}
}
if (blobPipeReaderStates.get(state.pipeId) === state) {
blobPipeReaderStates.delete(state.pipeId);
}
}
function finishBlobPipeReader(state, cancel, reason) {
if (!state) {
return Promise.resolve();
}
if (cancel) {
state.cancelRequested = true;
if (reason !== undefined) {
state.cancelReason = reason;
}
}
if (state.cleanupPromise) {
return state.cleanupPromise;
}
if (!state.reader) {
// A cancellation may arrive while the asynchronous opener is pending.
// Keep the state until the reader exists so it can still be canceled.
if (!state.cancelRequested || state.openSettled) {
releaseBlobPipeReader(state);
}
return Promise.resolve();
}
state.cleanupPromise = (async function () {
try {
if (state.cancelRequested && typeof state.reader.cancel === "function") {
await state.reader.cancel(state.cancelReason);
}
} catch (_) {
} finally {
releaseBlobPipeReader(state);
}
})();
return state.cleanupPromise;
}
globalThis.__gopeed_blob_cancel_pipe_source = function (pipeId, reason) {
const state = blobPipeReaderStates.get(pipeId);
if (!state) {
return;
}
// finishBlobPipeReader invokes reader.cancel() synchronously up to its
// first await. Do not return the cleanup Promise: Go only needs to ensure
// cancellation has started before releasing the source session.
finishBlobPipeReader(state, true, reason).catch(function () {});
};
globalThis.__gopeed_blob_pipe_source = function (openReadable, request, pipeId) {
if (typeof openReadable !== "function") {
throw new TypeError("blob opener must be callable");
}
const state = {
pipeId,
reader: null,
cancelRequested: false,
cancelReason: "blob pipe closed",
cleanupPromise: null,
openSettled: false,
released: false,
};
blobPipeReaderStates.set(pipeId, state);
const startPipe = async function () {
try {
const source = await openReadable(normalizeOpenRequest(request));
state.openSettled = true;
state.reader = toReadableStreamReader(source, "gopeed.runtime.blob.createObjectURL opener function");
if (state.cancelRequested) {
await finishBlobPipeReader(state, true, state.cancelReason);
return;
}
while (true) {
const { done, value } = await state.reader.read();
if (done) {
globalThis.__gopeed_blob_pipe_close(pipeId);
return;
}
const chunk = value instanceof Uint8Array ? value : new Uint8Array(value);
const buffer = chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength);
if (!globalThis.__gopeed_blob_pipe_chunk(pipeId, buffer)) {
state.cancelRequested = true;
state.cancelReason = "blob pipe closed";
return;
}
await yieldBlobPipeTask();
}
} catch (error) {
if (!state.cancelRequested) {
globalThis.__gopeed_blob_pipe_error(pipeId, error && error.stack ? error.stack : String(error));
}
} finally {
state.openSettled = true;
await finishBlobPipeReader(state, state.cancelRequested, state.cancelReason);
}
};
if (typeof setTimeout === "function") {
setTimeout(startPipe, 0);
} else {
Promise.resolve().then(startPipe);
}
};
globalThis.__gopeed_blob_create_object_url = function (value, options) {
const described = describeObjectURLValue(value);
if (described.kind === "blob") {
const blob = described.value;
const normalized = normalizeBlobObjectURLOptions(blob, options, Number(blob.size) > 0);
validateBlobObjectURLOptions(normalized);
const opener = async (request) => openBlobReadable(blob, request);
return createBlobObjectURL(opener, normalized);
}
if (described.kind === "opener") {
const normalized = normalizeBlobObjectURLOptions(null, options, false);
validateBlobObjectURLOptions(normalized);
return createBlobObjectURL(described.openReadable, normalized);
}
throw new TypeError("Unsupported object type for gopeed.runtime.blob.createObjectURL: " + getValueTypeName(described.value) + ". Expected Blob or opener function");
};
globalThis.__gopeed_blob_revoke_object_url = function (url) {
if (typeof url === "string") {
revokeObjectURL(url);
}
};
const originalFetch = typeof globalThis.fetch === "function"
? globalThis.fetch.bind(globalThis)
: null;
if (typeof fetchOpen === "function") {
globalThis.fetch = async function (input, init) {
const request = new Request(input, init);
if (originalFetch && (request.method === "HEAD" || request.redirect === "manual" || request.redirect === "error")) {
return originalFetch(input, init);
}
let body = null;
if (request._bodyFormData) {
body = request._bodyFormData;
} else if (request._bodyArrayBuffer) {
body = request._bodyArrayBuffer;
} else if (request._bodyBlob) {
body = await request._bodyBlob.arrayBuffer();
} else if (request._bodyInit != null && typeof request._bodyInit === "object") {
body = request._bodyInit;
} else if (request._bodyText != null) {
body = request._bodyText;
} else if (request._bodyInit != null) {
body = request._bodyInit;
}
const headers = [];
request.headers.forEach((value, key) => {
headers.push([key, value]);
});
let meta;
try {
meta = await fetchOpen({
url: request.url,
method: request.method,
headers,
body,
redirect: request.redirect,
credentials: request.credentials
});
} catch (error) {
throw error instanceof Error ? error : new TypeError(String(error));
}
const stream = new ReadableStream({
async pull(controller) {
let chunk;
try {
chunk = await fetchRead(meta.id, 64 * 1024);
} catch (error) {
fetchClose(meta.id);
controller.error(error instanceof Error ? error : new TypeError(String(error)));
return;
}
if (chunk == null) {
fetchClose(meta.id);
controller.close();
return;
}
const bytes = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
if (bytes.byteLength === 0) {
fetchClose(meta.id);
controller.close();
return;
}
controller.enqueue(bytes);
},
cancel(reason) {
fetchAbort(meta.id, reason == null ? "" : String(reason));
}
});
const response = new Response(null, {
status: meta.status,
statusText: meta.statusText,
headers: meta.headers,
url: meta.url
});
return attachResponseStreaming(response, stream);
};
globalThis.fetch.__gopeedOriginalFetch = originalFetch;
}
})();