Skip to content
8 changes: 6 additions & 2 deletions file.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@ const _File = class File extends Blob {

if (options === null) options = {};

const modified = Number(options.lastModified);
this.#lastModified = Number.isNaN(modified) ? Date.now() : modified
// Simulate WebIDL type casting for NaN value in lastModified option.
const lastModified = options.lastModified === undefined ? Date.now() : Number(options.lastModified);
if (!Number.isNaN(lastModified)) {
this.#lastModified = lastModified;
}

this.#name = String(fileName);
}

Expand Down
110 changes: 58 additions & 52 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,48 @@ async function * toIterator (parts, clone = true) {
}
}

/**
* @param {Blob | NodeBlob | Uint8Array} blobParts
* @param {number} blobSize
* @param {number} start
* @param {number} end
*/
function* sliceBlob(blobParts, blobSize, start, end) {
let relativeStart = start < 0 ? Math.max(blobSize + start, 0) : Math.min(start, blobSize);
let relativeEnd = end < 0 ? Math.max(blobSize + end, 0) : Math.min(end, blobSize);

const span = Math.max(relativeEnd - relativeStart, 0);

let added = 0;
for (const part of blobParts) {
if (added >= span) {
break;
}

const partSize = ArrayBuffer.isView(part) ? part.byteLength : part.size;
if (relativeStart && partSize <= relativeStart) {
// Skip the beginning and change the relative
// start & end position as we skip the unwanted parts
relativeStart -= partSize;
relativeEnd -= partSize;
} else {
let chunk;
if (ArrayBuffer.isView(part)) {
chunk = part.subarray(relativeStart, Math.min(partSize, relativeEnd));
added += chunk.byteLength;
} else {
chunk = part.slice(relativeStart, Math.min(partSize, relativeEnd));
added += chunk.size;
}

relativeEnd -= partSize;
relativeStart = 0; // All next sequential parts should start at 0

yield chunk;
}
}
}

