nstdlib-nightly
Version:
Node.js standard library converted to runtime-agnostic ES modules.
460 lines (400 loc) • 13.3 kB
JavaScript
// Source: https://github.com/nodejs/node/blob/65eff1eb/lib/internal/blob.js
import {
createBlob as _createBlob,
createBlobFromFilePath as _createBlobFromFilePath,
concat,
getDataObject,
} from "nstdlib/stub/binding/blob";
import { kMaxLength } from "nstdlib/stub/binding/buffer";
import { TextDecoder, TextEncoder } from "nstdlib/lib/internal/encoding";
import { URL } from "nstdlib/lib/internal/url";
import {
markTransferMode,
kClone,
kDeserialize,
} from "nstdlib/lib/internal/worker/js_transferable";
import {
isAnyArrayBuffer,
isArrayBufferView,
} from "nstdlib/lib/internal/util/types";
import {
createDeferredPromise,
customInspectSymbol as kInspect,
kEmptyObject,
kEnumerableProperty,
lazyDOMException,
} from "nstdlib/lib/internal/util";
import { inspect } from "nstdlib/lib/internal/util/inspect";
import { codes as __codes__ } from "nstdlib/lib/internal/errors";
import { validateDictionary } from "nstdlib/lib/internal/validators";
import { CountQueuingStrategy } from "nstdlib/lib/internal/webstreams/queuingstrategies";
import { queueMicrotask } from "nstdlib/lib/internal/process/task_queues";
import { EOL } from "nstdlib/lib/internal/constants";
import * as __hoisted_internal_webstreams_readablestream__ from "nstdlib/lib/internal/webstreams/readablestream";
const {
ERR_BUFFER_TOO_LARGE,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
ERR_INVALID_STATE,
ERR_INVALID_THIS,
} = __codes__;
const kHandle = Symbol("kHandle");
const kType = Symbol("kType");
const kLength = Symbol("kLength");
const kNotCloneable = Symbol("kNotCloneable");
const disallowedTypeCharacters = /[^\u{0020}-\u{007E}]/u;
let ReadableStream;
const enc = new TextEncoder();
let dec;
// Yes, lazy loading is annoying but because of circular
// references between the url, internal/blob, and buffer
// modules, lazy loading here makes sure that things work.
function lazyReadableStream(options) {
// eslint-disable-next-line no-global-assign
ReadableStream ??=
__hoisted_internal_webstreams_readablestream__.ReadableStream;
return new ReadableStream(options);
}
function isBlob(object) {
return object?.[kHandle] !== undefined;
}
function getSource(source, endings) {
if (isBlob(source)) return [source.size, source[kHandle]];
if (isAnyArrayBuffer(source)) {
source = new Uint8Array(source);
} else if (!isArrayBufferView(source)) {
source = `${source}`;
if (endings === "native")
source = RegExp.prototype[Symbol.replace].call(/\n|\r\n/g, source, EOL);
source = enc.encode(source);
}
// We copy into a new Uint8Array because the underlying
// BackingStores are going to be detached and owned by
// the Blob.
const { buffer, byteOffset, byteLength } = source;
const slice = buffer.slice(byteOffset, byteOffset + byteLength);
return [byteLength, new Uint8Array(slice)];
}
class Blob {
/**
* @typedef {string|ArrayBuffer|ArrayBufferView|Blob} SourcePart
*/
/**
* @param {SourcePart[]} [sources]
* @param {{
* endings? : string,
* type? : string,
* }} [options]
* @constructs {Blob}
*/
constructor(sources = [], options) {
markTransferMode(this, true, false);
if (
sources === null ||
typeof sources[Symbol.iterator] !== "function" ||
typeof sources === "string"
) {
throw new ERR_INVALID_ARG_TYPE("sources", "a sequence", sources);
}
validateDictionary(options, "options");
let { type = "", endings = "transparent" } = options ?? kEmptyObject;
endings = `${endings}`;
if (endings !== "transparent" && endings !== "native")
throw new ERR_INVALID_ARG_VALUE("options.endings", endings);
let length = 0;
const sources_ = Array.from(sources, (source) => {
const { 0: len, 1: src } = getSource(source, endings);
length += len;
return src;
});
if (length > kMaxLength) throw new ERR_BUFFER_TOO_LARGE(kMaxLength);
this[kHandle] = _createBlob(sources_, length);
this[kLength] = length;
type = `${type}`;
this[kType] =
RegExp.prototype.exec.call(disallowedTypeCharacters, type) !== null
? ""
: String.prototype.toLowerCase.call(type);
}
[kInspect](depth, options) {
if (depth < 0) return this;
const opts = {
...options,
depth: options.depth == null ? null : options.depth - 1,
};
return `Blob ${inspect(
{
size: this.size,
type: this.type,
},
opts,
)}`;
}
[kClone]() {
if (this[kNotCloneable]) {
// We do not currently allow file-backed Blobs to be cloned or passed across
// worker threads.
throw new ERR_INVALID_STATE.TypeError(
"File-backed Blobs are not cloneable",
);
}
const handle = this[kHandle];
const type = this[kType];
const length = this[kLength];
return {
data: { handle, type, length },
deserializeInfo: "internal/blob:Blob",
};
}
[kDeserialize]({ handle, type, length }) {
this[kHandle] = handle;
this[kType] = type;
this[kLength] = length;
}
/**
* @readonly
* @type {string}
*/
get type() {
if (!isBlob(this)) throw new ERR_INVALID_THIS("Blob");
return this[kType];
}
/**
* @readonly
* @type {number}
*/
get size() {
if (!isBlob(this)) throw new ERR_INVALID_THIS("Blob");
return this[kLength];
}
/**
* @param {number} [start]
* @param {number} [end]
* @param {string} [contentType]
* @returns {Blob}
*/
slice(start = 0, end = this[kLength], contentType = "") {
if (!isBlob(this)) throw new ERR_INVALID_THIS("Blob");
if (start < 0) {
start = Math.max(this[kLength] + start, 0);
} else {
start = Math.min(start, this[kLength]);
}
if (end < 0) {
end = Math.max(this[kLength] + end, 0);
} else {
end = Math.min(end, this[kLength]);
}
contentType = `${contentType}`;
if (
RegExp.prototype.exec.call(disallowedTypeCharacters, contentType) !== null
) {
contentType = "";
} else {
contentType = String.prototype.toLowerCase.call(contentType);
}
const span = Math.max(end - start, 0);
return createBlob(
this[kHandle].slice(start, start + span),
span,
contentType,
);
}
/**
* @returns {Promise<ArrayBuffer>}
*/
arrayBuffer() {
if (!isBlob(this)) return Promise.reject(new ERR_INVALID_THIS("Blob"));
const { promise, resolve, reject } = createDeferredPromise();
const reader = this[kHandle].getReader();
const buffers = [];
const readNext = () => {
reader.pull((status, buffer) => {
if (status === 0) {
// EOS, concat & resolve
// buffer should be undefined here
resolve(concat(buffers));
return;
} else if (status < 0) {
// The read could fail for many different reasons when reading
// from a non-memory resident blob part (e.g. file-backed blob).
// The error details the system error code.
const error = lazyDOMException(
"The blob could not be read",
"NotReadableError",
);
reject(error);
return;
}
if (buffer !== undefined) buffers.push(buffer);
queueMicrotask(() => readNext());
});
};
readNext();
return promise;
}
/**
* @returns {Promise<string>}
*/
async text() {
if (!isBlob(this)) throw new ERR_INVALID_THIS("Blob");
dec ??= new TextDecoder();
return dec.decode(await this.arrayBuffer());
}
/**
* @returns {Promise<Uint8Array>}
*/
bytes() {
if (!isBlob(this)) throw new ERR_INVALID_THIS("Blob");
return Promise.prototype.then.call(
this.arrayBuffer(),
(buffer) => new Uint8Array(buffer),
);
}
/**
* @returns {ReadableStream}
*/
stream() {
if (!isBlob(this)) throw new ERR_INVALID_THIS("Blob");
const reader = this[kHandle].getReader();
return new lazyReadableStream(
{
type: "bytes",
start(c) {
// There really should only be one read at a time so using an
// array here is purely defensive.
this.pendingPulls = [];
},
pull(c) {
const { promise, resolve, reject } = createDeferredPromise();
this.pendingPulls.push({ resolve, reject });
const readNext = () => {
reader.pull((status, buffer) => {
// If pendingPulls is empty here, the stream had to have
// been canceled, and we don't really care about the result.
// We can simply exit.
if (this.pendingPulls.length === 0) {
return;
}
if (status === 0) {
// EOS
c.close();
// This is to signal the end for byob readers
// see https://streams.spec.whatwg.org/#example-rbs-pull
c.byobRequest?.respond(0);
const pending = this.pendingPulls.shift();
pending.resolve();
return;
} else if (status < 0) {
// The read could fail for many different reasons when reading
// from a non-memory resident blob part (e.g. file-backed blob).
// The error details the system error code.
const error = lazyDOMException(
"The blob could not be read",
"NotReadableError",
);
const pending = this.pendingPulls.shift();
c.error(error);
pending.reject(error);
return;
}
// ReadableByteStreamController.enqueue errors if we submit a 0-length
// buffer. We need to check for that here.
if (buffer !== undefined && buffer.byteLength !== 0) {
c.enqueue(new Uint8Array(buffer));
}
// We keep reading until we either reach EOS, some error, or we
// hit the flow rate of the stream (c.desiredSize).
queueMicrotask(() => {
if (c.desiredSize < 0) {
// A manual backpressure check.
if (this.pendingPulls.length !== 0) {
// A case of waiting pull finished (= not yet canceled)
const pending = this.pendingPulls.shift();
pending.resolve();
}
return;
}
readNext();
});
});
};
readNext();
return promise;
},
cancel(reason) {
// Reject any currently pending pulls here.
for (const pending of this.pendingPulls) {
pending.reject(reason);
}
this.pendingPulls = [];
},
// We set the highWaterMark to 0 because we do not want the stream to
// start reading immediately on creation. We want it to wait until read
// is called.
},
new CountQueuingStrategy({ highWaterMark: 0 }),
);
}
}
function TransferableBlob(handle, length, type = "") {
markTransferMode(this, true, false);
this[kHandle] = handle;
this[kType] = type;
this[kLength] = length;
}
Object.setPrototypeOf(TransferableBlob.prototype, Blob.prototype);
Object.setPrototypeOf(TransferableBlob, Blob);
function createBlob(handle, length, type = "") {
const transferredBlob = new TransferableBlob(handle, length, type);
// Fix issues like: https://github.com/nodejs/node/pull/49730#discussion_r1331720053
transferredBlob.constructor = Blob;
return transferredBlob;
}
Object.defineProperty(Blob.prototype, Symbol.toStringTag, {
__proto__: null,
configurable: true,
value: "Blob",
});
Object.defineProperties(Blob.prototype, {
size: kEnumerableProperty,
type: kEnumerableProperty,
slice: kEnumerableProperty,
stream: kEnumerableProperty,
text: kEnumerableProperty,
arrayBuffer: kEnumerableProperty,
});
function resolveObjectURL(url) {
url = `${url}`;
try {
const parsed = new URL(url);
const split = String.prototype.split.call(parsed.pathname, ":");
if (split.length !== 2) return;
const { 0: base, 1: id } = split;
if (base !== "nodedata") return;
const ret = getDataObject(id);
if (ret === undefined) return;
const { 0: handle, 1: length, 2: type } = ret;
if (handle !== undefined) return createBlob(handle, length, type);
} catch {
// If there's an error, it's ignored and nothing is returned
}
}
// TODO(@jasnell): Now that the File class exists, we might consider having
// this return a `File` instead of a `Blob`.
function createBlobFromFilePath(path, options) {
const maybeBlob = _createBlobFromFilePath(path);
if (maybeBlob === undefined) {
return lazyDOMException("The blob could not be read", "NotReadableError");
}
const { 0: blob, 1: length } = maybeBlob;
const res = createBlob(blob, length, options?.type);
res[kNotCloneable] = true;
return res;
}
export { Blob };
export { createBlob };
export { createBlobFromFilePath };
export { isBlob };
export { kHandle };
export { resolveObjectURL };