diff --git a/libraries/shared/src/shared/MiniPromises.cpp b/libraries/shared/src/shared/MiniPromises.cpp new file mode 100644 index 0000000000..fdfb509608 --- /dev/null +++ b/libraries/shared/src/shared/MiniPromises.cpp @@ -0,0 +1,4 @@ +#include "MiniPromises.h" +namespace { + int promiseMetaTypeId = qRegisterMetaType("MiniPromise::Promise"); +} diff --git a/libraries/shared/src/shared/MiniPromises.h b/libraries/shared/src/shared/MiniPromises.h new file mode 100644 index 0000000000..17f2394a2f --- /dev/null +++ b/libraries/shared/src/shared/MiniPromises.h @@ -0,0 +1,249 @@ +#pragma once + +// Minimalist threadsafe Promise-like helper for instrumenting asynchronous results +// +// This class pivots around composable continuation-style callback handlers: +// auto successCallback = [=](QVariantMap result) { .... } +// auto errorCallback = [=](QString error) { .... } +// auto combinedCallback = [=](QString error, QVariantMap result) { .... } +// +// * Callback Handlers are automatically invoked on the right thread (the Promise's thread). +// * Callbacks can be assigned anytime during a Promise's life and "do the right thing". +// - ie: for code clarity you can define success cases first (or maintain time order) +// * "Deferred" concept can be used to publish outcomes. +// * "Promise" concept be used to subscribe to outcomes. +// +// See AssetScriptingInterface.cpp for some examples of using to simplify chained async result. + +#include +#include +#include +#include +#include "ReadWriteLockable.h" +#include + +class MiniPromise : public QObject, public std::enable_shared_from_this, public ReadWriteLockable { + Q_OBJECT +public: + using handlerFunction = std::function; + using successFunction = std::function; + using errorFunction = std::function; + using handlers = QVector; + using Promise = std::shared_ptr; + MiniPromise(const QString debugName) { setObjectName(debugName); } + MiniPromise() {} + ~MiniPromise() { + qDebug() << "~MiniPromise" << objectName(); + if (!_rejected && !_resolved) { + qWarning() << "====== WARNING: unhandled MiniPromise" << objectName() << _error << _result; + } + } + + QString _error; + QVariantMap _result; + std::atomic _rejected{false}; + std::atomic _resolved{false}; + handlers _onresolve; + handlers _onreject; + handlers _onfinally; + + Promise self() { return shared_from_this(); } + + // result aggregation helpers -- eg: deferred->defaults(interimResultMap)->ready(...) + + // copy values from the input map, but only for keys that don't already exist + Promise mixin(const QVariantMap& source) { + qDebug() << objectName() << "mixin"; + withWriteLock([&]{ + for (const auto& key : source.keys()) { + if (!_result.contains(key)) { + _result[key] = source[key]; + } + } + }); + return self(); + } + // copy values from the input map, replacing any existing keys + Promise assignResult(const QVariantMap& source) { + qDebug() << objectName() << "assignResult"; + withWriteLock([&]{ + for (const auto& key : source.keys()) { + _result[key] = source[key]; + } + }); + return self(); + } + + // TODO: I think calling as "ready" makes it read better, but is customary Promise "finally" sufficient? + Promise ready(handlerFunction always) { return finally(always); } + Promise finally(handlerFunction always) { + if (!_rejected && !_resolved) { + withWriteLock([&]{ + _onfinally << always; + }); + } else { + qDebug() << "finally (already resolved/rejected)" << objectName(); + executeOnPromiseThread([&]{ + withReadLock([&]{ + always(_error, _result); + }); + }); + } + return self(); + } + Promise fail(errorFunction errorOnly) { + return fail([this, errorOnly](QString error, QVariantMap result) { + errorOnly(error); + }); + } + + Promise fail(handlerFunction failFunc) { + if (!_rejected) { + withWriteLock([&]{ + _onreject << failFunc; + }); + } else { + executeOnPromiseThread([&]{ + qDebug() << "fail (already rejected)" << objectName(); + withReadLock([&]{ + failFunc(_error, _result); + }); + }); + } + return self(); + } + + Promise then(successFunction successOnly) { + return then([this, successOnly](QString error, QVariantMap result) { + successOnly(result); + }); + } + Promise then(handlerFunction successFunc) { + if (!_resolved) { + withWriteLock([&]{ + _onresolve << successFunc; + }); + } else { + executeOnPromiseThread([&]{ + qDebug() << "fail (already resolved)" << objectName(); + withReadLock([&]{ + successFunc(_error, _result); + }); + }); + } + return self(); + } + // register combined success/error handlers + Promise then(successFunction successOnly, errorFunction errorOnly) { + // note: first arg can be null (see ES6 .then(null, errorHandler) conventions) + if (successOnly) { + then(successOnly); + } + if (errorOnly) { + fail(errorOnly); + } + return self(); + } + + // trigger methods + Promise handle(QString error, const QVariantMap& result) { + qDebug() << "handle" << objectName() << error; + if (error.isEmpty()) { + resolve(error, result); + } else { + reject(error, result); + } + return self(); + } + Promise resolve(QVariantMap result) { + return resolve(QString(), result); + } + + Q_INVOKABLE void executeOnPromiseThread(std::function function) { + if (QThread::currentThread() != thread()) { + qDebug() << "-0-0-00-0--0" << objectName() << "executeOnPromiseThread -- wrong thread" << QThread::currentThread(); + QMetaObject::invokeMethod( + this, "executeOnPromiseThread", Qt::BlockingQueuedConnection, + Q_ARG(std::function, function)); + } else { + function(); + } + } + + Promise setState(bool resolved, QString error, const QVariantMap& result) { + qDebug() << "setState" << objectName() << resolved << error; + if (resolved) { + _resolved = true; + } else { + _rejected = true; + } + withWriteLock([&]{ + _error = error; + }); + assignResult(result); + qDebug() << "//setState" << objectName() << resolved << error; + return self(); + } + Promise resolve(QString error, const QVariantMap& result) { + setState(true, error, result); + qDebug() << "handle" << objectName() << error; + { + QString error; + QVariantMap result; + handlers toresolve; + handlers tofinally; + withReadLock([&]{ + error = _error; + result = _result; + toresolve = _onresolve; + tofinally = _onfinally; + }); + executeOnPromiseThread([&]{ + for (const auto& onresolve : toresolve) { + onresolve(error, result); + } + for (const auto& onfinally : tofinally) { + onfinally(error, result); + } + }); + } + return self(); + } + Promise reject(QString error) { + return reject(error, QVariantMap()); + } + Promise reject(QString error, const QVariantMap& result) { + setState(false, error, result); + qDebug() << "handle" << objectName() << error; + { + QString error; + QVariantMap result; + handlers toreject; + handlers tofinally; + withReadLock([&]{ + error = _error; + result = _result; + toreject = _onreject; + tofinally = _onfinally; + }); + executeOnPromiseThread([&]{ + for (const auto& onreject : toreject) { + onreject(error, result); + } + for (const auto& onfinally : tofinally) { + onfinally(error, result); + } + if (toreject.isEmpty() && tofinally.isEmpty()) { + qWarning() << "WARNING: unhandled MiniPromise::reject" << objectName() << error << result; + } + }); + } + return self(); + } +}; + +inline MiniPromise::Promise makePromise(const QString& hint = QString()) { + return std::make_shared(hint); +} + +Q_DECLARE_METATYPE(MiniPromise::Promise)