diff --git a/src/ContentMessages.js b/src/ContentMessages.js
index 2d58622db8..dab8de2465 100644
--- a/src/ContentMessages.js
+++ b/src/ContentMessages.js
@@ -59,40 +59,38 @@ export class UploadCanceledError extends Error {}
* and a thumbnail key.
*/
function createThumbnail(element, inputWidth, inputHeight, mimeType) {
- const deferred = Promise.defer();
+ return new Promise((resolve) => {
+ let targetWidth = inputWidth;
+ let targetHeight = inputHeight;
+ if (targetHeight > MAX_HEIGHT) {
+ targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight));
+ targetHeight = MAX_HEIGHT;
+ }
+ if (targetWidth > MAX_WIDTH) {
+ targetHeight = Math.floor(targetHeight * (MAX_WIDTH / targetWidth));
+ targetWidth = MAX_WIDTH;
+ }
- let targetWidth = inputWidth;
- let targetHeight = inputHeight;
- if (targetHeight > MAX_HEIGHT) {
- targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight));
- targetHeight = MAX_HEIGHT;
- }
- if (targetWidth > MAX_WIDTH) {
- targetHeight = Math.floor(targetHeight * (MAX_WIDTH / targetWidth));
- targetWidth = MAX_WIDTH;
- }
-
- const canvas = document.createElement("canvas");
- canvas.width = targetWidth;
- canvas.height = targetHeight;
- canvas.getContext("2d").drawImage(element, 0, 0, targetWidth, targetHeight);
- canvas.toBlob(function(thumbnail) {
- deferred.resolve({
- info: {
- thumbnail_info: {
- w: targetWidth,
- h: targetHeight,
- mimetype: thumbnail.type,
- size: thumbnail.size,
+ const canvas = document.createElement("canvas");
+ canvas.width = targetWidth;
+ canvas.height = targetHeight;
+ canvas.getContext("2d").drawImage(element, 0, 0, targetWidth, targetHeight);
+ canvas.toBlob(function(thumbnail) {
+ resolve({
+ info: {
+ thumbnail_info: {
+ w: targetWidth,
+ h: targetHeight,
+ mimetype: thumbnail.type,
+ size: thumbnail.size,
+ },
+ w: inputWidth,
+ h: inputHeight,
},
- w: inputWidth,
- h: inputHeight,
- },
- thumbnail: thumbnail,
- });
- }, mimeType);
-
- return deferred.promise;
+ thumbnail: thumbnail,
+ });
+ }, mimeType);
+ });
}
/**
@@ -179,30 +177,29 @@ function infoForImageFile(matrixClient, roomId, imageFile) {
* @return {Promise} A promise that resolves with the video image element.
*/
function loadVideoElement(videoFile) {
- const deferred = Promise.defer();
+ return new Promise((resolve, reject) => {
+ // Load the file into an html element
+ const video = document.createElement("video");
- // Load the file into an html element
- const video = document.createElement("video");
+ const reader = new FileReader();
- const reader = new FileReader();
- reader.onload = function(e) {
- video.src = e.target.result;
+ reader.onload = function(e) {
+ video.src = e.target.result;
- // Once ready, returns its size
- // Wait until we have enough data to thumbnail the first frame.
- video.onloadeddata = function() {
- deferred.resolve(video);
+ // Once ready, returns its size
+ // Wait until we have enough data to thumbnail the first frame.
+ video.onloadeddata = function() {
+ resolve(video);
+ };
+ video.onerror = function(e) {
+ reject(e);
+ };
};
- video.onerror = function(e) {
- deferred.reject(e);
+ reader.onerror = function(e) {
+ reject(e);
};
- };
- reader.onerror = function(e) {
- deferred.reject(e);
- };
- reader.readAsDataURL(videoFile);
-
- return deferred.promise;
+ reader.readAsDataURL(videoFile);
+ });
}
/**
@@ -236,16 +233,16 @@ function infoForVideoFile(matrixClient, roomId, videoFile) {
* is read.
*/
function readFileAsArrayBuffer(file) {
- const deferred = Promise.defer();
- const reader = new FileReader();
- reader.onload = function(e) {
- deferred.resolve(e.target.result);
- };
- reader.onerror = function(e) {
- deferred.reject(e);
- };
- reader.readAsArrayBuffer(file);
- return deferred.promise;
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = function(e) {
+ resolve(e.target.result);
+ };
+ reader.onerror = function(e) {
+ reject(e);
+ };
+ reader.readAsArrayBuffer(file);
+ });
}
/**
@@ -461,33 +458,34 @@ export default class ContentMessages {
content.info.mimetype = file.type;
}
- const def = Promise.defer();
- if (file.type.indexOf('image/') == 0) {
- content.msgtype = 'm.image';
- infoForImageFile(matrixClient, roomId, file).then((imageInfo)=>{
- extend(content.info, imageInfo);
- def.resolve();
- }, (error)=>{
- console.error(error);
+ const prom = new Promise((resolve) => {
+ if (file.type.indexOf('image/') == 0) {
+ content.msgtype = 'm.image';
+ infoForImageFile(matrixClient, roomId, file).then((imageInfo)=>{
+ extend(content.info, imageInfo);
+ resolve();
+ }, (error)=>{
+ console.error(error);
+ content.msgtype = 'm.file';
+ resolve();
+ });
+ } else if (file.type.indexOf('audio/') == 0) {
+ content.msgtype = 'm.audio';
+ resolve();
+ } else if (file.type.indexOf('video/') == 0) {
+ content.msgtype = 'm.video';
+ infoForVideoFile(matrixClient, roomId, file).then((videoInfo)=>{
+ extend(content.info, videoInfo);
+ resolve();
+ }, (error)=>{
+ content.msgtype = 'm.file';
+ resolve();
+ });
+ } else {
content.msgtype = 'm.file';
- def.resolve();
- });
- } else if (file.type.indexOf('audio/') == 0) {
- content.msgtype = 'm.audio';
- def.resolve();
- } else if (file.type.indexOf('video/') == 0) {
- content.msgtype = 'm.video';
- infoForVideoFile(matrixClient, roomId, file).then((videoInfo)=>{
- extend(content.info, videoInfo);
- def.resolve();
- }, (error)=>{
- content.msgtype = 'm.file';
- def.resolve();
- });
- } else {
- content.msgtype = 'm.file';
- def.resolve();
- }
+ resolve();
+ }
+ });
const upload = {
fileName: file.name || 'Attachment',
@@ -509,7 +507,7 @@ export default class ContentMessages {
dis.dispatch({action: 'upload_progress', upload: upload});
}
- return def.promise.then(function() {
+ return prom.then(function() {
// XXX: upload.promise must be the promise that
// is returned by uploadFile as it has an abort()
// method hacked onto it.
diff --git a/src/Lifecycle.js b/src/Lifecycle.js
index f2b50d7f2d..ffd5baace4 100644
--- a/src/Lifecycle.js
+++ b/src/Lifecycle.js
@@ -313,18 +313,14 @@ async function _restoreFromLocalStorage(opts) {
function _handleLoadSessionFailure(e) {
console.error("Unable to load session", e);
- const def = Promise.defer();
const SessionRestoreErrorDialog =
sdk.getComponent('views.dialogs.SessionRestoreErrorDialog');
- Modal.createTrackedDialog('Session Restore Error', '', SessionRestoreErrorDialog, {
+ const modal = Modal.createTrackedDialog('Session Restore Error', '', SessionRestoreErrorDialog, {
error: e.message,
- onFinished: (success) => {
- def.resolve(success);
- },
});
- return def.promise.then((success) => {
+ return modal.finished.then(([success]) => {
if (success) {
// user clicked continue.
_clearStorage();
diff --git a/src/Modal.js b/src/Modal.js
index 26c9da8bbb..cb19731f01 100644
--- a/src/Modal.js
+++ b/src/Modal.js
@@ -24,6 +24,7 @@ import sdk from './index';
import dis from './dispatcher';
import { _t } from './languageHandler';
import Promise from "bluebird";
+import {defer} from "./utils/promise";
const DIALOG_CONTAINER_ID = "mx_Dialog_Container";
const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer";
@@ -202,7 +203,7 @@ class ModalManager {
}
_getCloseFn(modal, props) {
- const deferred = Promise.defer();
+ const deferred = defer();
return [(...args) => {
deferred.resolve(args);
if (props && props.onFinished) props.onFinished.apply(null, args);
diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js
index 4d8f47003c..4056557a7c 100644
--- a/src/components/structures/GroupView.js
+++ b/src/components/structures/GroupView.js
@@ -38,6 +38,7 @@ import FlairStore from '../../stores/FlairStore';
import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
import {makeGroupPermalink, makeUserPermalink} from "../../utils/permalinks/Permalinks";
import {Group} from "matrix-js-sdk";
+import {sleep} from "../../utils/promise";
const LONG_DESC_PLACEHOLDER = _td(
`
HTML for your community's page
@@ -692,7 +693,7 @@ export default createReactClass({
// Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the
// spinner disappearing after we have fetched new group data.
- await Promise.delay(500);
+ await sleep(500);
GroupStore.acceptGroupInvite(this.props.groupId).then(() => {
// don't reset membershipBusy here: wait for the membership change to come down the sync
@@ -711,7 +712,7 @@ export default createReactClass({
// Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the
// spinner disappearing after we have fetched new group data.
- await Promise.delay(500);
+ await sleep(500);
GroupStore.leaveGroup(this.props.groupId).then(() => {
// don't reset membershipBusy here: wait for the membership change to come down the sync
@@ -735,7 +736,7 @@ export default createReactClass({
// Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the
// spinner disappearing after we have fetched new group data.
- await Promise.delay(500);
+ await sleep(500);
GroupStore.joinGroup(this.props.groupId).then(() => {
// don't reset membershipBusy here: wait for the membership change to come down the sync
@@ -787,7 +788,7 @@ export default createReactClass({
// Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the
// spinner disappearing after we have fetched new group data.
- await Promise.delay(500);
+ await sleep(500);
GroupStore.leaveGroup(this.props.groupId).then(() => {
// don't reset membershipBusy here: wait for the membership change to come down the sync
diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js
index cd5b27f2b9..620e73bf93 100644
--- a/src/components/structures/MatrixChat.js
+++ b/src/components/structures/MatrixChat.js
@@ -61,6 +61,7 @@ import DMRoomMap from '../../utils/DMRoomMap';
import { countRoomsWithNotif } from '../../RoomNotifs';
import { setTheme } from "../../theme";
import { storeRoomAliasInCache } from '../../RoomAliasCache';
+import { defer } from "../../utils/promise";
// Disable warnings for now: we use deprecated bluebird functions
// and need to migrate, but they spam the console with warnings.
@@ -237,7 +238,7 @@ export default createReactClass({
// Used by _viewRoom before getting state from sync
this.firstSyncComplete = false;
- this.firstSyncPromise = Promise.defer();
+ this.firstSyncPromise = defer();
if (this.props.config.sync_timeline_limit) {
MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit;
@@ -1267,7 +1268,7 @@ export default createReactClass({
// since we're about to start the client and therefore about
// to do the first sync
this.firstSyncComplete = false;
- this.firstSyncPromise = Promise.defer();
+ this.firstSyncPromise = defer();
const cli = MatrixClientPeg.get();
const IncomingSasDialog = sdk.getComponent('views.dialogs.IncomingSasDialog');
diff --git a/src/components/views/context_menus/RoomTileContextMenu.js b/src/components/views/context_menus/RoomTileContextMenu.js
index 9bb573026f..fb056ee47f 100644
--- a/src/components/views/context_menus/RoomTileContextMenu.js
+++ b/src/components/views/context_menus/RoomTileContextMenu.js
@@ -17,7 +17,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import Promise from 'bluebird';
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
@@ -32,6 +31,7 @@ import * as RoomNotifs from '../../../RoomNotifs';
import Modal from '../../../Modal';
import RoomListActions from '../../../actions/RoomListActions';
import RoomViewStore from '../../../stores/RoomViewStore';
+import {sleep} from "../../../utils/promise";
module.exports = createReactClass({
displayName: 'RoomTileContextMenu',
@@ -62,7 +62,7 @@ module.exports = createReactClass({
_toggleTag: function(tagNameOn, tagNameOff) {
if (!MatrixClientPeg.get().isGuest()) {
- Promise.delay(500).then(() => {
+ sleep(500).then(() => {
dis.dispatch(RoomListActions.tagRoom(
MatrixClientPeg.get(),
this.props.room,
@@ -119,7 +119,7 @@ module.exports = createReactClass({
Rooms.guessAndSetDMRoom(
this.props.room, newIsDirectMessage,
- ).delay(500).finally(() => {
+ ).then(sleep(500)).finally(() => {
// Close the context menu
if (this.props.onFinished) {
this.props.onFinished();
@@ -193,7 +193,7 @@ module.exports = createReactClass({
RoomNotifs.setRoomNotifsState(roomId, newState).done(() => {
// delay slightly so that the user can see their state change
// before closing the menu
- return Promise.delay(500).then(() => {
+ return sleep(500).then(() => {
if (this._unmounted) return;
// Close the context menu
if (this.props.onFinished) {
diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js
index fb779fa96f..24d8b96e0c 100644
--- a/src/components/views/dialogs/AddressPickerDialog.js
+++ b/src/components/views/dialogs/AddressPickerDialog.js
@@ -25,13 +25,13 @@ import { _t, _td } from '../../../languageHandler';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
import dis from '../../../dispatcher';
-import Promise from 'bluebird';
import { addressTypes, getAddressType } from '../../../UserAddress.js';
import GroupStore from '../../../stores/GroupStore';
import * as Email from '../../../email';
import IdentityAuthClient from '../../../IdentityAuthClient';
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from '../../../utils/IdentityServerUtils';
import { abbreviateUrl } from '../../../utils/UrlUtils';
+import {sleep} from "../../../utils/promise";
const TRUNCATE_QUERY_LIST = 40;
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
@@ -533,7 +533,7 @@ module.exports = createReactClass({
};
// wait a bit to let the user finish typing
- await Promise.delay(500);
+ await sleep(500);
if (cancelled) return null;
try {
diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js
index ad5fa198a3..d4b51081f4 100644
--- a/src/components/views/rooms/Autocomplete.js
+++ b/src/components/views/rooms/Autocomplete.js
@@ -26,6 +26,7 @@ import { Room } from 'matrix-js-sdk';
import SettingsStore from "../../../settings/SettingsStore";
import Autocompleter from '../../../autocomplete/Autocompleter';
+import {sleep} from "../../../utils/promise";
const COMPOSER_SELECTED = 0;
@@ -105,13 +106,11 @@ export default class Autocomplete extends React.Component {
autocompleteDelay = 0;
}
- const deferred = Promise.defer();
- this.debounceCompletionsRequest = setTimeout(() => {
- this.processQuery(query, selection).then(() => {
- deferred.resolve();
- });
- }, autocompleteDelay);
- return deferred.promise;
+ return new Promise((resolve) => {
+ this.debounceCompletionsRequest = setTimeout(() => {
+ resolve(this.processQuery(query, selection));
+ }, autocompleteDelay);
+ });
}
processQuery(query, selection) {
@@ -197,16 +196,16 @@ export default class Autocomplete extends React.Component {
}
forceComplete() {
- const done = Promise.defer();
- this.setState({
- forceComplete: true,
- hide: false,
- }, () => {
- this.complete(this.props.query, this.props.selection).then(() => {
- done.resolve(this.countCompletions());
+ return new Promise((resolve) => {
+ this.setState({
+ forceComplete: true,
+ hide: false,
+ }, () => {
+ this.complete(this.props.query, this.props.selection).then(() => {
+ resolve(this.countCompletions());
+ });
});
});
- return done.promise;
}
onCompletionClicked(selectionOffset: number): boolean {
diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js
index a086efaa6d..91292b19f9 100644
--- a/src/components/views/settings/ChangePassword.js
+++ b/src/components/views/settings/ChangePassword.js
@@ -178,17 +178,12 @@ module.exports = createReactClass({
},
_optionallySetEmail: function() {
- const deferred = Promise.defer();
// Ask for an email otherwise the user has no way to reset their password
const SetEmailDialog = sdk.getComponent("dialogs.SetEmailDialog");
- Modal.createTrackedDialog('Do you want to set an email address?', '', SetEmailDialog, {
+ const modal = Modal.createTrackedDialog('Do you want to set an email address?', '', SetEmailDialog, {
title: _t('Do you want to set an email address?'),
- onFinished: (confirmed) => {
- // ignore confirmed, setting an email is optional
- deferred.resolve(confirmed);
- },
});
- return deferred.promise;
+ return modal.finished.then(([confirmed]) => confirmed);
},
_onExportE2eKeysClicked: function() {
diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js
index 126cdc9557..a7a2e01c22 100644
--- a/src/components/views/settings/SetIdServer.js
+++ b/src/components/views/settings/SetIdServer.js
@@ -26,6 +26,7 @@ import { getThreepidsWithBindStatus } from '../../../boundThreepids';
import IdentityAuthClient from "../../../IdentityAuthClient";
import {abbreviateUrl, unabbreviateUrl} from "../../../utils/UrlUtils";
import { getDefaultIdentityServerUrl, doesIdentityServerHaveTerms } from '../../../utils/IdentityServerUtils';
+import {timeout} from "../../../utils/promise";
// We'll wait up to this long when checking for 3PID bindings on the IS.
const REACHABILITY_TIMEOUT = 10000; // ms
@@ -245,14 +246,11 @@ export default class SetIdServer extends React.Component {
let threepids = [];
let currentServerReachable = true;
try {
- threepids = await Promise.race([
+ threepids = await timeout(
getThreepidsWithBindStatus(MatrixClientPeg.get()),
- new Promise((resolve, reject) => {
- setTimeout(() => {
- reject(new Error("Timeout attempting to reach identity server"));
- }, REACHABILITY_TIMEOUT);
- }),
- ]);
+ Promise.reject(new Error("Timeout attempting to reach identity server")),
+ REACHABILITY_TIMEOUT,
+ );
} catch (e) {
currentServerReachable = false;
console.warn(
diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js
index e619791b01..0732bcf926 100644
--- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js
@@ -22,9 +22,9 @@ import MatrixClientPeg from "../../../../../MatrixClientPeg";
import * as FormattingUtils from "../../../../../utils/FormattingUtils";
import AccessibleButton from "../../../elements/AccessibleButton";
import Analytics from "../../../../../Analytics";
-import Promise from "bluebird";
import Modal from "../../../../../Modal";
import sdk from "../../../../..";
+import {sleep} from "../../../../../utils/promise";
export class IgnoredUser extends React.Component {
static propTypes = {
@@ -129,7 +129,7 @@ export default class SecurityUserSettingsTab extends React.Component {
if (e.errcode === "M_LIMIT_EXCEEDED") {
// Add a delay between each invite change in order to avoid rate
// limiting by the server.
- await Promise.delay(e.retry_after_ms || 2500);
+ await sleep(e.retry_after_ms || 2500);
// Redo last action
i--;
diff --git a/src/rageshake/submit-rageshake.js b/src/rageshake/submit-rageshake.js
index 99c412a6ab..e772912e48 100644
--- a/src/rageshake/submit-rageshake.js
+++ b/src/rageshake/submit-rageshake.js
@@ -105,26 +105,22 @@ export default async function sendBugReport(bugReportEndpoint, opts) {
}
function _submitReport(endpoint, body, progressCallback) {
- const deferred = Promise.defer();
-
- const req = new XMLHttpRequest();
- req.open("POST", endpoint);
- req.timeout = 5 * 60 * 1000;
- req.onreadystatechange = function() {
- if (req.readyState === XMLHttpRequest.LOADING) {
- progressCallback(_t("Waiting for response from server"));
- } else if (req.readyState === XMLHttpRequest.DONE) {
- on_done();
- }
- };
- req.send(body);
- return deferred.promise;
-
- function on_done() {
- if (req.status < 200 || req.status >= 400) {
- deferred.reject(new Error(`HTTP ${req.status}`));
- return;
- }
- deferred.resolve();
- }
+ return new Promise((resolve, reject) => {
+ const req = new XMLHttpRequest();
+ req.open("POST", endpoint);
+ req.timeout = 5 * 60 * 1000;
+ req.onreadystatechange = function() {
+ if (req.readyState === XMLHttpRequest.LOADING) {
+ progressCallback(_t("Waiting for response from server"));
+ } else if (req.readyState === XMLHttpRequest.DONE) {
+ // on done
+ if (req.status < 200 || req.status >= 400) {
+ reject(new Error(`HTTP ${req.status}`));
+ return;
+ }
+ resolve();
+ }
+ };
+ req.send(body);
+ });
}
diff --git a/src/utils/MultiInviter.js b/src/utils/MultiInviter.js
index e8995b46d7..de5c2e7610 100644
--- a/src/utils/MultiInviter.js
+++ b/src/utils/MultiInviter.js
@@ -24,6 +24,7 @@ import {_t} from "../languageHandler";
import sdk from "../index";
import Modal from "../Modal";
import SettingsStore from "../settings/SettingsStore";
+import {defer} from "./promise";
/**
* Invites multiple addresses to a room or group, handling rate limiting from the server
@@ -71,7 +72,7 @@ export default class MultiInviter {
};
}
}
- this.deferred = Promise.defer();
+ this.deferred = defer();
this._inviteMore(0);
return this.deferred.promise;
diff --git a/src/utils/promise.js b/src/utils/promise.js
new file mode 100644
index 0000000000..f7a2e7c3e7
--- /dev/null
+++ b/src/utils/promise.js
@@ -0,0 +1,49 @@
+/*
+Copyright 2019 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// This is only here to allow access to methods like done for the time being
+import Promise from "bluebird";
+
+// @flow
+
+// Returns a promise which resolves with a given value after the given number of ms
+export const sleep = (ms: number, value: any): Promise => new Promise((resolve => { setTimeout(resolve, ms, value); }));
+
+// Returns a promise which resolves when the input promise resolves with its value
+// or when the timeout of ms is reached with the value of given timeoutValue
+export async function timeout(promise: Promise, timeoutValue: any, ms: number): Promise {
+ const timeoutPromise = new Promise((resolve) => {
+ const timeoutId = setTimeout(resolve, ms, timeoutValue);
+ promise.then(() => {
+ clearTimeout(timeoutId);
+ });
+ });
+
+ return Promise.race([promise, timeoutPromise]);
+}
+
+// Returns a Deferred
+export function defer(): {resolve: () => {}, reject: () => {}, promise: Promise} {
+ let resolve;
+ let reject;
+
+ const promise = new Promise((_resolve, _reject) => {
+ resolve = _resolve;
+ reject = _reject;
+ });
+
+ return {resolve, reject, promise};
+}