promiseUtils.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
  2. import Gio from 'gi://Gio';
  3. import GLib from 'gi://GLib';
  4. import GObject from 'gi://GObject';
  5. import Meta from 'gi://GdkPixbuf';
  6. import * as Signals from 'resource:///org/gnome/shell/misc/signals.js';
  7. export class CancellablePromise extends Promise {
  8. constructor(executor, cancellable) {
  9. if (!(executor instanceof Function))
  10. throw TypeError('executor is not a function');
  11. if (cancellable && !(cancellable instanceof Gio.Cancellable))
  12. throw TypeError('cancellable parameter is not a Gio.Cancellable');
  13. let rejector;
  14. let resolver;
  15. super((resolve, reject) => {
  16. resolver = resolve;
  17. rejector = reject;
  18. });
  19. const {stack: promiseStack} = new Error();
  20. this._promiseStack = promiseStack;
  21. this._resolver = (...args) => {
  22. resolver(...args);
  23. this._resolved = true;
  24. this._cleanup();
  25. };
  26. this._rejector = (...args) => {
  27. rejector(...args);
  28. this._rejected = true;
  29. this._cleanup();
  30. };
  31. if (!cancellable) {
  32. executor(this._resolver, this._rejector);
  33. return;
  34. }
  35. this._cancellable = cancellable;
  36. this._cancelled = cancellable.is_cancelled();
  37. if (this._cancelled) {
  38. this._rejector(new GLib.Error(Gio.IOErrorEnum,
  39. Gio.IOErrorEnum.CANCELLED, 'Promise cancelled'));
  40. return;
  41. }
  42. this._cancellationId = cancellable.connect(() => {
  43. const id = this._cancellationId;
  44. this._cancellationId = 0;
  45. GLib.idle_add(GLib.PRIORITY_DEFAULT, () => cancellable.disconnect(id));
  46. this.cancel();
  47. });
  48. executor(this._resolver, this._rejector);
  49. }
  50. _cleanup() {
  51. if (this._cancellationId)
  52. this._cancellable.disconnect(this._cancellationId);
  53. }
  54. get cancellable() {
  55. return this._chainRoot._cancellable || null;
  56. }
  57. get _chainRoot() {
  58. return this._root ? this._root : this;
  59. }
  60. then(...args) {
  61. const ret = super.then(...args);
  62. /* Every time we call then() on this promise we'd get a new
  63. * CancellablePromise however that won't have the properties that the
  64. * root one has set, and then it won't be possible to cancel a promise
  65. * chain from the last one.
  66. * To allow this we keep track of the root promise, make sure that
  67. * the same method on the root object is called during cancellation
  68. * or any destruction method if you want this to work. */
  69. if (ret instanceof CancellablePromise)
  70. ret._root = this._chainRoot;
  71. return ret;
  72. }
  73. resolved() {
  74. return !!this._chainRoot._resolved;
  75. }
  76. rejected() {
  77. return !!this._chainRoot._rejected;
  78. }
  79. cancelled() {
  80. return !!this._chainRoot._cancelled;
  81. }
  82. pending() {
  83. return !this.resolved() && !this.rejected();
  84. }
  85. cancel() {
  86. if (this._root) {
  87. this._root.cancel();
  88. return this;
  89. }
  90. if (!this.pending())
  91. return this;
  92. this._cancelled = true;
  93. const error = new GLib.Error(Gio.IOErrorEnum,
  94. Gio.IOErrorEnum.CANCELLED, 'Promise cancelled');
  95. error.stack += `## Promise created at:\n${this._promiseStack}`;
  96. this._rejector(error);
  97. return this;
  98. }
  99. }
  100. export class SignalConnectionPromise extends CancellablePromise {
  101. constructor(object, signal, cancellable) {
  102. if (arguments.length === 1 && object instanceof Function) {
  103. super(object);
  104. return;
  105. }
  106. if (!(object.connect instanceof Function))
  107. throw new TypeError('Not a valid object');
  108. if (object instanceof GObject.Object &&
  109. !GObject.signal_lookup(signal.split(':')[0], object.constructor.$gtype))
  110. throw new TypeError(`Signal ${signal} not found on object ${object}`);
  111. let id;
  112. let destroyId;
  113. super(resolve => {
  114. let connectSignal;
  115. if (object instanceof GObject.Object)
  116. connectSignal = (sig, cb) => GObject.signal_connect(object, sig, cb);
  117. else
  118. connectSignal = (sig, cb) => object.connect(sig, cb);
  119. id = connectSignal(signal, (_obj, ...args) => {
  120. if (!args.length)
  121. resolve();
  122. else
  123. resolve(args.length === 1 ? args[0] : args);
  124. });
  125. if (signal !== 'destroy' &&
  126. (!(object instanceof GObject.Object) ||
  127. GObject.signal_lookup('destroy', object.constructor.$gtype)))
  128. destroyId = connectSignal('destroy', () => this.cancel());
  129. }, cancellable);
  130. this._object = object;
  131. this._id = id;
  132. this._destroyId = destroyId;
  133. }
  134. _cleanup() {
  135. if (this._id) {
  136. let disconnectSignal;
  137. if (this._object instanceof GObject.Object)
  138. disconnectSignal = id => GObject.signal_handler_disconnect(this._object, id);
  139. else
  140. disconnectSignal = id => this._object.disconnect(id);
  141. disconnectSignal(this._id);
  142. if (this._destroyId) {
  143. disconnectSignal(this._destroyId);
  144. this._destroyId = 0;
  145. }
  146. this._object = null;
  147. this._id = 0;
  148. }
  149. super._cleanup();
  150. }
  151. get object() {
  152. return this._chainRoot._object;
  153. }
  154. }
  155. export class GSourcePromise extends CancellablePromise {
  156. constructor(gsource, priority, cancellable) {
  157. if (arguments.length === 1 && gsource instanceof Function) {
  158. super(gsource);
  159. return;
  160. }
  161. if (gsource.constructor.$gtype !== GLib.Source.$gtype)
  162. throw new TypeError(`gsource ${gsource} is not of type GLib.Source`);
  163. if (priority === undefined)
  164. priority = GLib.PRIORITY_DEFAULT;
  165. else if (!Number.isInteger(priority))
  166. throw TypeError('Invalid priority');
  167. super(resolve => {
  168. gsource.set_priority(priority);
  169. gsource.set_callback(() => {
  170. resolve();
  171. return GLib.SOURCE_REMOVE;
  172. });
  173. gsource.attach(null);
  174. }, cancellable);
  175. this._gsource = gsource;
  176. this._gsource.set_name(`[gnome-shell] ${this.constructor.name} ${
  177. new Error().stack.split('\n').filter(line =>
  178. !line.match(/misc\/promiseUtils\.js/))[0]}`);
  179. if (this.rejected())
  180. this._gsource.destroy();
  181. }
  182. get gsource() {
  183. return this._chainRoot._gsource;
  184. }
  185. _cleanup() {
  186. if (this._gsource) {
  187. this._gsource.destroy();
  188. this._gsource = null;
  189. }
  190. super._cleanup();
  191. }
  192. }
  193. export class IdlePromise extends GSourcePromise {
  194. constructor(priority, cancellable) {
  195. if (arguments.length === 1 && priority instanceof Function) {
  196. super(priority);
  197. return;
  198. }
  199. if (priority === undefined)
  200. priority = GLib.PRIORITY_DEFAULT_IDLE;
  201. super(GLib.idle_source_new(), priority, cancellable);
  202. }
  203. }
  204. export class TimeoutPromise extends GSourcePromise {
  205. constructor(interval, priority, cancellable) {
  206. if (arguments.length === 1 && interval instanceof Function) {
  207. super(interval);
  208. return;
  209. }
  210. if (!Number.isInteger(interval) || interval < 0)
  211. throw TypeError('Invalid interval');
  212. super(GLib.timeout_source_new(interval), priority, cancellable);
  213. }
  214. }
  215. export class TimeoutSecondsPromise extends GSourcePromise {
  216. constructor(interval, priority, cancellable) {
  217. if (arguments.length === 1 && interval instanceof Function) {
  218. super(interval);
  219. return;
  220. }
  221. if (!Number.isInteger(interval) || interval < 0)
  222. throw TypeError('Invalid interval');
  223. super(GLib.timeout_source_new_seconds(interval), priority, cancellable);
  224. }
  225. }
  226. export class MetaLaterPromise extends CancellablePromise {
  227. constructor(laterType, cancellable) {
  228. if (arguments.length === 1 && laterType instanceof Function) {
  229. super(laterType);
  230. return;
  231. }
  232. if (laterType && laterType.constructor.$gtype !== Meta.LaterType.$gtype)
  233. throw new TypeError(`laterType ${laterType} is not of type Meta.LaterType`);
  234. else if (!laterType)
  235. laterType = Meta.LaterType.BEFORE_REDRAW;
  236. let id;
  237. super(resolve => {
  238. id = Meta.later_add(laterType, () => {
  239. this.remove();
  240. resolve();
  241. return GLib.SOURCE_REMOVE;
  242. });
  243. }, cancellable);
  244. this._id = id;
  245. }
  246. _cleanup() {
  247. if (this._id) {
  248. Meta.later_remove(this._id);
  249. this._id = 0;
  250. }
  251. super._cleanup();
  252. }
  253. }
  254. export function _promisifySignals(proto) {
  255. if (proto.connect_once)
  256. return;
  257. proto.connect_once = function (signal, cancellable) {
  258. return new SignalConnectionPromise(this, signal, cancellable);
  259. };
  260. }
  261. _promisifySignals(GObject.Object.prototype);
  262. _promisifySignals(Signals.EventEmitter.prototype);