Overte C++ Documentation
MiniPromises.h
1 //
2 // Created by Timothy Dedischew on 2017/12/21
3 // Copyright 2017 High Fidelity, Inc.
4 // Copyright 2023 Overte e.V.
5 //
6 // Distributed under the Apache License, Version 2.0.
7 // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
8 // SPDX-License-Identifier: Apache-2.0
9 //
10 
11 #pragma once
12 
13 // Minimalist threadsafe Promise-like helper for managing asynchronous results
14 //
15 // This class pivots around continuation-style callback handlers:
16 // auto successCallback = [=](QVariantMap result) { .... }
17 // auto errorCallback = [=](QString error) { .... }
18 // auto combinedCallback = [=](QString error, QVariantMap result) { .... }
19 //
20 // * Callback Handlers are automatically invoked on the right thread (the Promise's thread).
21 // * Callbacks can be assigned anytime during a Promise's life and "do the right thing".
22 // - ie: for code clarity you can define success cases first or choose to maintain time order
23 // * "Deferred" concept can be used to publish outcomes.
24 // * "Promise" concept be used to subscribe to outcomes.
25 //
26 // See AssetScriptingInterface.cpp for some examples of using to simplify chained async results.
27 
28 #include <QtCore/QObject>
29 #include <QtCore/QThread>
30 #include <QtCore/QVariantMap>
31 #include <QDebug>
32 #include "ReadWriteLockable.h"
33 #include <memory>
34 
35 class MiniPromise : public QObject, public std::enable_shared_from_this<MiniPromise>, public ReadWriteLockable {
36  Q_OBJECT
37  Q_PROPERTY(QString state READ getStateString)
38  Q_PROPERTY(QString error READ getError)
39  Q_PROPERTY(QVariantMap result READ getResult)
40 public:
41  using HandlerFunction = std::function<void(QString error, QVariantMap result)>;
42  using SuccessFunction = std::function<void(QVariantMap result)>;
43  using ErrorFunction = std::function<void(QString error)>;
44  using HandlerFunctions = QVector<HandlerFunction>;
45  using Promise = std::shared_ptr<MiniPromise>;
46 
47  static int metaTypeID;
48 
49  MiniPromise() {}
50  MiniPromise(const QString debugName) { setObjectName(debugName); }
51 
52  ~MiniPromise() {
53  if (getStateString() == "pending") {
54  qWarning() << "MiniPromise::~MiniPromise -- destroying pending promise:" << objectName() << _error << _result << "handlers:" << getPendingHandlerCount();
55  }
56  }
57  Promise self() { return shared_from_this(); }
58 
59  Q_INVOKABLE void executeOnPromiseThread(std::function<void()> function, MiniPromise::Promise root = nullptr) {
60  if (QThread::currentThread() != thread()) {
61  QMetaObject::invokeMethod(
62  this, "executeOnPromiseThread", Qt::QueuedConnection,
63  Q_ARG(std::function<void()>, function),
64  Q_ARG(MiniPromise::Promise, self()));
65  } else {
66  function();
67  }
68  }
69 
70  // result aggregation helpers -- eg: deferred->defaults(interimResultMap)->ready(...)
71  // copy values from the input map, but only for keys that don't already exist
72  Promise mixin(const QVariantMap& source) {
73  withWriteLock([&]{
74  for (const auto& key : source.keys()) {
75  if (!_result.contains(key)) {
76  _result[key] = source[key];
77  }
78  }
79  });
80  return self();
81  }
82  // copy values from the input map, replacing any existing keys
83  Promise assignResult(const QVariantMap& source) {
84  withWriteLock([&]{
85  for (const auto& key : source.keys()) {
86  _result[key] = source[key];
87  }
88  });
89  return self();
90  }
91 
92  // callback registration methods
93  Promise ready(HandlerFunction always) { return finally(always); }
94  Promise finally(HandlerFunction always) {
95  if (!_rejected && !_resolved) {
96  withWriteLock([&]{
97  _onfinally << always;
98  });
99  } else {
100  executeOnPromiseThread([&]{
101  always(getError(), getResult());
102  });
103  }
104  return self();
105  }
106  Promise fail(ErrorFunction errorOnly) {
107  return fail([errorOnly](QString error, QVariantMap result) {
108  errorOnly(error);
109  });
110  }
111 
112  Promise fail(HandlerFunction failFunc) {
113  if (!_rejected) {
114  withWriteLock([&]{
115  _onreject << failFunc;
116  });
117  } else {
118  executeOnPromiseThread([&]{
119  failFunc(getError(), getResult());
120  });
121  }
122  return self();
123  }
124 
125  Promise then(SuccessFunction successOnly) {
126  return then([successOnly](QString error, QVariantMap result) {
127  successOnly(result);
128  });
129  }
130  Promise then(HandlerFunction successFunc) {
131  if (!_resolved) {
132  withWriteLock([&]{
133  _onresolve << successFunc;
134  });
135  } else {
136  executeOnPromiseThread([&]{
137  successFunc(getError(), getResult());
138  });
139  }
140  return self();
141  }
142 
143  // NOTE: first arg may be null (see ES6 .then(null, errorHandler) conventions)
144  Promise then(SuccessFunction successOnly, ErrorFunction errorOnly) {
145  if (successOnly) {
146  then(successOnly);
147  }
148  if (errorOnly) {
149  fail(errorOnly);
150  }
151  return self();
152  }
153 
154 
155  // helper functions for forwarding results on to a next Promise
156  Promise ready(Promise next) { return finally(next); }
157  Promise finally(Promise next) {
158  return finally([next](QString error, QVariantMap result) {
159  next->handle(error, result);
160  });
161  }
162  Promise fail(Promise next) {
163  return fail([next](QString error, QVariantMap result) {
164  next->reject(error, result);
165  });
166  }
167  Promise then(Promise next) {
168  return then([next](QString error, QVariantMap result) {
169  next->resolve(error, result);
170  });
171  }
172 
173 
174  // trigger methods
175  // handle() automatically resolves or rejects the promise (based on whether an error value occurred)
176  Promise handle(QString error, const QVariantMap& result) {
177  if (error.isEmpty()) {
178  resolve(error, result);
179  } else {
180  reject(error, result);
181  }
182  return self();
183  }
184 
185  Promise resolve(QVariantMap result) {
186  return resolve(QString(), result);
187  }
188  Promise resolve(QString error, const QVariantMap& result) {
189  setState(true, error, result);
190 
191  executeOnPromiseThread([&]{
192  const QString localError{ getError() };
193  const QVariantMap localResult{ getResult() };
194  HandlerFunctions resolveHandlers;
195  HandlerFunctions finallyHandlers;
196  withReadLock([&]{
197  resolveHandlers = _onresolve;
198  finallyHandlers = _onfinally;
199  });
200  for (const auto& onresolve : resolveHandlers) {
201  onresolve(localError, localResult);
202  }
203  for (const auto& onfinally : finallyHandlers) {
204  onfinally(localError, localResult);
205  }
206  });
207  return self();
208  }
209 
210  Promise reject(QString error) {
211  return reject(error, QVariantMap());
212  }
213  Promise reject(QString error, const QVariantMap& result) {
214  setState(false, error, result);
215 
216  executeOnPromiseThread([&]{
217  const QString localError{ getError() };
218  const QVariantMap localResult{ getResult() };
219  HandlerFunctions rejectHandlers;
220  HandlerFunctions finallyHandlers;
221  withReadLock([&]{
222  rejectHandlers = _onreject;
223  finallyHandlers = _onfinally;
224  });
225  for (const auto& onreject : rejectHandlers) {
226  onreject(localError, localResult);
227  }
228  for (const auto& onfinally : finallyHandlers) {
229  onfinally(localError, localResult);
230  }
231  });
232  return self();
233  }
234 
235 private:
236 
237  Promise setState(bool resolved, QString error, const QVariantMap& result) {
238  if (resolved) {
239  _resolved = true;
240  } else {
241  _rejected = true;
242  }
243  setError(error);
244  assignResult(result);
245  return self();
246  }
247 
248  void setError(const QString error) { withWriteLock([&]{ _error = error; }); }
249  QString getError() const { return resultWithReadLock<QString>([this]() -> QString { return _error; }); }
250  QVariantMap getResult() const { return resultWithReadLock<QVariantMap>([this]() -> QVariantMap { return _result; }); }
251  int getPendingHandlerCount() const {
252  return resultWithReadLock<int>([this]() -> int {
253  return _onresolve.size() + _onreject.size() + _onfinally.size();
254  });
255  }
256  QString getStateString() const {
257  return _rejected ? "rejected" :
258  _resolved ? "resolved" :
259  getPendingHandlerCount() ? "pending" :
260  "unknown";
261  }
262  QString _error;
263  QVariantMap _result;
264  std::atomic<bool> _rejected{false};
265  std::atomic<bool> _resolved{false};
266  HandlerFunctions _onresolve;
267  HandlerFunctions _onreject;
268  HandlerFunctions _onfinally;
269 };
270 
271 Q_DECLARE_METATYPE(MiniPromise::Promise)
272 
273 inline MiniPromise::Promise makePromise(const QString& hint = QString()) {
274  if (!QMetaType::isRegistered(qMetaTypeId<MiniPromise::Promise>())) {
275  int type = qRegisterMetaType<MiniPromise::Promise>();
276  qDebug() << "makePromise -- registered MetaType<MiniPromise::Promise>:" << type;
277  }
278  return std::make_shared<MiniPromise>(hint);
279 }