Make naming convention consistent, and synchronize code
This commit is contained in:
@@ -3,7 +3,6 @@
|
||||
if(typeof window === 'undefined') {
|
||||
self.addEventListener("install", () => self.skipWaiting());
|
||||
self.addEventListener("activate", e => e.waitUntil(self.clients.claim()));
|
||||
|
||||
async function handleFetch(request) {
|
||||
if(request.cache === "only-if-cached" && request.mode !== "same-origin") {
|
||||
return;
|
||||
|
||||
@@ -2,25 +2,23 @@
|
||||
#include "model.h"
|
||||
#include "recognizer.h"
|
||||
using namespace emscripten;
|
||||
|
||||
EMSCRIPTEN_BINDINGS(BrowserRecognizer) {
|
||||
EMSCRIPTEN_BINDINGS() {
|
||||
function("setLogLevel", &vosk_set_log_level, allow_raw_pointers());
|
||||
|
||||
class_<Model>("__Model__")
|
||||
class_<model, base<genericModel>>("__model__")
|
||||
.constructor<std::string, std::string, std::string, int>(allow_raw_pointers());
|
||||
|
||||
class_<SpkModel>("__SpkModel__")
|
||||
class_<spkModel, base<genericModel>>("__spkModel__")
|
||||
.constructor<std::string, std::string, std::string, const int>(allow_raw_pointers());
|
||||
|
||||
class_<Recognizer>("__Recognizer__")
|
||||
.constructor<Model*, int, int>(allow_raw_pointers())
|
||||
.function("start", &Recognizer::start, allow_raw_pointers())
|
||||
.function("stop", &Recognizer::stop, allow_raw_pointers())
|
||||
.function("deinit", &Recognizer::deinit, allow_raw_pointers())
|
||||
.function("setWords", &Recognizer::setWords, allow_raw_pointers())
|
||||
.function("setPartialWords", &Recognizer::setPartialWords, allow_raw_pointers())
|
||||
.function("setGrm", &Recognizer::setGrm, allow_raw_pointers())
|
||||
.function("setNLSML", &Recognizer::setNLSML, allow_raw_pointers())
|
||||
.function("setSpkModel", &Recognizer::setSpkModel, allow_raw_pointers())
|
||||
.function("setMaxAlternatives", &Recognizer::setMaxAlternatives, allow_raw_pointers());
|
||||
class_<recognizer, base<genericObj>>("__recognizer__")
|
||||
.constructor<model*, int, int>(allow_raw_pointers())
|
||||
.function("start", &recognizer::start, allow_raw_pointers())
|
||||
.function("stop", &recognizer::stop, allow_raw_pointers())
|
||||
.function("deinit", &recognizer::deinit, allow_raw_pointers())
|
||||
.function("setWords", &recognizer::setWords, allow_raw_pointers())
|
||||
.function("setPartialWords", &recognizer::setPartialWords, allow_raw_pointers())
|
||||
.function("setGrm", &recognizer::setGrm, allow_raw_pointers())
|
||||
.function("setNLSML", &recognizer::setNLSML, allow_raw_pointers())
|
||||
.function("setSpkModel", &recognizer::setSpkModel, allow_raw_pointers())
|
||||
.function("setMaxAlternatives", &recognizer::setMaxAlternatives, allow_raw_pointers());
|
||||
};
|
||||
@@ -1,9 +1,10 @@
|
||||
#include "genericModel.h"
|
||||
|
||||
bool GenericModel::first = true;
|
||||
GenericModel::GenericModel(const std::string &url, const std::string& storepath, const std::string &id, int index) : url(url), id(id), storepath(("opfs/" + storepath)), GenericObj(index) {
|
||||
bool genericModel::first = true;
|
||||
genericModel::genericModel(const std::string &url, const std::string& storepath, const std::string &id, int index) : url(url), id(id), genericObj(index) {
|
||||
fs::current_path(storepath);
|
||||
if(first) {
|
||||
vosk_set_log_level(-1);
|
||||
vosk_set_log_level(0);
|
||||
int res{};
|
||||
std::thread t{[&res](){
|
||||
res = wasmfs_create_directory("opfs",0777,wasmfs_create_opfs_backend());
|
||||
@@ -16,8 +17,8 @@ GenericModel::GenericModel(const std::string &url, const std::string& storepath,
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
bool GenericModel::checkId(const std::string& path, const std::string& id) {
|
||||
std::ifstream file {(path + "/id"), std::ifstream::binary};
|
||||
bool genericModel::checkId(const std::string& id) {
|
||||
std::ifstream file {"id", std::ifstream::binary};
|
||||
if(!file.is_open()) {
|
||||
return false;
|
||||
};
|
||||
@@ -27,36 +28,36 @@ bool GenericModel::checkId(const std::string& path, const std::string& id) {
|
||||
file.read(&oldid[0], size);
|
||||
return id.compare(oldid) == 0 ? true : false;
|
||||
}
|
||||
bool GenericModel::loadModel() {
|
||||
if(!checkModel(storepath) || !checkId(storepath, id)) {
|
||||
if(emscripten_wget(url.c_str(), "opfs/model.tzst") == 1) {
|
||||
bool genericModel::loadModel() {
|
||||
if(!checkModel() || !checkId(id)) {
|
||||
if(emscripten_wget(url.c_str(),"opfs/model.tzst") == 1) {
|
||||
fireEv("error", "Unable to fetch model");
|
||||
return false;
|
||||
}
|
||||
if(!extractModel("opfs/model.tzst", storepath)) {
|
||||
if(!extractModel("opfs/model.tzst", ".")) {
|
||||
fireEv("error", "Unable to extract model");
|
||||
}
|
||||
fs::remove("opfs/model.tzst");
|
||||
if(!checkModel(storepath)) {
|
||||
if(!checkModel()) {
|
||||
fireEv("error", "Model URL contains invalid model files");
|
||||
}
|
||||
std::ofstream idFile((storepath + "/id"));
|
||||
std::ofstream idFile("id");
|
||||
if(!idFile.is_open()) {
|
||||
fireEv("error", "Unable to write new id");
|
||||
fs::remove_all(storepath);
|
||||
fs::remove_all(".");
|
||||
return false;
|
||||
}
|
||||
idFile << id;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool GenericModel::extractModel(const char* target, const std::string& dest) {
|
||||
bool genericModel::extractModel(const char* target, const std::string& dest) {
|
||||
std::string path{};
|
||||
archive* src {archive_read_new()};
|
||||
archive_entry* entry {};
|
||||
archive_read_support_filter_all(src);
|
||||
archive_read_support_format_all(src);
|
||||
archive_read_open_filename(src, target,22480);
|
||||
archive_read_open_filename(src, ".",10240);
|
||||
if(archive_errno(src) != 0) return false;
|
||||
while (archive_read_next_header(src, &entry) == ARCHIVE_OK) {
|
||||
path = archive_entry_pathname(entry);
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
#include "genericObj.h"
|
||||
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
@@ -14,15 +13,13 @@
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
class GenericModel : public GenericObj {
|
||||
struct genericModel : genericObj {
|
||||
static bool first;
|
||||
const std::string url{};
|
||||
const std::string id{};
|
||||
static bool extractModel(const char* target, const std::string& dest);
|
||||
static bool checkId(const std::string& path, const std::string& id);
|
||||
public:
|
||||
const std::string storepath{};
|
||||
virtual bool checkModel(const std::string& path) = 0;
|
||||
static bool checkId(const std::string& id);
|
||||
virtual bool checkModel() = 0;
|
||||
bool loadModel();
|
||||
GenericModel(const std::string& url, const std::string& storepath, const std::string& id, int index);
|
||||
genericModel(const std::string& url, const std::string& storepath, const std::string& id, int index);
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
#include "genericObj.h"
|
||||
|
||||
void GenericObj::fireEv(const char *type, const char *content) {
|
||||
void genericObj::fireEv(const char *type, const char *content) {
|
||||
if(content == nullptr) {
|
||||
MAIN_THREAD_EM_ASM({
|
||||
__GenericObj__.objects[$0].dispatchEvent(new Event(UTF8ToString($1)));
|
||||
EM_ASM({
|
||||
__genericObj__.objects[$0].dispatchEvent(new Event(UTF8ToString($1)));
|
||||
},this->index, type);
|
||||
return;
|
||||
}
|
||||
MAIN_THREAD_EM_ASM({
|
||||
__GenericObj__.objects[$0].dispatchEvent(new CustomEvent(UTF8ToString($0), {"details" : UTF8ToString($1)}));
|
||||
EM_ASM({
|
||||
__genericObj__.objects[$0].dispatchEvent(new CustomEvent(UTF8ToString($0), {"details" : UTF8ToString($1)}));
|
||||
},this->index, type, content);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
#pragma once
|
||||
#include <emscripten.h>
|
||||
|
||||
class GenericObj {
|
||||
#include <emscripten.h>
|
||||
#include <emscripten/console.h>
|
||||
|
||||
struct genericObj {
|
||||
const int index{};
|
||||
public:
|
||||
GenericObj(int index) : index(index) {};
|
||||
genericObj(int index) : index(index) {};
|
||||
void fireEv(const char *type, const char *content = nullptr);
|
||||
};
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
class __GenericObj__ {static objects = []}
|
||||
class __genericObj__ {static objects = []}
|
||||
34
src/model.cc
34
src/model.cc
@@ -1,27 +1,27 @@
|
||||
#include "model.h"
|
||||
|
||||
Model::Model(const std::string &url, const std::string& storepath, const std::string& id, int index) : GenericModel(url, storepath, id, index) {
|
||||
model::model(const std::string &url, const std::string& storepath, const std::string& id, int index) : genericModel(url, id, storepath, index) {
|
||||
if(!loadModel()) return;
|
||||
model = vosk_model_new(this->storepath.c_str());
|
||||
if(model == nullptr) {
|
||||
mdl = vosk_model_new(".");
|
||||
if(mdl == nullptr) {
|
||||
fireEv("error", "Unable to initialize model");
|
||||
return;
|
||||
}
|
||||
fireEv("ready");
|
||||
};
|
||||
|
||||
bool Model::checkModel(const std::string& path) {
|
||||
return fs::exists(path + "/am/final.mdl") &&
|
||||
fs::exists(path + "/conf/mfcc.conf") &&
|
||||
fs::exists(path + "/conf/model.conf") &&
|
||||
fs::exists(path + "/graph/phones/word_boundary.int") &&
|
||||
fs::exists(path + "/graph/Gr.fst") &&
|
||||
fs::exists(path + "/graph/HCLr.fst") &&
|
||||
fs::exists(path + "/graph/disambig_tid.int") &&
|
||||
fs::exists(path + "/ivector/final.dubm") &&
|
||||
fs::exists(path + "/ivector/final.ie") &&
|
||||
fs::exists(path + "/ivector/final.mat") &&
|
||||
fs::exists(path + "/ivector/global_cmvn.stats") &&
|
||||
fs::exists(path + "/ivector/online_cmvn.conf") &&
|
||||
fs::exists(path + "/ivector/splice.conf");
|
||||
bool model::checkModel() {
|
||||
return fs::exists("am/final.mdl") &&
|
||||
fs::exists("conf/mfcc.conf") &&
|
||||
fs::exists("conf/model.conf") &&
|
||||
fs::exists("graph/phones/word_boundary.int") &&
|
||||
fs::exists("graph/Gr.fst") &&
|
||||
fs::exists("graph/HCLr.fst") &&
|
||||
fs::exists("graph/disambig_tid.int") &&
|
||||
fs::exists("ivector/final.dubm") &&
|
||||
fs::exists("ivector/final.ie") &&
|
||||
fs::exists("ivector/final.mat") &&
|
||||
fs::exists("ivector/global_cmvn.stats") &&
|
||||
fs::exists("ivector/online_cmvn.conf") &&
|
||||
fs::exists("ivector/splice.conf");
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
#pragma once
|
||||
#include "genericModel.h"
|
||||
|
||||
class Model : public GenericModel {
|
||||
bool checkModel(const std::string& path);
|
||||
public:
|
||||
VoskModel* model{};
|
||||
Model(const std::string &url, const std::string& storepath, const std::string& id, int index);
|
||||
struct model : genericModel {
|
||||
bool checkModel();
|
||||
VoskModel* mdl{};
|
||||
model(const std::string &url, const std::string& storepath, const std::string& id, int index);
|
||||
};
|
||||
|
||||
|
||||
|
||||
10
src/model.js
10
src/model.js
@@ -1,10 +1,14 @@
|
||||
class Model extends EventTarget{
|
||||
constructor(url, storepath, id) {
|
||||
super()
|
||||
this.obj = new Module.__Model__(url, storepath, id, __GenericObj__.objects.length)
|
||||
__GenericObj__.objects.push(this)
|
||||
this.obj = (async () => {
|
||||
return new Module.__model__(url, storepath, id, __genericObj__.objects.length)
|
||||
})()
|
||||
__genericObj__.objects.push(this)
|
||||
}
|
||||
delete() {
|
||||
this.obj.delete()
|
||||
this.obj.then(() => {
|
||||
this.obj.delete()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,31 @@
|
||||
#include "./recognizer.h"
|
||||
void Recognizer::start() {
|
||||
void recognizer::start() {
|
||||
controller.test_and_set(std::memory_order_relaxed);
|
||||
controller.notify_all();
|
||||
}
|
||||
void Recognizer::stop() {
|
||||
void recognizer::stop() {
|
||||
controller.clear(std::memory_order_relaxed);
|
||||
controller.notify_all();
|
||||
}
|
||||
void Recognizer::deinit() {
|
||||
void recognizer::deinit() {
|
||||
done.test_and_set(std::memory_order_relaxed);
|
||||
done.notify_all();
|
||||
stop();
|
||||
}
|
||||
Recognizer::Recognizer(Model* model, int sampleRate, int index) : GenericObj(index) {
|
||||
recognizer::recognizer(model* mdl, int sampleRate, int index) : genericObj(index) {
|
||||
mic = alcCaptureOpenDevice("Emscripten OpenAL capture",sampleRate, AL_FORMAT_MONO16, 22480);
|
||||
if(alcGetError(mic) != 0) {
|
||||
fireEv("error", "Unable to initialize microphone");
|
||||
return;
|
||||
}
|
||||
std::thread t{[this](Model* model, int sampleRate) {
|
||||
recognizer = vosk_recognizer_new(model->model,static_cast<float>(sampleRate));
|
||||
if(recognizer == nullptr) {
|
||||
fireEv("error", "Unable to construct recognizer");
|
||||
return;
|
||||
}
|
||||
main();
|
||||
}, model, sampleRate};
|
||||
t.detach();
|
||||
rec = vosk_recognizer_new(mdl->mdl,static_cast<float>(sampleRate));
|
||||
if(rec == nullptr) {
|
||||
fireEv("error", "Unable to construct recognizer");
|
||||
return;
|
||||
}
|
||||
main();
|
||||
}
|
||||
void Recognizer::main() {
|
||||
void recognizer::main() {
|
||||
char buffer[22480];
|
||||
int sample{};
|
||||
fireEv("ready");
|
||||
@@ -38,12 +35,12 @@ void Recognizer::main() {
|
||||
while(controller.test()) {
|
||||
alcGetIntegerv(mic, ALC_CAPTURE_SAMPLES, sizeof(int), &sample);
|
||||
alcCaptureSamples(mic, buffer, sample);
|
||||
switch(vosk_recognizer_accept_waveform(recognizer, buffer, 22480)) {
|
||||
switch(vosk_recognizer_accept_waveform(rec, buffer, 22480)) {
|
||||
case 0:
|
||||
fireEv("result", vosk_recognizer_result(recognizer));
|
||||
fireEv("result", vosk_recognizer_result(rec));
|
||||
break;
|
||||
case 1:
|
||||
fireEv("partialResult", vosk_recognizer_partial_result(recognizer));
|
||||
fireEv("partialResult", vosk_recognizer_partial_result(rec));
|
||||
break;
|
||||
default:
|
||||
fireEv("error", "Recognition result error");
|
||||
@@ -51,24 +48,24 @@ void Recognizer::main() {
|
||||
}
|
||||
alcCaptureStop(mic);
|
||||
}
|
||||
vosk_recognizer_free(recognizer);
|
||||
vosk_recognizer_free(rec);
|
||||
alcCaptureCloseDevice(mic);
|
||||
}
|
||||
void Recognizer::setGrm(const std::string& grm) {
|
||||
vosk_recognizer_set_grm(recognizer, grm.c_str());
|
||||
void recognizer::setGrm(const std::string& grm) {
|
||||
vosk_recognizer_set_grm(rec, grm.c_str());
|
||||
}
|
||||
void Recognizer::setSpkModel(SpkModel* model) {
|
||||
vosk_recognizer_set_spk_model(recognizer,model->model);
|
||||
void recognizer::setSpkModel(spkModel* mdl) {
|
||||
vosk_recognizer_set_spk_model(rec, mdl->mdl);
|
||||
}
|
||||
void Recognizer::setWords(bool words) {
|
||||
vosk_recognizer_set_words(recognizer,words);
|
||||
void recognizer::setWords(bool words) {
|
||||
vosk_recognizer_set_words(rec,words);
|
||||
}
|
||||
void Recognizer::setPartialWords(bool partialWords) {
|
||||
vosk_recognizer_set_partial_words(recognizer, partialWords);
|
||||
void recognizer::setPartialWords(bool partialWords) {
|
||||
vosk_recognizer_set_partial_words(rec, partialWords);
|
||||
}
|
||||
void Recognizer::setNLSML(bool nlsml) {
|
||||
vosk_recognizer_set_nlsml(recognizer, nlsml);
|
||||
void recognizer::setNLSML(bool nlsml) {
|
||||
vosk_recognizer_set_nlsml(rec, nlsml);
|
||||
}
|
||||
void Recognizer::setMaxAlternatives(int alts) {
|
||||
vosk_recognizer_set_max_alternatives(recognizer, alts);
|
||||
void recognizer::setMaxAlternatives(int alts) {
|
||||
vosk_recognizer_set_max_alternatives(rec, alts);
|
||||
}
|
||||
@@ -16,18 +16,17 @@
|
||||
#include <archive_entry.h>
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
class Recognizer : public GenericObj {
|
||||
VoskRecognizer* recognizer{};
|
||||
struct recognizer : genericObj {
|
||||
VoskRecognizer* rec{};
|
||||
ALCdevice* mic{};
|
||||
std::atomic_flag done {false};
|
||||
std::atomic_flag controller{false};
|
||||
void main();
|
||||
public:
|
||||
Recognizer(Model* model, int sampleRate, int index);
|
||||
recognizer(model* model, int sampleRate, int index);
|
||||
void start();
|
||||
void stop();
|
||||
void deinit();
|
||||
void setSpkModel(SpkModel* model);
|
||||
void setSpkModel(spkModel* model);
|
||||
void setGrm(const std::string& grm);
|
||||
void setWords(bool words);
|
||||
void setPartialWords(bool partialWords);
|
||||
|
||||
@@ -1,36 +1,57 @@
|
||||
class Recognizer extends EventTarget {
|
||||
constructor(model) {
|
||||
ctx = new (AudioContext || webkitAudioContext)()
|
||||
this.obj = new Module.__Recognizer__(model.obj,ctx.sampleRate,__GenericObj__.objects.length)
|
||||
__GenericObj__.objects.push(this)
|
||||
this.obj = (async () => {
|
||||
new Module.__recognizer__(model.obj,ctx.sampleRate,__genericObj__.objects.length)
|
||||
})()
|
||||
__genericObj__.objects.push(this)
|
||||
ctx.close()
|
||||
return this;
|
||||
}
|
||||
start() {
|
||||
this.obj.start()
|
||||
this.obj.then(() => {
|
||||
this.obj.start()
|
||||
})
|
||||
}
|
||||
stop() {
|
||||
this.obj.stop()
|
||||
this.obj.then(() => {
|
||||
this.obj.stop()
|
||||
})
|
||||
}
|
||||
delete() {
|
||||
this.obj.deinit()
|
||||
this.obj.delete()
|
||||
this.obj.then(() => {
|
||||
this.obj.deinit()
|
||||
this.obj.delete()
|
||||
})
|
||||
}
|
||||
setWords(words) {
|
||||
this.obj.setWords(words)
|
||||
this.obj.then(() => {
|
||||
this.obj.setWords(words)
|
||||
})
|
||||
}
|
||||
setPartialWords(partialWords) {
|
||||
this.obj.setPartialWords(words)
|
||||
this.obj.then(() => {
|
||||
this.obj.setPartialWords(partialWords)
|
||||
})
|
||||
}
|
||||
setGrm(grm) {
|
||||
this.obj.setGrm(grm)
|
||||
this.obj.then(() => {
|
||||
this.obj.setGrm(grm)
|
||||
})
|
||||
}
|
||||
setSpkModel(model) {
|
||||
this.obj.setSpkModel(model.obj)
|
||||
this.obj.then(() => {
|
||||
this.obj.setSpkModel(model.obj)
|
||||
})
|
||||
}
|
||||
setNLSML(nlsml) {
|
||||
this.obj.setNLSML(nlsml)
|
||||
this.obj.then(() => {
|
||||
this.obj.setNLSML(nlsml)
|
||||
})
|
||||
}
|
||||
setMaxAlternatives(alts) {
|
||||
this.obj.setMaxAlternatives(alts)
|
||||
this.obj.then(() => {
|
||||
this.obj.setMaxAlternatives(alts)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
#include "spkModel.h"
|
||||
SpkModel::SpkModel(const std::string &url, const std::string& storepath, const std::string& id, int index) : GenericModel(url, storepath, id, index) {
|
||||
spkModel::spkModel(const std::string &url, const std::string& storepath, const std::string& id, int index) : genericModel(url, storepath, id, index) {
|
||||
if(!loadModel()) return;
|
||||
model = vosk_spk_model_new(this->storepath.c_str());
|
||||
if(model == nullptr) {
|
||||
mdl = vosk_spk_model_new(".");
|
||||
if(mdl == nullptr) {
|
||||
fireEv("error", "Unable to initialize speaker model");
|
||||
}
|
||||
fireEv("ready");
|
||||
};
|
||||
bool SpkModel::checkModel(const std::string& path) {
|
||||
return fs::exists((path + "/mfcc.conf")) &&
|
||||
fs::exists((path + "/final.ext.raw")) &&
|
||||
fs::exists((path + "/mean.vec")) &&
|
||||
fs::exists((path + "/transform.mat"));
|
||||
bool spkModel::checkModel() {
|
||||
return fs::exists("mfcc.conf") &&
|
||||
fs::exists("final.ext.raw") &&
|
||||
fs::exists("mean.vec") &&
|
||||
fs::exists("transform.mat");
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
#pragma once
|
||||
#include "genericModel.h"
|
||||
|
||||
class SpkModel : public GenericModel {
|
||||
bool checkModel(const std::string& path);
|
||||
public:
|
||||
SpkModel(const std::string &url, const std::string& storepath, const std::string& id, const int index);
|
||||
VoskSpkModel* model{};
|
||||
struct spkModel : genericModel {
|
||||
bool checkModel();
|
||||
spkModel(const std::string &url, const std::string& storepath, const std::string& id, const int index);
|
||||
VoskSpkModel* mdl{};
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
class SpkModel extends EventTarget{
|
||||
constructor(url, storepath, id) {
|
||||
super()
|
||||
this.obj = new Module.__SpkModel__(url, storepath, id, __GenericObj__.objects.length)
|
||||
__GenericObj__.objects.push(this)
|
||||
this.obj = (async () => {
|
||||
return new Module.__spkModel__(url, storepath, id, __genericObj__.objects.length)
|
||||
})()
|
||||
__genericObj__.objects.push(this)
|
||||
}
|
||||
delete() {
|
||||
this.obj.delete()
|
||||
this.obj.then(() => {
|
||||
this.obj.delete()
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user