From 7e1be2b17c9a1640aa1dc5fb314ecdcf589ab24a Mon Sep 17 00:00:00 2001 From: humbletim Date: Thu, 21 Dec 2017 11:35:14 -0500 Subject: [PATCH 1/4] shared promises library --- libraries/shared/src/shared/MiniPromises.cpp | 4 + libraries/shared/src/shared/MiniPromises.h | 249 +++++++++++++++++++ 2 files changed, 253 insertions(+) create mode 100644 libraries/shared/src/shared/MiniPromises.cpp create mode 100644 libraries/shared/src/shared/MiniPromises.h 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) From e94103633705d80c7095bd298b5e3703a5a5f9d9 Mon Sep 17 00:00:00 2001 From: humbletim Date: Tue, 2 Jan 2018 15:28:01 -0500 Subject: [PATCH 2/4] renames per CR feedback / code cleanup --- libraries/shared/src/shared/MiniPromises.cpp | 4 +- libraries/shared/src/shared/MiniPromises.h | 203 +++++++++---------- 2 files changed, 99 insertions(+), 108 deletions(-) diff --git a/libraries/shared/src/shared/MiniPromises.cpp b/libraries/shared/src/shared/MiniPromises.cpp index fdfb509608..28e2c18857 100644 --- a/libraries/shared/src/shared/MiniPromises.cpp +++ b/libraries/shared/src/shared/MiniPromises.cpp @@ -1,4 +1,2 @@ #include "MiniPromises.h" -namespace { - int promiseMetaTypeId = qRegisterMetaType("MiniPromise::Promise"); -} +int MiniPromise::metaTypeID = qRegisterMetaType("MiniPromise::Promise"); diff --git a/libraries/shared/src/shared/MiniPromises.h b/libraries/shared/src/shared/MiniPromises.h index 17f2394a2f..c5d21a22fb 100644 --- a/libraries/shared/src/shared/MiniPromises.h +++ b/libraries/shared/src/shared/MiniPromises.h @@ -1,19 +1,19 @@ #pragma once -// Minimalist threadsafe Promise-like helper for instrumenting asynchronous results +// Minimalist threadsafe Promise-like helper for managing asynchronous results // -// This class pivots around composable continuation-style callback handlers: +// This class pivots around 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) +// - ie: for code clarity you can define success cases first or choose to 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. +// See AssetScriptingInterface.cpp for some examples of using to simplify chained async results. #include #include @@ -25,35 +25,37 @@ 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 HandlerFunction = std::function; + using SuccessFunction = std::function; + using ErrorFunction = std::function; + using HandlerFunctions = QVector; using Promise = std::shared_ptr; - MiniPromise(const QString debugName) { setObjectName(debugName); } + + static int metaTypeID; + MiniPromise() {} + MiniPromise(const QString debugName) { setObjectName(debugName); } + ~MiniPromise() { - qDebug() << "~MiniPromise" << objectName(); if (!_rejected && !_resolved) { - qWarning() << "====== WARNING: unhandled MiniPromise" << objectName() << _error << _result; + qWarning() << "MiniPromise::~MiniPromise -- destroying unhandled promise:" << objectName() << _error << _result; + } + } + Promise self() { return shared_from_this(); } + + Q_INVOKABLE void executeOnPromiseThread(std::function function) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod( + this, "executeOnPromiseThread", Qt::BlockingQueuedConnection, + Q_ARG(std::function, function)); + } else { + function(); } } - 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)) { @@ -65,7 +67,6 @@ public: } // 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]; @@ -74,15 +75,15 @@ public: 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) { + // callback registration methods + Promise ready(HandlerFunction always) { return finally(always); } + Promise finally(HandlerFunction always) { if (!_rejected && !_resolved) { withWriteLock([&]{ _onfinally << always; }); } else { - qDebug() << "finally (already resolved/rejected)" << objectName(); + qWarning() << "MiniPromise::finally() called after promise was already rejected or resolved:" << objectName(); executeOnPromiseThread([&]{ withReadLock([&]{ always(_error, _result); @@ -91,20 +92,20 @@ public: } return self(); } - Promise fail(errorFunction errorOnly) { + Promise fail(ErrorFunction errorOnly) { return fail([this, errorOnly](QString error, QVariantMap result) { errorOnly(error); }); } - Promise fail(handlerFunction failFunc) { + Promise fail(HandlerFunction failFunc) { if (!_rejected) { withWriteLock([&]{ _onreject << failFunc; }); } else { executeOnPromiseThread([&]{ - qDebug() << "fail (already rejected)" << objectName(); + qWarning() << "MiniPromise::fail() called after promise was already rejected:" << objectName(); withReadLock([&]{ failFunc(_error, _result); }); @@ -113,19 +114,19 @@ public: return self(); } - Promise then(successFunction successOnly) { + Promise then(SuccessFunction successOnly) { return then([this, successOnly](QString error, QVariantMap result) { successOnly(result); }); } - Promise then(handlerFunction successFunc) { + Promise then(HandlerFunction successFunc) { if (!_resolved) { withWriteLock([&]{ _onresolve << successFunc; }); } else { executeOnPromiseThread([&]{ - qDebug() << "fail (already resolved)" << objectName(); + qWarning() << "MiniPromise::then() called after promise was already resolved:" << objectName(); withReadLock([&]{ successFunc(_error, _result); }); @@ -133,9 +134,9 @@ public: } 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) + + // NOTE: first arg may be null (see ES6 .then(null, errorHandler) conventions) + Promise then(SuccessFunction successOnly, ErrorFunction errorOnly) { if (successOnly) { then(successOnly); } @@ -146,8 +147,8 @@ public: } // trigger methods + // handle() automatically resolves or rejects the promise (based on whether an error value occurred) Promise handle(QString error, const QVariantMap& result) { - qDebug() << "handle" << objectName() << error; if (error.isEmpty()) { resolve(error, result); } else { @@ -155,23 +156,64 @@ public: } return self(); } + Promise resolve(QVariantMap result) { return resolve(QString(), result); } + Promise resolve(QString error, const QVariantMap& result) { + setState(true, error, 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(); - } + QString localError; + QVariantMap localResult; + HandlerFunctions resolveHandlers; + HandlerFunctions finallyHandlers; + withReadLock([&]{ + localError = _error; + localResult = _result; + resolveHandlers = _onresolve; + finallyHandlers = _onfinally; + }); + executeOnPromiseThread([&]{ + for (const auto& onresolve : resolveHandlers) { + onresolve(localError, localResult); + } + for (const auto& onfinally : finallyHandlers) { + onfinally(localError, localResult); + } + }); + return self(); } + Promise reject(QString error) { + return reject(error, QVariantMap()); + } + Promise reject(QString error, const QVariantMap& result) { + setState(false, error, result); + + QString localError; + QVariantMap localResult; + HandlerFunctions rejectHandlers; + HandlerFunctions finallyHandlers; + withReadLock([&]{ + localError = _error; + localResult = _result; + rejectHandlers = _onreject; + finallyHandlers = _onfinally; + }); + executeOnPromiseThread([&]{ + for (const auto& onreject : rejectHandlers) { + onreject(localError, localResult); + } + for (const auto& onfinally : finallyHandlers) { + onfinally(localError, localResult); + } + }); + return self(); + } + +private: + Promise setState(bool resolved, QString error, const QVariantMap& result) { - qDebug() << "setState" << objectName() << resolved << error; if (resolved) { _resolved = true; } else { @@ -181,65 +223,16 @@ public: _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(); } + + QString _error; + QVariantMap _result; + std::atomic _rejected{false}; + std::atomic _resolved{false}; + HandlerFunctions _onresolve; + HandlerFunctions _onreject; + HandlerFunctions _onfinally; }; inline MiniPromise::Promise makePromise(const QString& hint = QString()) { From fc41bcca5af66e995de46551e72298cb0bdf1449 Mon Sep 17 00:00:00 2001 From: humbletim Date: Fri, 5 Jan 2018 18:41:51 -0500 Subject: [PATCH 3/4] removing unnecessary qWarning() debug output --- libraries/shared/src/shared/MiniPromises.h | 3 --- 1 file changed, 3 deletions(-) diff --git a/libraries/shared/src/shared/MiniPromises.h b/libraries/shared/src/shared/MiniPromises.h index c5d21a22fb..0222a91d44 100644 --- a/libraries/shared/src/shared/MiniPromises.h +++ b/libraries/shared/src/shared/MiniPromises.h @@ -83,7 +83,6 @@ public: _onfinally << always; }); } else { - qWarning() << "MiniPromise::finally() called after promise was already rejected or resolved:" << objectName(); executeOnPromiseThread([&]{ withReadLock([&]{ always(_error, _result); @@ -105,7 +104,6 @@ public: }); } else { executeOnPromiseThread([&]{ - qWarning() << "MiniPromise::fail() called after promise was already rejected:" << objectName(); withReadLock([&]{ failFunc(_error, _result); }); @@ -126,7 +124,6 @@ public: }); } else { executeOnPromiseThread([&]{ - qWarning() << "MiniPromise::then() called after promise was already resolved:" << objectName(); withReadLock([&]{ successFunc(_error, _result); }); From 744da485512c691ea110145a8bb3a809402cf57e Mon Sep 17 00:00:00 2001 From: humbletim Date: Wed, 17 Jan 2018 14:00:50 -0500 Subject: [PATCH 4/4] add apache 2.0 headers --- libraries/shared/src/shared/MiniPromises.cpp | 8 ++++++++ libraries/shared/src/shared/MiniPromises.h | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/libraries/shared/src/shared/MiniPromises.cpp b/libraries/shared/src/shared/MiniPromises.cpp index 28e2c18857..faada3627a 100644 --- a/libraries/shared/src/shared/MiniPromises.cpp +++ b/libraries/shared/src/shared/MiniPromises.cpp @@ -1,2 +1,10 @@ +// +// Created by Timothy Dedischew on 2017/12/21 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + #include "MiniPromises.h" int MiniPromise::metaTypeID = qRegisterMetaType("MiniPromise::Promise"); diff --git a/libraries/shared/src/shared/MiniPromises.h b/libraries/shared/src/shared/MiniPromises.h index 0222a91d44..3385118666 100644 --- a/libraries/shared/src/shared/MiniPromises.h +++ b/libraries/shared/src/shared/MiniPromises.h @@ -1,3 +1,11 @@ +// +// Created by Timothy Dedischew on 2017/12/21 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + #pragma once // Minimalist threadsafe Promise-like helper for managing asynchronous results