325 lines
9.3 KiB
JavaScript
325 lines
9.3 KiB
JavaScript
|
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
||
|
|
||
|
import Gio from 'gi://Gio';
|
||
|
import GLib from 'gi://GLib';
|
||
|
import GObject from 'gi://GObject';
|
||
|
import Meta from 'gi://GdkPixbuf';
|
||
|
|
||
|
import * as Signals from 'resource:///org/gnome/shell/misc/signals.js';
|
||
|
|
||
|
export class CancellablePromise extends Promise {
|
||
|
constructor(executor, cancellable) {
|
||
|
if (!(executor instanceof Function))
|
||
|
throw TypeError('executor is not a function');
|
||
|
|
||
|
if (cancellable && !(cancellable instanceof Gio.Cancellable))
|
||
|
throw TypeError('cancellable parameter is not a Gio.Cancellable');
|
||
|
|
||
|
let rejector;
|
||
|
let resolver;
|
||
|
super((resolve, reject) => {
|
||
|
resolver = resolve;
|
||
|
rejector = reject;
|
||
|
});
|
||
|
|
||
|
const {stack: promiseStack} = new Error();
|
||
|
this._promiseStack = promiseStack;
|
||
|
|
||
|
this._resolver = (...args) => {
|
||
|
resolver(...args);
|
||
|
this._resolved = true;
|
||
|
this._cleanup();
|
||
|
};
|
||
|
this._rejector = (...args) => {
|
||
|
rejector(...args);
|
||
|
this._rejected = true;
|
||
|
this._cleanup();
|
||
|
};
|
||
|
|
||
|
if (!cancellable) {
|
||
|
executor(this._resolver, this._rejector);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
this._cancellable = cancellable;
|
||
|
this._cancelled = cancellable.is_cancelled();
|
||
|
if (this._cancelled) {
|
||
|
this._rejector(new GLib.Error(Gio.IOErrorEnum,
|
||
|
Gio.IOErrorEnum.CANCELLED, 'Promise cancelled'));
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
this._cancellationId = cancellable.connect(() => {
|
||
|
const id = this._cancellationId;
|
||
|
this._cancellationId = 0;
|
||
|
GLib.idle_add(GLib.PRIORITY_DEFAULT, () => cancellable.disconnect(id));
|
||
|
this.cancel();
|
||
|
});
|
||
|
|
||
|
executor(this._resolver, this._rejector);
|
||
|
}
|
||
|
|
||
|
_cleanup() {
|
||
|
if (this._cancellationId)
|
||
|
this._cancellable.disconnect(this._cancellationId);
|
||
|
}
|
||
|
|
||
|
get cancellable() {
|
||
|
return this._chainRoot._cancellable || null;
|
||
|
}
|
||
|
|
||
|
get _chainRoot() {
|
||
|
return this._root ? this._root : this;
|
||
|
}
|
||
|
|
||
|
then(...args) {
|
||
|
const ret = super.then(...args);
|
||
|
|
||
|
/* Every time we call then() on this promise we'd get a new
|
||
|
* CancellablePromise however that won't have the properties that the
|
||
|
* root one has set, and then it won't be possible to cancel a promise
|
||
|
* chain from the last one.
|
||
|
* To allow this we keep track of the root promise, make sure that
|
||
|
* the same method on the root object is called during cancellation
|
||
|
* or any destruction method if you want this to work. */
|
||
|
if (ret instanceof CancellablePromise)
|
||
|
ret._root = this._chainRoot;
|
||
|
|
||
|
return ret;
|
||
|
}
|
||
|
|
||
|
resolved() {
|
||
|
return !!this._chainRoot._resolved;
|
||
|
}
|
||
|
|
||
|
rejected() {
|
||
|
return !!this._chainRoot._rejected;
|
||
|
}
|
||
|
|
||
|
cancelled() {
|
||
|
return !!this._chainRoot._cancelled;
|
||
|
}
|
||
|
|
||
|
pending() {
|
||
|
return !this.resolved() && !this.rejected();
|
||
|
}
|
||
|
|
||
|
cancel() {
|
||
|
if (this._root) {
|
||
|
this._root.cancel();
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
if (!this.pending())
|
||
|
return this;
|
||
|
|
||
|
this._cancelled = true;
|
||
|
const error = new GLib.Error(Gio.IOErrorEnum,
|
||
|
Gio.IOErrorEnum.CANCELLED, 'Promise cancelled');
|
||
|
error.stack += `## Promise created at:\n${this._promiseStack}`;
|
||
|
this._rejector(error);
|
||
|
|
||
|
return this;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export class SignalConnectionPromise extends CancellablePromise {
|
||
|
constructor(object, signal, cancellable) {
|
||
|
if (arguments.length === 1 && object instanceof Function) {
|
||
|
super(object);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (!(object.connect instanceof Function))
|
||
|
throw new TypeError('Not a valid object');
|
||
|
|
||
|
if (object instanceof GObject.Object &&
|
||
|
!GObject.signal_lookup(signal.split(':')[0], object.constructor.$gtype))
|
||
|
throw new TypeError(`Signal ${signal} not found on object ${object}`);
|
||
|
|
||
|
let id;
|
||
|
let destroyId;
|
||
|
super(resolve => {
|
||
|
let connectSignal;
|
||
|
if (object instanceof GObject.Object)
|
||
|
connectSignal = (sig, cb) => GObject.signal_connect(object, sig, cb);
|
||
|
else
|
||
|
connectSignal = (sig, cb) => object.connect(sig, cb);
|
||
|
|
||
|
id = connectSignal(signal, (_obj, ...args) => {
|
||
|
if (!args.length)
|
||
|
resolve();
|
||
|
else
|
||
|
resolve(args.length === 1 ? args[0] : args);
|
||
|
});
|
||
|
|
||
|
if (signal !== 'destroy' &&
|
||
|
(!(object instanceof GObject.Object) ||
|
||
|
GObject.signal_lookup('destroy', object.constructor.$gtype)))
|
||
|
destroyId = connectSignal('destroy', () => this.cancel());
|
||
|
}, cancellable);
|
||
|
|
||
|
this._object = object;
|
||
|
this._id = id;
|
||
|
this._destroyId = destroyId;
|
||
|
}
|
||
|
|
||
|
_cleanup() {
|
||
|
if (this._id) {
|
||
|
let disconnectSignal;
|
||
|
|
||
|
if (this._object instanceof GObject.Object)
|
||
|
disconnectSignal = id => GObject.signal_handler_disconnect(this._object, id);
|
||
|
else
|
||
|
disconnectSignal = id => this._object.disconnect(id);
|
||
|
|
||
|
disconnectSignal(this._id);
|
||
|
if (this._destroyId) {
|
||
|
disconnectSignal(this._destroyId);
|
||
|
this._destroyId = 0;
|
||
|
}
|
||
|
this._object = null;
|
||
|
this._id = 0;
|
||
|
}
|
||
|
|
||
|
super._cleanup();
|
||
|
}
|
||
|
|
||
|
get object() {
|
||
|
return this._chainRoot._object;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export class GSourcePromise extends CancellablePromise {
|
||
|
constructor(gsource, priority, cancellable) {
|
||
|
if (arguments.length === 1 && gsource instanceof Function) {
|
||
|
super(gsource);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (gsource.constructor.$gtype !== GLib.Source.$gtype)
|
||
|
throw new TypeError(`gsource ${gsource} is not of type GLib.Source`);
|
||
|
|
||
|
if (priority === undefined)
|
||
|
priority = GLib.PRIORITY_DEFAULT;
|
||
|
else if (!Number.isInteger(priority))
|
||
|
throw TypeError('Invalid priority');
|
||
|
|
||
|
super(resolve => {
|
||
|
gsource.set_priority(priority);
|
||
|
gsource.set_callback(() => {
|
||
|
resolve();
|
||
|
return GLib.SOURCE_REMOVE;
|
||
|
});
|
||
|
gsource.attach(null);
|
||
|
}, cancellable);
|
||
|
|
||
|
this._gsource = gsource;
|
||
|
this._gsource.set_name(`[gnome-shell] ${this.constructor.name} ${
|
||
|
new Error().stack.split('\n').filter(line =>
|
||
|
!line.match(/misc\/promiseUtils\.js/))[0]}`);
|
||
|
|
||
|
if (this.rejected())
|
||
|
this._gsource.destroy();
|
||
|
}
|
||
|
|
||
|
get gsource() {
|
||
|
return this._chainRoot._gsource;
|
||
|
}
|
||
|
|
||
|
_cleanup() {
|
||
|
if (this._gsource) {
|
||
|
this._gsource.destroy();
|
||
|
this._gsource = null;
|
||
|
}
|
||
|
super._cleanup();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export class IdlePromise extends GSourcePromise {
|
||
|
constructor(priority, cancellable) {
|
||
|
if (arguments.length === 1 && priority instanceof Function) {
|
||
|
super(priority);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (priority === undefined)
|
||
|
priority = GLib.PRIORITY_DEFAULT_IDLE;
|
||
|
|
||
|
super(GLib.idle_source_new(), priority, cancellable);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export class TimeoutPromise extends GSourcePromise {
|
||
|
constructor(interval, priority, cancellable) {
|
||
|
if (arguments.length === 1 && interval instanceof Function) {
|
||
|
super(interval);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (!Number.isInteger(interval) || interval < 0)
|
||
|
throw TypeError('Invalid interval');
|
||
|
|
||
|
super(GLib.timeout_source_new(interval), priority, cancellable);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export class TimeoutSecondsPromise extends GSourcePromise {
|
||
|
constructor(interval, priority, cancellable) {
|
||
|
if (arguments.length === 1 && interval instanceof Function) {
|
||
|
super(interval);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (!Number.isInteger(interval) || interval < 0)
|
||
|
throw TypeError('Invalid interval');
|
||
|
|
||
|
super(GLib.timeout_source_new_seconds(interval), priority, cancellable);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export class MetaLaterPromise extends CancellablePromise {
|
||
|
constructor(laterType, cancellable) {
|
||
|
if (arguments.length === 1 && laterType instanceof Function) {
|
||
|
super(laterType);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (laterType && laterType.constructor.$gtype !== Meta.LaterType.$gtype)
|
||
|
throw new TypeError(`laterType ${laterType} is not of type Meta.LaterType`);
|
||
|
else if (!laterType)
|
||
|
laterType = Meta.LaterType.BEFORE_REDRAW;
|
||
|
|
||
|
let id;
|
||
|
super(resolve => {
|
||
|
id = Meta.later_add(laterType, () => {
|
||
|
this.remove();
|
||
|
resolve();
|
||
|
return GLib.SOURCE_REMOVE;
|
||
|
});
|
||
|
}, cancellable);
|
||
|
|
||
|
this._id = id;
|
||
|
}
|
||
|
|
||
|
_cleanup() {
|
||
|
if (this._id) {
|
||
|
Meta.later_remove(this._id);
|
||
|
this._id = 0;
|
||
|
}
|
||
|
super._cleanup();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export function _promisifySignals(proto) {
|
||
|
if (proto.connect_once)
|
||
|
return;
|
||
|
|
||
|
proto.connect_once = function (signal, cancellable) {
|
||
|
return new SignalConnectionPromise(this, signal, cancellable);
|
||
|
};
|
||
|
}
|
||
|
|
||
|
_promisifySignals(GObject.Object.prototype);
|
||
|
_promisifySignals(Signals.EventEmitter.prototype);
|