@web-std/blob
Version:
Web API compatible Blob implementation
309 lines (278 loc) • 8.06 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
var webEncoding = require('web-encoding');
var stream = require('@web-std/stream');
/**
* @implements {globalThis.Blob}
*/
const WebBlob = class Blob {
/**
* @param {BlobPart[]} [init]
* @param {BlobPropertyBag} [options]
*/
constructor(init = [], options = {}) {
/** @type {Uint8Array[]} */
const parts = [];
let size = 0;
for (const part of init) {
if (typeof part === "string") {
const bytes = new webEncoding.TextEncoder().encode(part);
parts.push(bytes);
size += bytes.byteLength;
} else if (part instanceof WebBlob) {
size += part.size;
// @ts-ignore - `_parts` is marked private so TS will complain about
// accessing it.
parts.push(...part._parts);
} else if (part instanceof ArrayBuffer) {
parts.push(new Uint8Array(part));
size += part.byteLength;
} else if (part instanceof Uint8Array) {
parts.push(part);
size += part.byteLength;
} else if (ArrayBuffer.isView(part)) {
const { buffer, byteOffset, byteLength } = part;
parts.push(new Uint8Array(buffer, byteOffset, byteLength));
size += byteLength;
} else {
const bytes = new webEncoding.TextEncoder().encode(String(part));
parts.push(bytes);
size += bytes.byteLength;
}
}
/** @private */
this._size = size;
/** @private */
this._type = readType(options.type);
/** @private */
this._parts = parts;
Object.defineProperties(this, {
_size: { enumerable: false },
_type: { enumerable: false },
_parts: { enumerable: false },
});
}
/**
* A string indicating the MIME type of the data contained in the Blob.
* If the type is unknown, this string is empty.
* @type {string}
*/
get type() {
return this._type
}
/**
* The size, in bytes, of the data contained in the Blob object.
* @type {number}
*/
get size() {
return this._size
}
/**
* Returns a new Blob object containing the data in the specified range of
* bytes of the blob on which it's called.
* @param {number} [start=0] - An index into the Blob indicating the first
* byte to include in the new Blob. If you specify a negative value, it's
* treated as an offset from the end of the Blob toward the beginning. For
* example, `-10` would be the 10th from last byte in the Blob. The default
* value is `0`. If you specify a value for start that is larger than the
* size of the source Blob, the returned Blob has size 0 and contains no
* data.
* @param {number} [end] - An index into the `Blob` indicating the first byte
* that will *not* be included in the new `Blob` (i.e. the byte exactly at
* this index is not included). If you specify a negative value, it's treated
* as an offset from the end of the Blob toward the beginning. For example,
* `-10` would be the 10th from last byte in the `Blob`. The default value is
* size.
* @param {string} [type] - The content type to assign to the new Blob;
* this will be the value of its type property. The default value is an empty
* string.
* @returns {Blob}
*/
slice(start = 0, end = this.size, type = "") {
const { size, _parts } = this;
let offset = start < 0 ? Math.max(size + start, 0) : Math.min(start, size);
let limit = end < 0 ? Math.max(size + end, 0) : Math.min(end, size);
const span = Math.max(limit - offset, 0);
const blob = new Blob([], { type });
if (span === 0) {
return blob
}
let blobSize = 0;
const blobParts = [];
for (const part of _parts) {
const { byteLength } = part;
if (offset > 0 && byteLength <= offset) {
offset -= byteLength;
limit -= byteLength;
} else {
const chunk = part.subarray(offset, Math.min(byteLength, limit));
blobParts.push(chunk);
blobSize += chunk.byteLength;
// no longer need to take that into account
offset = 0;
// don't add the overflow to new blobParts
if (blobSize >= span) {
break
}
}
}
blob._parts = blobParts;
blob._size = blobSize;
return blob
}
/**
* Returns a promise that resolves with an ArrayBuffer containing the entire
* contents of the Blob as binary data.
* @returns {Promise<ArrayBuffer>}
*/
// eslint-disable-next-line require-await
async arrayBuffer() {
const buffer = new ArrayBuffer(this.size);
const bytes = new Uint8Array(buffer);
let offset = 0;
for (const part of this._parts) {
bytes.set(part, offset);
offset += part.byteLength;
}
return buffer
}
/**
* Returns a promise that resolves with a USVString containing the entire
* contents of the Blob interpreted as UTF-8 text.
* @returns {Promise<string>}
*/
// eslint-disable-next-line require-await
async text() {
const decoder = new webEncoding.TextDecoder();
let text = "";
for (const part of this._parts) {
text += decoder.decode(part);
}
return text
}
/**
* @returns {BlobStream}
*/
stream() {
return new BlobStream(this._parts)
}
/**
* @returns {string}
*/
toString() {
return "[object Blob]"
}
get [Symbol.toStringTag]() {
return "Blob"
}
};
// Marking export as a DOM File object instead of custom class.
/** @type {typeof globalThis.Blob} */
const Blob = WebBlob;
/**
* Blob stream is a `ReadableStream` extension optimized to have minimal
* overhead when consumed as `AsyncIterable<Uint8Array>`.
* @extends {ReadableStream<Uint8Array>}
* @implements {AsyncIterable<Uint8Array>}
*/
class BlobStream extends stream.ReadableStream {
/**
* @param {Uint8Array[]} chunks
*/
constructor(chunks) {
// @ts-ignore
super(new BlobStreamController(chunks.values()), { type: "bytes" });
/** @private */
this._chunks = chunks;
}
/**
* @param {Object} [_options]
* @property {boolean} [_options.preventCancel]
* @returns {AsyncIterator<Uint8Array>}
*/
async *[Symbol.asyncIterator](_options) {
const reader = this.getReader();
yield* this._chunks;
reader.releaseLock();
}
}
class BlobStreamController {
/**
* @param {Iterator<Uint8Array>} chunks
*/
constructor(chunks) {
this.chunks = chunks;
}
/**
* @param {ReadableStreamDefaultController} controller
*/
start(controller) {
this.work(controller);
this.isWorking = false;
this.isCancelled = false;
}
/**
*
* @param {ReadableStreamDefaultController} controller
*/
async work(controller) {
const { chunks } = this;
this.isWorking = true;
while (!this.isCancelled && (controller.desiredSize || 0) > 0) {
let next = null;
try {
next = chunks.next();
} catch (error) {
controller.error(error);
break
}
if (next) {
if (!next.done && !this.isCancelled) {
controller.enqueue(next.value);
} else {
controller.close();
}
}
}
this.isWorking = false;
}
/**
* @param {ReadableStreamDefaultController} controller
*/
pull(controller) {
if (!this.isWorking) {
this.work(controller);
}
}
cancel() {
this.isCancelled = true;
}
}
/**
* @param {string} [input]
* @returns {string}
*/
const readType = (input = "") => {
const type = String(input).toLowerCase();
return /[^\u0020-\u007E]/.test(type) ? "" : type
};
Object.defineProperty(exports, 'TextDecoder', {
enumerable: true,
get: function () {
return webEncoding.TextDecoder;
}
});
Object.defineProperty(exports, 'TextEncoder', {
enumerable: true,
get: function () {
return webEncoding.TextEncoder;
}
});
Object.defineProperty(exports, 'ReadableStream', {
enumerable: true,
get: function () {
return stream.ReadableStream;
}
});
exports.Blob = Blob;
//# sourceMappingURL=blob.cjs.map