Skip to content

Commit 17fd327

Browse files
authored
buffer: make File cloneable
Fixes: #47612 PR-URL: #47613 Reviewed-By: Antoine du Hamel <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Yagiz Nizipli <[email protected]> Reviewed-By: Matteo Collina <[email protected]>
1 parent da5887d commit 17fd327

File tree

5 files changed

+88
-18
lines changed

5 files changed

+88
-18
lines changed

doc/api/buffer.md

+3
Original file line numberDiff line numberDiff line change
@@ -5094,6 +5094,9 @@ added:
50945094
- v19.2.0
50955095
- v18.13.0
50965096
changes:
5097+
- version: REPLACEME
5098+
pr-url: https://github.com/nodejs/node/pull/47613
5099+
description: Makes File instances cloneable.
50975100
- version: v20.0.0
50985101
pr-url: https://github.com/nodejs/node/pull/47153
50995102
description: No longer experimental.

lib/internal/blob.js

+1
Original file line numberDiff line numberDiff line change
@@ -504,4 +504,5 @@ module.exports = {
504504
isBlob,
505505
kHandle,
506506
resolveObjectURL,
507+
TransferableBlob,
507508
};

lib/internal/file.js

+61-11
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@
22

33
const {
44
DateNow,
5+
FunctionPrototypeApply,
56
NumberIsNaN,
67
ObjectDefineProperties,
8+
ObjectSetPrototypeOf,
79
StringPrototypeToWellFormed,
10+
Symbol,
811
SymbolToStringTag,
912
} = primordials;
1013

1114
const {
1215
Blob,
16+
TransferableBlob,
1317
} = require('internal/blob');
1418

1519
const {
@@ -20,6 +24,7 @@ const {
2024

2125
const {
2226
codes: {
27+
ERR_INVALID_THIS,
2328
ERR_MISSING_ARGS,
2429
},
2530
} = require('internal/errors');
@@ -28,13 +33,32 @@ const {
2833
inspect,
2934
} = require('internal/util/inspect');
3035

31-
class File extends Blob {
32-
/** @type {string} */
33-
#name;
36+
const {
37+
kClone,
38+
kDeserialize,
39+
} = require('internal/worker/js_transferable');
40+
41+
const kState = Symbol('state');
42+
43+
function isFile(object) {
44+
return object?.[kState] !== undefined;
45+
}
3446

35-
/** @type {number} */
36-
#lastModified;
47+
class FileState {
48+
name;
49+
lastModified;
50+
51+
/**
52+
* @param {string} name
53+
* @param {number} lastModified
54+
*/
55+
constructor(name, lastModified) {
56+
this.name = name;
57+
this.lastModified = lastModified;
58+
}
59+
}
3760

61+
class File extends Blob {
3862
constructor(fileBits, fileName, options = kEmptyObject) {
3963
if (arguments.length < 2) {
4064
throw new ERR_MISSING_ARGS('fileBits', 'fileName');
@@ -55,16 +79,21 @@ class File extends Blob {
5579
lastModified = DateNow();
5680
}
5781

58-
this.#name = StringPrototypeToWellFormed(`${fileName}`);
59-
this.#lastModified = lastModified;
82+
this[kState] = new FileState(StringPrototypeToWellFormed(`${fileName}`), lastModified);
6083
}
6184

6285
get name() {
63-
return this.#name;
86+
if (!isFile(this))
87+
throw new ERR_INVALID_THIS('File');
88+
89+
return this[kState].name;
6490
}
6591

6692
get lastModified() {
67-
return this.#lastModified;
93+
if (!isFile(this))
94+
throw new ERR_INVALID_THIS('File');
95+
96+
return this[kState].lastModified;
6897
}
6998

7099
[kInspect](depth, options) {
@@ -80,12 +109,32 @@ class File extends Blob {
80109
return `File ${inspect({
81110
size: this.size,
82111
type: this.type,
83-
name: this.#name,
84-
lastModified: this.#lastModified,
112+
name: this[kState].name,
113+
lastModified: this[kState].lastModified,
85114
}, opts)}`;
86115
}
116+
117+
[kClone]() {
118+
return {
119+
data: { ...super[kClone]().data, ...this[kState] },
120+
deserializeInfo: 'internal/file:TransferableFile',
121+
};
122+
}
123+
124+
[kDeserialize](data) {
125+
super[kDeserialize](data);
126+
127+
this[kState] = new FileState(data.name, data.lastModified);
128+
}
129+
}
130+
131+
function TransferableFile(handle, length, type = '') {
132+
FunctionPrototypeApply(TransferableBlob, this, [handle, length, type]);
87133
}
88134

135+
ObjectSetPrototypeOf(TransferableFile.prototype, File.prototype);
136+
ObjectSetPrototypeOf(TransferableFile, File);
137+
89138
ObjectDefineProperties(File.prototype, {
90139
name: kEnumerableProperty,
91140
lastModified: kEnumerableProperty,
@@ -98,4 +147,5 @@ ObjectDefineProperties(File.prototype, {
98147

99148
module.exports = {
100149
File,
150+
TransferableFile,
101151
};

test/parallel/test-file.js

+22
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,25 @@ const { inspect } = require('util');
158158
);
159159
});
160160
}
161+
162+
(async () => {
163+
// File should be cloneable via structuredClone.
164+
// Refs: https://github.com/nodejs/node/issues/47612
165+
166+
const body = ['hello, ', 'world'];
167+
const lastModified = Date.now() - 10_000;
168+
const name = 'hello_world.txt';
169+
170+
const file = new File(body, name, { lastModified });
171+
const clonedFile = structuredClone(file);
172+
173+
assert.deepStrictEqual(await clonedFile.text(), await file.text());
174+
assert.deepStrictEqual(clonedFile.lastModified, file.lastModified);
175+
assert.deepStrictEqual(clonedFile.name, file.name);
176+
177+
const clonedFile2 = structuredClone(clonedFile);
178+
179+
assert.deepStrictEqual(await clonedFile2.text(), await clonedFile.text());
180+
assert.deepStrictEqual(clonedFile2.lastModified, clonedFile.lastModified);
181+
assert.deepStrictEqual(clonedFile2.name, clonedFile.name);
182+
})().then(common.mustCall());
Original file line numberDiff line numberDiff line change
@@ -1,7 +1 @@
1-
{
2-
"structured-clone.any.js": {
3-
"fail": {
4-
"expected": ["File basic"]
5-
}
6-
}
7-
}
1+
{}

0 commit comments

Comments
 (0)