import { registerWorkerAdapter, worker, Util } from 'maptalks'; //@ts-ignore import WORKERCODE from './worker/worker.bundle.js'; import { isNumber, uuid, isImageBitmap, isPolygon, checkBuffers, disposeImage, needFormatImageType, needPostProcessingImage, removeTimeOut, validateSubdomains, isString, isFunction, getFunctionBody, checkArray, isAbsoluteURL } from './util'; import { getCanvas, getCanvasContext, resizeCanvas } from './canvas'; import { privateOptions, getTileOptions, layoutTilesOptions, getTileWithMaxZoomOptions, transformTileOptions, clipTileOptions, GeoJSONPolygon, GeoJSONMultiPolygon, encodeTerrainTileOptions, colorTerrainTileOptions, tileIntersectMaskOptions, injectImageOptions, getImageTileOptions, terrainTileFixBoundaryOptions, resolveResultType, rejectResultType, sliceImageResultType, getVTTileOptions, rectifyTileOptions, rectifyBaiduTileOptions } from './types.js'; import { imageTile } from './imagetile.js'; import { CANVAS_ERROR_MESSAGE, createParamsValidateError, FetchCancelError, TaskCancelError } from './Error.js'; export { getBlankTile, get404Tile } from './canvas'; export { getBlankVTTile } from './util' const WORKERNAME = '__maptalks.tileclip__'; registerWorkerAdapter(WORKERNAME, WORKERCODE as unknown as string); const MASKMAP = {}; const IMAGEMAP = {}; const SUPPORTPROJECTION = ['EPSG:4326', 'EPSG:3857']; const transformTypes = ['WGS84-GCJ02', 'GCJ02-WGS84']; const transformBaiduTypes = ['BAIDU-WGS84', 'BAIDU-GCJ02']; const TerrainTypes = ['mapzen', 'tianditu', 'cesium', 'arcgis', 'qgis-gray']; const URLKEYS = ['url', 'urlTemplate', 'tile']; function checkOptions(options, type: string) { const opts = Object.assign( { //default params referrer: document.location.href, tileSize: 256, maxAvailableZoom: 18, opacity: 1, quality: 1 }, options, { __type: type, __taskId: uuid(), __workerId: getWorkerId() }); URLKEYS.forEach(key => { let url = opts[key]; if (!url) { return; } const isArray = Array.isArray(url); url = checkArray(url); let urls = url.map(v => { if (isAbsoluteURL(v)) { return v; } return Util.getAbsoluteURL(v); }); if (!isArray) { urls = urls[0]; } opts[key] = urls; }) return opts; } function getTaskId(options: Record) { const workerId = options.__workerId as number; const taskId = options.__taskId as number; return { workerId, taskId } } function isErrorOrCancel(error: Error, promise): boolean { return (error || (promise && promise.canceled)); } function validateUrlTemplate(type: string, options: any) { const { subdomains, maxAvailableZoom } = options; let urlTemplate = options.urlTemplate; const maxZoomEnable = maxAvailableZoom && isNumber(maxAvailableZoom) && maxAvailableZoom >= 1; if (!maxZoomEnable) { return createParamsValidateError(`${type} error:maxAvailableZoom is error`) } if (!urlTemplate) { return createParamsValidateError(`${type} error:urlTemplate is null`) } if (!validateSubdomains(urlTemplate, subdomains)) { return createParamsValidateError(`${type} error: urlTemplate has {s} but subdomains is null`); } urlTemplate = checkArray(urlTemplate); const urlTemplateBody = []; urlTemplate.forEach(url => { if (isFunction(url)) { urlTemplateBody.push('function:' + getFunctionBody(url)); } else { urlTemplateBody.push(url); } }); options.urlTemplate = urlTemplateBody; return null; } function validateTileXYZ(x: number, y: number, z: number) { return isNumber(x) && isNumber(y) && isNumber(z); } function prepareCanvasBuffers(options: any, key: string, callback: (buffers: any[]) => void) { const value = options[key]; const buffers = []; if (!value) { callback(buffers); return; } const items = checkArray(value); let idx = 0; const isEnd = () => { if (idx >= items.length) { if (!Array.isArray(value) && buffers.length === 1) { options[key] = buffers[0]; } callback(buffers); } } items.forEach((item, index) => { if (!item) { idx++; isEnd(); return; } if (isImageBitmap(item) || (item instanceof ArrayBuffer)) { buffers.push(item); idx++; isEnd(); } else if (item instanceof HTMLImageElement || item instanceof HTMLCanvasElement || item instanceof OffscreenCanvas || item instanceof Image) { createImageBitmap(item).then(imageBitmap => { buffers.push(imageBitmap); if (Array.isArray(value)) { value[index] = imageBitmap; } idx++; isEnd(); }).catch(error => { console.error(error) idx++; isEnd(); }) } else { idx++; isEnd(); } }); } class TileActor extends worker.Actor { _cancelTask(options: privateOptions) { const { workerId, taskId } = getTaskId(options); if (!isNumber(workerId) || !isNumber(taskId)) { return; } if (taskId) { this.send({ __type: 'cancelFetch', __taskId: taskId }, [], (error, image) => { // if (error) { // reject(error); // } else { // resolve(image); // } }, workerId); } } getTile(options: getTileOptions) { const type = 'getTile'; options = checkOptions(options, type); const { workerId } = getTaskId(options); const promise = new Promise((resolve: resolveResultType, reject: rejectResultType) => { const { url } = options; if (!url) { reject(createParamsValidateError(`${type} error:url is null`)); return; } prepareCanvasBuffers(options, 'url', (buffers => { this.send(options, buffers, (error, image) => { if (isErrorOrCancel(error, promise)) { disposeImage(image); reject(error || FetchCancelError); } else { resolve(image); } }, workerId); })); }); wrapPromise(promise, options); return promise; } layoutTiles(options: layoutTilesOptions) { const type = 'layoutTiles'; options = checkOptions(options, type); const { workerId } = getTaskId(options); const promise = new Promise((resolve: resolveResultType, reject: rejectResultType) => { const { tiles } = options; const error = validateUrlTemplate(type, options); if (error) { reject(error); return; } if (!tiles || tiles.length === 0) { reject(createParamsValidateError(`${type} error:tiles is null`)); return; } const buffers = []; this.send(options, buffers, (error, image) => { if (isErrorOrCancel(error, promise)) { disposeImage(image); reject(error || FetchCancelError); } else { resolve(image); } }, workerId); }); wrapPromise(promise, options); return promise; } getTileWithMaxZoom(options: getTileWithMaxZoomOptions) { const type = 'getTileWithMaxZoom'; options = checkOptions(options, type); const { workerId } = getTaskId(options); const promise = new Promise((resolve: resolveResultType, reject: rejectResultType) => { const { x, y, z } = options; const error = validateUrlTemplate(type, options); if (error) { reject(error); return; } if (!validateTileXYZ(x, y, z)) { reject(createParamsValidateError(`${type} error:x/y/z is error`)); return; } this.send(options, [], (error, image) => { if (isErrorOrCancel(error, promise)) { disposeImage(image); reject(error || FetchCancelError); } else { resolve(image); } }, workerId); }); wrapPromise(promise, options); return promise; } reProjectTile(options: transformTileOptions) { return this.transformTile(options); } transformTile(options: transformTileOptions) { const type = 'transformTile'; options = checkOptions(options, type); const { workerId } = getTaskId(options); const promise = new Promise((resolve: resolveResultType, reject: rejectResultType) => { const { x, y, z, projection } = options; if (!projection) { reject(createParamsValidateError(`${type} error:not find projection`)); return; } if (SUPPORTPROJECTION.indexOf(projection) === -1) { reject(createParamsValidateError(`${type} error:not support projection:` + projection + '.the support:' + SUPPORTPROJECTION.join(',').toString())); return; } const error = validateUrlTemplate(type, options); if (error) { reject(error); return; } if (!validateTileXYZ(x, y, z)) { reject(createParamsValidateError(`${type} error:x/y/z is error`)); return; } this.send(options, [], (error, image) => { if (isErrorOrCancel(error, promise)) { disposeImage(image); reject(error || FetchCancelError); } else { resolve(image); } }, workerId); }); wrapPromise(promise, options); return promise; } rectifyTile(options: rectifyTileOptions) { const type = 'rectifyTile'; options = checkOptions(options, type); const { workerId } = getTaskId(options); const promise = new Promise((resolve: resolveResultType, reject: rejectResultType) => { const { x, y, z, maxAvailableZoom, projection, tileBBOX, transform } = options; if (!projection) { reject(createParamsValidateError(`${type} error:not find projection`)); return; } if (SUPPORTPROJECTION.indexOf(projection) === -1) { reject(createParamsValidateError(`${type} error:not support projection:` + projection + '.the support:' + SUPPORTPROJECTION.join(',').toString())); return; } const error = validateUrlTemplate(type, options); if (error) { reject(error); return; } if (!validateTileXYZ(x, y, z)) { reject(createParamsValidateError(`${type} error:x/y/z is error`)); return; } if (!tileBBOX) { reject(createParamsValidateError(`${type} error:tileBBOX is null`)); return; } if (!transform) { reject(createParamsValidateError(`${type} error:transform is null`)); return; } if (transformTypes.indexOf(transform) === -1) { reject(createParamsValidateError(`${type} error:not support transformTo:` + transform + '.the support:' + transformTypes.join(',').toString())); return; } this.send(options, [], (error, image) => { if (isErrorOrCancel(error, promise)) { disposeImage(image); reject(error || FetchCancelError); } else { resolve(image); } }, workerId); }); wrapPromise(promise, options); return promise; } rectifyBaiduTile(options: rectifyBaiduTileOptions) { const type = 'rectifyBaiduTile'; options = checkOptions(options, type); const { workerId } = getTaskId(options); const promise = new Promise((resolve: resolveResultType, reject: rejectResultType) => { const { x, y, z, tileBBOX, transform } = options; const error = validateUrlTemplate(type, options); if (error) { reject(error); return; } if (!validateTileXYZ(x, y, z)) { reject(createParamsValidateError(`${type} error:x/y/z is error`)); return; } if (!tileBBOX) { reject(createParamsValidateError(`${type} error:tileBBOX is null`)); return; } if (!transform) { reject(createParamsValidateError(`${type} error:transform is null`)); return; } if (transformBaiduTypes.indexOf(transform) === -1) { reject(createParamsValidateError(`${type} error:not support transformTo:` + transform + '.the support:' + transformBaiduTypes.join(',').toString())); return; } this.send(options, [], (error, image) => { if (isErrorOrCancel(error, promise)) { disposeImage(image); reject(error || FetchCancelError); } else { resolve(image); } }, workerId); }); wrapPromise(promise, options); return promise; } clipTile(options: clipTileOptions) { const type = 'clipTile'; options = checkOptions(options, type); const { workerId } = getTaskId(options); const promise = new Promise((resolve: resolveResultType, reject: rejectResultType) => { const { tile, tileBBOX, projection, tileSize, maskId } = options; if (!tile) { reject(createParamsValidateError(`${type} error:tile is null.It should be a ImageBitmap`)); return; } if (!tileBBOX) { reject(createParamsValidateError(`${type} error:tileBBOX is null`)); return; } if (!projection) { reject(createParamsValidateError(`${type} error:projection is null`)); return; } if (!tileSize) { reject(createParamsValidateError(`${type} error:tileSize is null`)); return; } if (!maskId) { reject(createParamsValidateError(`${type} error:maskId is null`)); return; } if (!this.maskHasInjected(maskId)) { reject(createParamsValidateError(`${type} error:not find mask by maskId:` + maskId)); return; } const buffers: ArrayBuffer[] = []; if (isImageBitmap(options.tile)) { buffers.push(options.tile as unknown as ArrayBuffer); } this.send(options, buffers, (error, image) => { if (isErrorOrCancel(error, promise)) { disposeImage(image); reject(error || TaskCancelError); } else { resolve(image); } }, workerId); }); wrapPromise(promise, options); return promise; } tileIntersectMask(options: tileIntersectMaskOptions) { const type = 'tileIntersectMask'; options = checkOptions(options, type); delete (options as unknown as privateOptions).__taskId; delete (options as unknown as privateOptions).__workerId; const promise = new Promise((resolve: (result: { intersect: boolean }) => void, reject: rejectResultType) => { const { tileBBOX, maskId } = options; if (!tileBBOX) { reject(createParamsValidateError(`${type} error:tileBBOX is null`)); return; } if (!maskId) { reject(createParamsValidateError(`${type} error:maskId is null`)); return; } if (!this.maskHasInjected(maskId)) { reject(createParamsValidateError(`${type} error:not find mask by maskId:` + maskId)); return; } const buffers: ArrayBuffer[] = []; this.send(options, buffers, (error, result) => { if (isErrorOrCancel(error, promise)) { reject(error || TaskCancelError); } else { resolve(result); } }); }); wrapPromise(promise, options); return promise; } injectMask(maskId: string, geojsonFeature: GeoJSONPolygon | GeoJSONMultiPolygon) { const type = 'injectMask'; const promise = new Promise((resolve, reject: rejectResultType) => { if (!maskId) { reject(createParamsValidateError(`${type} error:maskId is null`)); return; } if (MASKMAP[maskId]) { reject(createParamsValidateError(`${type} error:${maskId} has injected`)); return; } if (!isPolygon(geojsonFeature)) { reject(createParamsValidateError(`${type} error:geojsonFeature is not Polygon,It should be GeoJSON Polygon/MultiPolygon`)); return; } this.broadcast({ maskId, geojsonFeature, __type: type }, [], (error, data) => { if (isErrorOrCancel(error, promise)) { reject(error || TaskCancelError); return; } resolve(null); MASKMAP[maskId] = true; }); }); wrapPromise(promise, {}); return promise; } removeMask(maskId: string) { const type = 'removeMask'; const promise = new Promise((resolve, reject: rejectResultType) => { if (!maskId) { reject(createParamsValidateError(`${type} error:maskId is null`)); return; } this.broadcast({ maskId, __type: type }, [], (error, data) => { if (isErrorOrCancel(error, promise)) { reject(error || TaskCancelError); return; } resolve(null); delete MASKMAP[maskId]; }); }); wrapPromise(promise, {}); return promise; } maskHasInjected(maskId: string) { if (!maskId) { console.error('maskHasInjected error:maskId is null'); return false; } return !!MASKMAP[maskId]; } imageSlicing(options: getTileOptions) { options = checkOptions(options, 'imageSlicing'); const { workerId } = getTaskId(options); const promise = new Promise((resolve: (data: sliceImageResultType) => void, reject: rejectResultType) => { const { url } = options; if (!url) { reject(createParamsValidateError('url is null')); return; } prepareCanvasBuffers(options, 'url', (buffers => { this.send(options, buffers, (error, result) => { if (isErrorOrCancel(error, promise)) { reject(error || FetchCancelError); } else { if (!needFormatImageType(options)) { resolve(result); } else { const items = result.items || []; const workerIds = []; while (1) { const workerId = getWorkerId(); if (workerIds.indexOf(workerId) === -1) { workerIds.push(workerId); } else { break; } } const pageSize = Math.ceil(items.length / workerIds.length); let temp = []; const isEnd = () => { return temp.length === items.length; } const mergeResult = () => { temp.forEach(d => { for (let i = 0, len = items.length; i < len; i++) { const item = items[i]; if (item.id === d.id) { item.image = d.url; break; } } }); resolve(result); } for (let i = 0, len = workerIds.length; i < len; i++) { const workerId = workerIds[i]; const start = i * pageSize; const end = start + pageSize; const subItems = items.slice(start, end); if (subItems.length === 0) { if (isEnd()) { mergeResult(); } continue; } const opts = Object.assign({}, options); (opts as any).__type = 'imageToBlobURL'; (opts as any).items = subItems; (opts as any)._workerId = workerId; const buffers = subItems.map(item => item.image as ArrayBuffer); this.send(opts, buffers, (error, resultItems) => { if (isErrorOrCancel(error, promise)) { reject(error || FetchCancelError); return; } else { temp = temp.concat(resultItems); if (isEnd()) { mergeResult(); } } }, workerId); } } } }, workerId); })) }); wrapPromise(promise, options); return promise; } encodeTerrainTile(options: encodeTerrainTileOptions) { const type = 'encodeTerrainTile'; options = checkOptions(options, type); const { workerId } = getTaskId(options); const promise = new Promise((resolve: resolveResultType, reject: rejectResultType) => { const { url, terrainType, minHeight, maxHeight } = options; if (!url) { reject(createParamsValidateError(`${type} error:url is null`)); return; } if (!terrainType) { reject(createParamsValidateError(`${type} error:terrainType is null`)); return; } if (TerrainTypes.indexOf(terrainType) === -1) { reject(createParamsValidateError(`${type} error:terrainType:not support the terrainType:` + terrainType)); return; } if (terrainType === 'qgis-gray') { if (!isNumber(minHeight) || !isNumber(maxHeight) || minHeight > maxHeight) { reject(createParamsValidateError(`${type} error:terrainType:qgis-gray,require minHeight/maxHeight should number`)); return; } } this.send(Object.assign({ terrainWidth: 65, tileSize: 256 }, options), [], (error, image) => { if (isErrorOrCancel(error, promise)) { disposeImage(image); reject(error || FetchCancelError); } else { resolve(image); } }, workerId); }); wrapPromise(promise, options); return promise; } terrainTileFixBoundary(options: terrainTileFixBoundaryOptions) { const type = 'terrainTileFixBoundary'; options = checkOptions(options, type); const { workerId } = getTaskId(options); const promise = new Promise((resolve: resolveResultType, reject: rejectResultType) => { const { tiles } = options; if (!tiles || tiles.length === 0) { reject(createParamsValidateError(`${type} error:tiles is null`)); return; } const z = tiles[0].z; let tileSize = 256; for (let i = 0, len = tiles.length; i < len; i++) { const zoom = tiles[i].z; const image = tiles[i].image; if (!image) { reject(createParamsValidateError(`${type} error:not find tile.image`)); console.error(tiles); return; } tileSize = image.width; if (zoom !== z) { reject(createParamsValidateError(`${type} error:All tiles should be on the same level`)); console.error(tiles); return; } } let minX = Infinity, minY = Infinity; for (let i = 0, len = tiles.length; i < len; i++) { const { x, y } = tiles[i]; minX = Math.min(x, minX); minY = Math.min(y, minY); } const findTile = (tileX: number, tileY: number) => { for (let i = 0, len = tiles.length; i < len; i++) { const { x, y } = tiles[i]; if (x === tileX && y === tileY) { return tiles[i]; } } } const currentTile = findTile(minX, minY); const rightTile = findTile(minX + 1, minY); const bottomTile = findTile(minX, minY + 1); const canvas = getCanvas(tileSize); resizeCanvas(canvas, tileSize, tileSize); const ctx = getCanvasContext(canvas); ctx.drawImage(currentTile.image, 0, 0, tileSize, tileSize); if (rightTile) { ctx.drawImage(rightTile.image, 0, 0, 1, tileSize, tileSize - 1, 0, 1, tileSize); } if (bottomTile) { ctx.drawImage(bottomTile.image, 0, 0, tileSize, 1, 0, tileSize - 1, tileSize, 1); } const image = canvas.transferToImageBitmap(); if (needFormatImageType(options)) { const buffers = checkBuffers(image); const { returnBlobURL, returnUint32Buffer, returnBase64 } = options; this.send(Object.assign({ __type: 'tilePostAndToBlobURL' }, { returnBlobURL, returnUint32Buffer, returnBase64, image }), buffers, (error, url) => { if (isErrorOrCancel(error, promise)) { reject(error || FetchCancelError); } else { resolve(url); } }, workerId); } else { const tid = setTimeout(() => { if (isErrorOrCancel(null, promise)) { disposeImage(image); reject(TaskCancelError); return; } removeTimeOut(tid); resolve(image); }, 1); } }); wrapPromise(promise, options); return promise; } colorTerrainTile(options: colorTerrainTileOptions) { const type = 'colorTerrainTile'; options = checkOptions(options, type); const { workerId } = getTaskId(options); const promise = new Promise((resolve: resolveResultType, reject: rejectResultType) => { const { tile, colors } = options; if (!tile || !(isImageBitmap(tile) || isString(tile))) { reject(createParamsValidateError(`${type} error:tile is not ImageBitMap`)); return; } if (!colors || !Array.isArray(colors) || colors.length === 0) { reject(createParamsValidateError(`${type} error:colors is null`)); return; } prepareCanvasBuffers(options, 'tile', (buffers => { this.send(Object.assign({}, options), buffers, (error, image) => { if (isErrorOrCancel(error, promise)) { disposeImage(image); reject(error || TaskCancelError); } else { resolve(image); } }, workerId); })) },); wrapPromise(promise, options); return promise; } injectImage(options: injectImageOptions) { const type = 'injectImage'; options = checkOptions(options, type); const promise = new Promise((resolve: (data: any) => void, reject: rejectResultType) => { const { imageId, url, imageBBOX } = options; if (!imageId) { reject(createParamsValidateError(`${type} error:imageId is null`)); return; } if (!url) { reject(createParamsValidateError(`${type} error:url is null`)); return; } if (this.imageHasInjected(imageId)) { reject(createParamsValidateError(`${type} error:${imageId} has injected`)); return; } if (!imageBBOX) { reject(createParamsValidateError(`${type} error:imageBBOX is null`)); return; } options.url = Util.getAbsoluteURL(url); this.send(Object.assign({}, options, { __type: 'imagetTileFetch' }), [], (error, image) => { if (isErrorOrCancel(error, promise)) { reject(error || FetchCancelError); } else { IMAGEMAP[imageId] = { imageBBOX, url, image }; resolve(null); } }); }); wrapPromise(promise, {}); return promise; } removeImage(imageId: string) { const promise = new Promise((resolve: (data: any) => void, reject: rejectResultType) => { if (!imageId) { reject(createParamsValidateError('removeImage error:imageId is null')); return; } const tid = setTimeout(() => { if (isErrorOrCancel(null, promise)) { reject(TaskCancelError); return; } const imageInfo = IMAGEMAP[imageId] || {}; delete IMAGEMAP[imageId]; const image = imageInfo.image; disposeImage(image); removeTimeOut(tid); resolve(null); }, 1); }); wrapPromise(promise, {}); return promise; } imageHasInjected(imageId: string) { if (!imageId) { console.error('imageHasInjected error:imageId is null'); return false; } return !!IMAGEMAP[imageId]; } getImageTile(options: getImageTileOptions) { const type = 'getImageTile'; options = checkOptions(options, type); const promise = new Promise((resolve: resolveResultType, reject: rejectResultType) => { const { tileBBOX, projection, imageId } = options; if (!tileBBOX) { reject(createParamsValidateError(`${type} error:tileBBOX is null`)); return; } if (!imageId) { reject(createParamsValidateError(`${type} error:imageId is null`)); return; } if (!projection) { reject(createParamsValidateError(`${type} error:projection is null`)); return; } if (!this.imageHasInjected(imageId)) { reject(createParamsValidateError('not find image by imageId:' + imageId)); return; } const imageInfo = IMAGEMAP[imageId]; const image = imageTile(imageInfo, options); if (needPostProcessingImage(options) || needFormatImageType(options)) { (options as any).image = image; const buffers = checkBuffers(image); this.send(Object.assign({}, options, { __type: 'tilePostAndToBlobURL' }), buffers, (error, url) => { if (isErrorOrCancel(error, promise)) { reject(error || TaskCancelError); } else { resolve(url); } }); } else { const tid = setTimeout(() => { if (isErrorOrCancel(null, promise)) { disposeImage(image); reject(TaskCancelError); return; } removeTimeOut(tid); resolve(image); }, 1); } }); wrapPromise(promise, options); return promise; } getVTTile(options: getVTTileOptions) { const type = 'getVTTile' options = checkOptions(options, type); const { workerId } = getTaskId(options); const promise = new Promise((resolve: (buffer: ArrayBuffer) => void, reject: rejectResultType) => { const { url, customProperties } = options; if (!url) { reject(createParamsValidateError(`${type} error:url is null`)); return; } if (isFunction(customProperties)) { const body = getFunctionBody(customProperties.toString()); (options as any).customProperties = body; } const buffers = checkBuffers(url); this.send(options, buffers, (error, buffer) => { if (isErrorOrCancel(error, promise)) { reject(error || FetchCancelError); } else { resolve(buffer); } }, workerId); }); wrapPromise(promise, options); return promise; } } let actor: TileActor; let logger = false; export function getTileActor(): TileActor | null { if (!getCanvas()) { if (!logger) { console.error(CANVAS_ERROR_MESSAGE); logger = true; } return null; } if (!actor) { actor = new TileActor(WORKERNAME); } return actor; } let globalWorkerId = 0; function getWorkerId() { const actor = getTileActor(); const workers = actor.workers || []; const id = globalWorkerId % workers.length; globalWorkerId++; return id; } function wrapPromise(promise: Promise, options) { promise.cancel = () => { getTileActor()._cancelTask(options); promise.canceled = true; } }