const _Blob = class Blob {

/** @type {Array.<(Blob|Uint8Array)>} */
Expand All @@ -58,18 +100,21 @@ const _Blob = class Blob {
* @param {{ type?: string }} [options]
*/
constructor(blobParts = [], options = {}) {
const parts = [];
let size = 0;
if (typeof blobParts !== 'object') {
throw new TypeError(`Failed to construct 'Blob': parameter 1 is not an iterable object.`);
if (typeof blobParts !== "object" || blobParts === null) {
throw new TypeError('Failed to construct \'Blob\': The provided value cannot be converted to a sequence.');
}

if (typeof blobParts[Symbol.iterator] !== "function") {
throw new TypeError('Failed to construct \'Blob\': The object must have a callable @@iterator property.');
}

if (typeof options !== 'object' && typeof options !== 'function') {
throw new TypeError(`Failed to construct 'Blob': parameter 2 cannot convert to dictionary.`);
throw new TypeError('Failed to construct \'Blob\': parameter 2 cannot convert to dictionary.');
}

if (options === null) options = {};

const encoder = new TextEncoder()
for (const element of blobParts) {
let part;
if (ArrayBuffer.isView(element)) {
Expand All @@ -79,18 +124,16 @@ const _Blob = class Blob {
} else if (element instanceof Blob) {
part = element;
} else {
part = new TextEncoder().encode(element);
part = encoder.encode(element);
}

size += ArrayBuffer.isView(part) ? part.byteLength : part.size;
parts.push(part);
this.#size += ArrayBuffer.isView(part) ? part.byteLength : part.size;
this.#parts.push(part);
}

const type = options.type === undefined ? '' : String(options.type);

this.#type = /^[\x20-\x7E]*$/.test(type) ? type : '';
this.#size = size;
this.#parts = parts;
}

/**
Expand Down Expand Up @@ -159,6 +202,10 @@ const _Blob = class Blob {
async pull(ctrl) {
const chunk = await it.next();
chunk.done ? ctrl.close() : ctrl.enqueue(chunk.value);
},

async cancel() {
await it.return()
}
})
}
Expand All @@ -173,48 +220,7 @@ const _Blob = class Blob {
* @param {string} [type]
*/
slice(start = 0, end = this.size, type = '') {
const {size} = this;

let relativeStart = start < 0 ? Math.max(size + start, 0) : Math.min(start, size);
let relativeEnd = end < 0 ? Math.max(size + end, 0) : Math.min(end, size);

const span = Math.max(relativeEnd - relativeStart, 0);
const parts = this.#parts;
const blobParts = [];
let added = 0;

for (const part of parts) {
// don't add the overflow to new blobParts
if (added >= span) {
break;
}

const size = ArrayBuffer.isView(part) ? part.byteLength : part.size;
if (relativeStart && size <= relativeStart) {
// Skip the beginning and change the relative
// start & end position as we skip the unwanted parts
relativeStart -= size;
relativeEnd -= size;
} else {
let chunk
if (ArrayBuffer.isView(part)) {
chunk = part.subarray(relativeStart, Math.min(size, relativeEnd));
added += chunk.byteLength
} else {
chunk = part.slice(relativeStart, Math.min(size, relativeEnd));
added += chunk.size
}
relativeEnd -= size;
blobParts.push(chunk);
relativeStart = 0; // All next sequential parts should start at 0
}
}

const blob = new Blob([], {type: String(type).toLowerCase()});
blob.#size = span;
blob.#parts = blobParts;

return blob;
return new Blob(sliceBlob(this.#parts, this.size, start, end), {type})
}

get [Symbol.toStringTag]() {
Expand Down
50 changes: 49 additions & 1 deletion test.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,17 @@ test('Blob ctor reads blob parts from object with @@iterator', async t => {
});

test('Blob ctor throws a string', t => {
t.throws(() => new Blob('abc'));
t.throws(() => new Blob('abc'), {
instanceOf: TypeError,
message: 'Failed to construct \'Blob\': The provided value cannot be converted to a sequence.'
});
});

test('Blob ctor throws an error for an object that does not have @@iterable method', t => {
t.throws(() => new Blob({}), {
instanceOf: TypeError,
message: 'Failed to construct \'Blob\': The object must have a callable @@iterator property.'
});
});

test('Blob ctor threats Uint8Array as a sequence', async t => {
Expand Down Expand Up @@ -123,6 +133,20 @@ test('Blob stream()', async t => {
}
});

test('Blob stream() can be cancelled', async t => {
const stream = new Blob(['Some content']).stream();

// Cancel the stream before start reading, or this will throw an error
await stream.cancel();

const iterator = stream[Symbol.asyncIterator]();

const {done, value: chunk} = await iterator.next();

t.true(done);
t.is(chunk, undefined);
});

test('Blob toString()', t => {
const data = 'a=1';
const type = 'text/plain';
Expand Down Expand Up @@ -355,6 +379,30 @@ test('new File(,,{lastModified: new Date()})', t => {
t.true(mod <= 0 && mod >= -20); // Close to tolerance: 0.020ms
});

test('new File(,,{lastModified: undefined})', t => {
const mod = new File([], '', {lastModified: undefined}).lastModified - Date.now();
t.true(mod <= 0 && mod >= -20); // Close to tolerance: 0.020ms
});

test('new File(,,{lastModified: null})', t => {
const mod = new File([], '', {lastModified: null}).lastModified;
t.is(mod, 0);
});

test('Interpretes NaN value in lastModified option as 0', t => {
t.plan(3);

const values = ['Not a Number', [], {}];

// I can't really see anything about this in the spec,
// but this is how browsers handle type casting for this option...
for (const lastModified of values) {
const file = new File(['Some content'], 'file.txt', {lastModified});

t.is(file.lastModified, 0);
}
});

test('new File(,,{}) sets current time', t => {
const mod = new File([], '').lastModified - Date.now();
t.true(mod <= 0 && mod >= -20); // Close to tolerance: 0.020ms
Expand Down