Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(): support aborting promises/requests #7827

Merged
merged 57 commits into from
Jun 30, 2022
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
735e854
fix(loadFromJSON): clear canvas
ShaMan123 Mar 23, 2022
cb13c0c
Update canvas_serialization.mixin.js
ShaMan123 Mar 23, 2022
d927c7e
feat(util): support aborting `loadImage`
ShaMan123 Mar 23, 2022
47e98f4
refactor `renderOnAddRemove`
ShaMan123 Mar 23, 2022
172d550
Update misc.js
ShaMan123 Mar 23, 2022
65e6984
parallel promise
ShaMan123 Mar 24, 2022
b8f0ada
live namespace
ShaMan123 Mar 24, 2022
c02b917
enliven options
ShaMan123 Mar 24, 2022
c1fdfd4
Update misc.js
ShaMan123 Mar 24, 2022
cc0539d
Update image.class.js
ShaMan123 Mar 24, 2022
3099a65
Update pattern.class.js
ShaMan123 Mar 24, 2022
f46d833
aborting
ShaMan123 Mar 24, 2022
54b01e5
aborting svg parsing
ShaMan123 Mar 24, 2022
5857859
bump node to support AbortController
ShaMan123 Mar 24, 2022
4ffefd6
listen to abort once
ShaMan123 Mar 24, 2022
aaf3f5b
bump node tests
ShaMan123 Mar 24, 2022
da720f5
lint + JSDOC
ShaMan123 Mar 24, 2022
82c4e9e
Update parser.js
ShaMan123 Mar 24, 2022
b1470dd
feat(Image): abort `setSrc`
ShaMan123 Mar 24, 2022
20e8e87
Update .eslintrc.json
ShaMan123 Mar 24, 2022
ea049c9
Update canvas_serialization.mixin.js
ShaMan123 Mar 24, 2022
92f5336
remove `resolveNamespace`
ShaMan123 Mar 24, 2022
7510ab1
Update pattern.class.js
ShaMan123 Mar 24, 2022
0565006
Update misc.js
ShaMan123 Mar 24, 2022
88b3f0c
consolidate options object
ShaMan123 Mar 24, 2022
3e0f7b6
abort image filter loading
ShaMan123 Mar 24, 2022
9f82c17
fire `loading:aborted` event
ShaMan123 Mar 24, 2022
bb26215
Update object.class.js
ShaMan123 Mar 24, 2022
b3a0ea7
disposing after aborting
ShaMan123 Mar 24, 2022
fc55713
remove DOMException
ShaMan123 Mar 24, 2022
e4d3882
Update canvas_static.js
ShaMan123 Mar 24, 2022
131513c
Update image.class.js
ShaMan123 Mar 24, 2022
32ec3c5
Update canvas_static.js
ShaMan123 Mar 24, 2022
a21a4a4
fix(): concurrency error
ShaMan123 Mar 24, 2022
c9713b1
Update misc.js
ShaMan123 Mar 24, 2022
53b4c5d
tests + concurrency test
ShaMan123 Mar 24, 2022
2e4b7d4
removeListener on error
ShaMan123 Mar 24, 2022
1081460
Update misc.js
ShaMan123 Mar 24, 2022
8c4cd91
bump node version
ShaMan123 Mar 24, 2022
3d38254
Update dom_request.js
ShaMan123 Mar 24, 2022
a3030dc
bump node version
ShaMan123 Mar 24, 2022
19b5ea5
Update image.class.js
ShaMan123 Mar 25, 2022
13c6940
Update image.js
ShaMan123 Mar 25, 2022
639223e
bump node to v16
ShaMan123 Mar 25, 2022
ea8f234
Merge branch 'master' into fix-json-load
ShaMan123 May 1, 2022
13c0056
Merge branch 'master' into fix-json-load
ShaMan123 Jun 26, 2022
54e1c0e
typo
ShaMan123 Jun 26, 2022
fd6573e
rename
ShaMan123 Jun 26, 2022
1ff31f1
rename
ShaMan123 Jun 26, 2022
4f5b6e2
incoming signal **ONLY** + revert node to v14
ShaMan123 Jun 28, 2022
52ba6fb
missed out
ShaMan123 Jun 28, 2022
ec86828
tests
ShaMan123 Jun 28, 2022
a61e3e6
Update canvas_serialization.mixin.js
ShaMan123 Jun 28, 2022
b15bea1
conform rejection object
ShaMan123 Jun 28, 2022
db16268
Update canvas_serialization.mixin.js
ShaMan123 Jun 28, 2022
efaf0b4
restore config for tests
ShaMan123 Jun 28, 2022
06a1d62
revert node v
ShaMan123 Jun 28, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"Buffer": true,
"process": true,
"QUnit": true,
"assert": true
"assert": true,
"AbortController": true
},
"rules": {
"semi": 2,
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/node-unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
strategy:
fail-fast: false
matrix:
node-version: [14.x, 16.x]
node-version: [14.17.0, 16.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v2
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/visual-node.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
strategy:
fail-fast: false
matrix:
node-version: [14.x, 16.x]
node-version: [14.17.0, 16.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v2
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
"uglify-js": "3.3.x"
},
"engines": {
"node": ">=14.0.0"
"node": ">=14.17.0"
},
"main": "./dist/fabric.js",
"dependencies": {}
Expand Down
54 changes: 34 additions & 20 deletions src/mixins/canvas_serialization.mixin.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ {

/**
* Aborts instance's loading task ({@link fabric.Canvas#loadFromJSON} etc.) if exists
*/
abortLoadingTask: function () {
if (this.__abortController) {
this.__abortController.abort();
delete this.__abortController;
}
},

/**
* Populates canvas with data from the specified JSON.
* JSON format must conform to the one of {@link fabric.Canvas#toJSON}
* @param {String|Object} json JSON string or object
* @param {Function} [reviver] Method for further parsing of JSON elements, called after each fabric object created.
* @return {Promise<fabric.Canvas>} instance
* @chainable
* @tutorial {@link http://fabricjs.com/fabric-intro-part-3#deserialization}
* @see {@link http://jsfiddle.net/fabricjs/fmgXt/|jsFiddle demo}
* @example <caption>loadFromJSON</caption>
Expand All @@ -20,8 +30,9 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati
* });
*/
loadFromJSON: function (json, reviver) {
this.abortLoadingTask();
if (!json) {
return;
return Promise.reject(new Error('fabric.js: `json` is undefined'));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BREAKING

ShaMan123 marked this conversation as resolved.
Show resolved Hide resolved
}

// serialize if it wasn't already
Expand All @@ -30,42 +41,45 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati
: fabric.util.object.clone(json);

var _this = this,
renderOnAddRemove = this.renderOnAddRemove;
renderOnAddRemove = this.renderOnAddRemove,
abortController = new AbortController();
ShaMan123 marked this conversation as resolved.
Show resolved Hide resolved

this.__abortController = abortController;
this.renderOnAddRemove = false;

return fabric.util.enlivenObjects(serialized.objects || [], '', reviver)
.then(function(enlived) {
return Promise.all([
fabric.util.enlivenObjects(serialized.objects || [], { reviver: reviver, signal: abortController.signal }),
fabric.util.enlivenObjectEnlivables({
backgroundImage: serialized.backgroundImage,
backgroundColor: serialized.background,
overlayImage: serialized.overlayImage,
overlayColor: serialized.overlay,
clipPath: serialized.clipPath,
}, { signal: abortController.signal })
])
.then(function (res) {
var enlived = res[0], enlivedMap = res[1];
_this.clear();
return fabric.util.enlivenObjectEnlivables({
backgroundImage: serialized.backgroundImage,
backgroundColor: serialized.background,
overlayImage: serialized.overlayImage,
overlayColor: serialized.overlay,
clipPath: serialized.clipPath,
})
.then(function(enlivedMap) {
_this.__setupCanvas(serialized, enlived, renderOnAddRemove);
_this.set(enlivedMap);
return _this;
});
_this.__setupCanvas(serialized, enlived);
_this.renderOnAddRemove = renderOnAddRemove;
delete _this.__abortController;
_this.set(enlivedMap);
return _this;
});
},

/**
* @private
* @param {Object} serialized Object with background and overlay information
* @param {Array} enlivenedObjects canvas objects
* @param {boolean} renderOnAddRemove renderOnAddRemove setting for the canvas
*/
__setupCanvas: function(serialized, enlivenedObjects, renderOnAddRemove) {
__setupCanvas: function(serialized, enlivenedObjects) {
var _this = this;
enlivenedObjects.forEach(function(obj, index) {
// we splice the array just in case some custom classes restored from JSON
// will add more object to canvas at canvas init.
_this.insertAt(obj, index);
});
this.renderOnAddRemove = renderOnAddRemove;
// remove parts i cannot set as options
delete serialized.objects;
delete serialized.backgroundImage;
Expand Down
11 changes: 9 additions & 2 deletions src/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -688,19 +688,23 @@
* @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created.
* @param {Object} [parsingOptions] options for parsing document
* @param {String} [parsingOptions.crossOrigin] crossOrigin settings
* @param {AbortSignal} [parsingOptions.signal] see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal
*/
fabric.parseSVGDocument = function(doc, callback, reviver, parsingOptions) {
if (!doc) {
return;
}

if (parsingOptions && parsingOptions.signal && parsingOptions.signal.aborted) {
throw new DOMException('`options.signal` is in `aborted` state', 'ABORT_ERR');
}
parseUseDirectives(doc);

var svgUid = fabric.Object.__uid++, i, len,
options = applyViewboxTransform(doc),
descendants = fabric.util.toArray(doc.getElementsByTagName('*'));
options.crossOrigin = parsingOptions && parsingOptions.crossOrigin;
options.svgUid = svgUid;
options.signal = parsingOptions && parsingOptions.signal;

if (descendants.length === 0 && fabric.isLikelyNode) {
// we're likely in node, where "o3-xml" library fails to gEBTN("*")
Expand Down Expand Up @@ -1046,13 +1050,15 @@
* @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created.
* @param {Object} [options] Object containing options for parsing
* @param {String} [options.crossOrigin] crossOrigin crossOrigin setting to use for external resources
* @param {AbortSignal} [options.signal] handle aborting, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal
*/
loadSVGFromURL: function(url, callback, reviver, options) {

url = url.replace(/^\n\s*/, '').trim();
new fabric.util.request(url, {
method: 'get',
onComplete: onComplete
onComplete: onComplete,
signal: options && options.signal
});

function onComplete(r) {
Expand All @@ -1077,6 +1083,7 @@
* @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created.
* @param {Object} [options] Object containing options for parsing
* @param {String} [options.crossOrigin] crossOrigin crossOrigin setting to use for external resources
* @param {AbortSignal} [options.signal] handle aborting, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal
*/
loadSVGFromString: function(string, callback, reviver, options) {
var parser = new fabric.window.DOMParser(),
Expand Down
14 changes: 11 additions & 3 deletions src/pattern.class.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,17 @@
}
});

fabric.Pattern.fromObject = function(object) {
var patternOptions = Object.assign({}, object);
return fabric.util.loadImage(object.source, { crossOrigin: object.crossOrigin })
/**
*
* @param {object} object
* @param {object} [options]
* @param {AbortSignal} [options.signal] handle aborting, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal
* @returns
*/
fabric.Pattern.fromObject = function(object, options) {
var patternOptions = Object.assign({}, object),
imageOptions = Object.assign({ crossOrigin: object.crossOrigin }, options);
return fabric.util.loadImage(object.source, imageOptions)
.then(function(img) {
patternOptions.source = img;
return new fabric.Pattern(patternOptions);
Expand Down
35 changes: 28 additions & 7 deletions src/shapes/image.class.js
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,16 @@
}
},

/**
* Aborts pending image loading task
*/
abortLoadingTask: function () {
if (this.__abortController) {
this.__abortController.abort();
delete this.__abortController;
}
},

/**
* Sets source of an image
* @param {String} src Source string (URL)
Expand All @@ -376,9 +386,13 @@
*/
setSrc: function(src, options) {
var _this = this;
return fabric.util.loadImage(src, options).then(function(img) {
this.abortLoadingTask();
this.__abortController = new AbortController();
var opts = Object.assign({}, options, { signal: this.__abortController.signal });
return fabric.util.loadImage(src, opts).then(function(img) {
_this.setElement(img, options);
_this._setWidthHeight();
delete this.__abortController;
return _this;
});
},
Expand Down Expand Up @@ -681,20 +695,24 @@
* Creates an instance of fabric.Image from its object representation
* @static
* @param {Object} object Object to create an instance from
* @param {object} [options] Options object
* @param {AbortSignal} [options.signal] handle aborting, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal
* @returns {Promise<fabric.Image>}
*/
fabric.Image.fromObject = function(_object) {
fabric.Image.fromObject = function(_object, options) {
var object = fabric.util.object.clone(_object),
filters = object.filters,
resizeFilter = object.resizeFilter;
// the generic enliving will fail on filters for now
delete object.resizeFilter;
delete object.filters;
var imageOptions = Object.assign({}, options, { crossOrigin: _object.crossOrigin }),
filterOptions = Object.assign({}, options, { namespace: fabric.Image.filters });
return Promise.all([
fabric.util.loadImage(object.src, { crossOrigin: _object.crossOrigin }),
filters && fabric.util.enlivenObjects(filters, 'fabric.Image.filters'),
resizeFilter && fabric.util.enlivenObjects([resizeFilter], 'fabric.Image.filters'),
fabric.util.enlivenObjectEnlivables(object),
fabric.util.loadImage(object.src, imageOptions),
filters && fabric.util.enlivenObjects(filters, filterOptions),
resizeFilter && fabric.util.enlivenObjects([resizeFilter], filterOptions),
fabric.util.enlivenObjectEnlivables(object, options),
])
.then(function(imgAndFilters) {
object.filters = imgAndFilters[1] || [];
Expand All @@ -707,7 +725,9 @@
* Creates an instance of fabric.Image from an URL string
* @static
* @param {String} url URL to create an image from
* @param {Object} [imgOptions] Options object
* @param {object} [imgOptions] Options object
* @param {string} [options.crossOrigin] cors value for the image loading, default to anonymous
* @param {AbortSignal} [options.signal] handle aborting, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal
* @returns {Promise<fabric.Image>}
*/
fabric.Image.fromURL = function(url, imgOptions) {
Expand All @@ -732,6 +752,7 @@
* @static
* @param {SVGElement} element Element to parse
* @param {Object} [options] Options object
* @param {AbortSignal} [options.signal] handle aborting, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal
* @param {Function} callback Callback to execute when fabric.Image object is created
* @return {fabric.Image} Instance of fabric.Image
*/
Expand Down
30 changes: 20 additions & 10 deletions src/shapes/object.class.js
Original file line number Diff line number Diff line change
Expand Up @@ -1932,23 +1932,33 @@
fabric.Object.NUM_FRACTION_DIGITS = 2;

/**
* Defines which properties should be enlivened from the object passed to {@link fabric.Object._fromObject}
* @static
* @memberOf fabric.Object
* @constant
* @type string[]
*
* @param {Function} klass
* @param {object} object
* @param {string} [extraParam] property to pass as first argument to the constructor
* @param {object} [options]
* @param {AbortSignal} [options.signal] handle aborting, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal
* @returns {Promise<fabric.Object>}
*/

fabric.Object._fromObject = function(klass, object, extraParam) {
fabric.Object._fromObject = function(klass, object, extraParam, options) {
var serializedObject = clone(object, true);
return fabric.util.enlivenObjectEnlivables(serializedObject).then(function(enlivedMap) {
return fabric.util.enlivenObjectEnlivables(serializedObject, options).then(function(enlivedMap) {
var newObject = Object.assign(object, enlivedMap);
return extraParam ? new klass(object[extraParam], newObject) : new klass(newObject);
});
};

fabric.Object.fromObject = function(object) {
return fabric.Object._fromObject(fabric.Object, object);
/**
*
* @static
* @memberOf fabric.Object
* @param {object} object
* @param {object} [options]
* @param {AbortSignal} [options.signal] handle aborting, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal
* @returns {Promise<fabric.Object>}
*/
fabric.Object.fromObject = function(object, options) {
return fabric.Object._fromObject(fabric.Object, object, null, options);
};

/**
Expand Down
17 changes: 15 additions & 2 deletions src/util/dom_request.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,33 @@
* @param {String} [options.method="GET"]
* @param {String} [options.parameters] parameters to append to url in GET or in body
* @param {String} [options.body] body to send with POST or PUT request
* @param {AbortSignal} [options.signal] handle aborting, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal
* @param {Function} options.onComplete Callback to invoke when request is completed
* @return {XMLHttpRequest} request
*/
function request(url, options) {
options || (options = { });

var method = options.method ? options.method.toUpperCase() : 'GET',
onComplete = options.onComplete || function() { },
onComplete = options.onComplete || function () { },
xhr = new fabric.window.XMLHttpRequest(),
body = options.body || options.parameters;
body = options.body || options.parameters,
signal = options.signal,
abort = abort = function () {
xhr.abort();
};

if (signal && signal.aborted) {
throw new DOMException('`options.signal` is in `aborted` state', 'ABORT_ERR');
}
else if (signal) {
signal.addEventListener('abort', abort, { once: true });
}

/** @ignore */
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
signal && signal.removeEventListener('abort', abort);
onComplete(xhr);
xhr.onreadystatechange = emptyFn;
}
Expand Down