diff --git a/doc/api/buffer.md b/doc/api/buffer.md index fbbcc04083ef51..bc16808f1e1105 100644 --- a/doc/api/buffer.md +++ b/doc/api/buffer.md @@ -5094,6 +5094,9 @@ added: - v19.2.0 - v18.13.0 changes: + - version: REPLACEME + pr-url: https://github.com/nodejs/node/pull/47613 + description: Makes File instances cloneable. - version: v20.0.0 pr-url: https://github.com/nodejs/node/pull/47153 description: No longer experimental. diff --git a/lib/internal/blob.js b/lib/internal/blob.js index 4ff2b0e1e7051b..81dd627a1754ae 100644 --- a/lib/internal/blob.js +++ b/lib/internal/blob.js @@ -497,4 +497,5 @@ module.exports = { isBlob, kHandle, resolveObjectURL, + TransferableBlob, }; diff --git a/lib/internal/file.js b/lib/internal/file.js index e35eb7bf70931f..65fec6dfd0c627 100644 --- a/lib/internal/file.js +++ b/lib/internal/file.js @@ -2,14 +2,18 @@ const { DateNow, + FunctionPrototypeApply, NumberIsNaN, ObjectDefineProperties, + ObjectSetPrototypeOf, StringPrototypeToWellFormed, + Symbol, SymbolToStringTag, } = primordials; const { Blob, + TransferableBlob, } = require('internal/blob'); const { @@ -20,6 +24,7 @@ const { const { codes: { + ERR_INVALID_THIS, ERR_MISSING_ARGS, }, } = require('internal/errors'); @@ -28,13 +33,32 @@ const { inspect, } = require('internal/util/inspect'); -class File extends Blob { - /** @type {string} */ - #name; +const { + kClone, + kDeserialize, +} = require('internal/worker/js_transferable'); + +const kState = Symbol('state'); + +function isFile(object) { + return object?.[kState] !== undefined; +} - /** @type {number} */ - #lastModified; +class FileState { + name; + lastModified; + + /** + * @param {string} name + * @param {number} lastModified + */ + constructor(name, lastModified) { + this.name = name; + this.lastModified = lastModified; + } +} +class File extends Blob { constructor(fileBits, fileName, options = kEmptyObject) { if (arguments.length < 2) { throw new ERR_MISSING_ARGS('fileBits', 'fileName'); @@ -55,16 +79,21 @@ class File extends Blob { lastModified = DateNow(); } - this.#name = StringPrototypeToWellFormed(`${fileName}`); - this.#lastModified = lastModified; + this[kState] = new FileState(StringPrototypeToWellFormed(`${fileName}`), lastModified); } get name() { - return this.#name; + if (!isFile(this)) + throw new ERR_INVALID_THIS('File'); + + return this[kState].name; } get lastModified() { - return this.#lastModified; + if (!isFile(this)) + throw new ERR_INVALID_THIS('File'); + + return this[kState].lastModified; } [kInspect](depth, options) { @@ -80,12 +109,32 @@ class File extends Blob { return `File ${inspect({ size: this.size, type: this.type, - name: this.#name, - lastModified: this.#lastModified, + name: this[kState].name, + lastModified: this[kState].lastModified, }, opts)}`; } + + [kClone]() { + return { + data: { ...super[kClone]().data, ...this[kState] }, + deserializeInfo: 'internal/file:TransferableFile', + }; + } + + [kDeserialize](data) { + super[kDeserialize](data); + + this[kState] = new FileState(data.name, data.lastModified); + } +} + +function TransferableFile(handle, length, type = '') { + FunctionPrototypeApply(TransferableBlob, this, [handle, length, type]); } +ObjectSetPrototypeOf(TransferableFile.prototype, File.prototype); +ObjectSetPrototypeOf(TransferableFile, File); + ObjectDefineProperties(File.prototype, { name: kEnumerableProperty, lastModified: kEnumerableProperty, @@ -98,4 +147,5 @@ ObjectDefineProperties(File.prototype, { module.exports = { File, + TransferableFile, }; diff --git a/test/parallel/test-file.js b/test/parallel/test-file.js index bfc4548421be23..5f0cd2f4a035f8 100644 --- a/test/parallel/test-file.js +++ b/test/parallel/test-file.js @@ -158,3 +158,25 @@ const { inspect } = require('util'); ); }); } + +(async () => { + // File should be cloneable via structuredClone. + // Refs: https://github.com/nodejs/node/issues/47612 + + const body = ['hello, ', 'world']; + const lastModified = Date.now() - 10_000; + const name = 'hello_world.txt'; + + const file = new File(body, name, { lastModified }); + const clonedFile = structuredClone(file); + + assert.deepStrictEqual(await clonedFile.text(), await file.text()); + assert.deepStrictEqual(clonedFile.lastModified, file.lastModified); + assert.deepStrictEqual(clonedFile.name, file.name); + + const clonedFile2 = structuredClone(clonedFile); + + assert.deepStrictEqual(await clonedFile2.text(), await clonedFile.text()); + assert.deepStrictEqual(clonedFile2.lastModified, clonedFile.lastModified); + assert.deepStrictEqual(clonedFile2.name, clonedFile.name); +})().then(common.mustCall()); diff --git a/test/wpt/status/html/webappapis/structured-clone.json b/test/wpt/status/html/webappapis/structured-clone.json index 873f2f9b46eb03..0967ef424bce67 100644 --- a/test/wpt/status/html/webappapis/structured-clone.json +++ b/test/wpt/status/html/webappapis/structured-clone.json @@ -1,7 +1 @@ -{ - "structured-clone.any.js": { - "fail": { - "expected": ["File basic"] - } - } -} +{}