ember-stateful-promise
Version:
Stateful wrapper for native Promises
162 lines (145 loc) • 4.16 kB
JavaScript
import { tracked } from '@glimmer/tracking';
import { registerDestructor, isDestroying } from '@ember/destroyable';
import { CanceledPromise } from 'ember-stateful-promise/utils/canceled-promise';
import { DestroyableCanceledPromise } from 'ember-stateful-promise/utils/destroyable-canceled-promise';
export class StatefulPromise extends Promise {
/**
@type {'RUNNING' | 'RESOLVED' | 'ERROR' | 'CANCELED'}
@private
*/
_state = 'RUNNING';
_resolve = null;
_reject = null;
constructor(fn) {
let resolveFn;
let rejectFn;
let state;
let ctx;
super((resolve, reject) => {
// fn when .then or .catch is executed
// or if an executor is provided directly from the function invocation
if (fn) {
// preserve native Promise behaviour
// Interface when .then or .catch is called
fn(
(data) => {
resolve(data);
state = 'RESOLVED';
if (ctx) {
ctx._state = state;
}
},
(err) => {
reject(err);
state = 'ERROR';
if (ctx) {
ctx._state = state;
}
}
);
} else {
// store resolve/reject fns for later use in .create method
resolveFn = resolve;
rejectFn = reject;
}
});
ctx = this;
if (!fn) {
this._resolve = resolveFn;
this._reject = rejectFn;
}
// keep this b/c ctx may be undefined if no async when invoking body of StatefulPromise callback
if (state) {
this._state = state;
}
}
create(destroyable, executor) {
if (!destroyable) {
throw new Error('destroyable is required as first argument');
}
registerDestructor(destroyable, () => {
if (this._state === 'RUNNING') {
this._state = 'CANCELED';
this._reject(
new DestroyableCanceledPromise(
'The object this promise was attached to was destroyed'
)
);
}
});
// Three options for executor
// 1. a promise instance
// 2. a callback with resolve and reject arguments
// 3. a primitive or non function or Promise
if (typeof executor !== 'function') {
if (!executor.then) {
executor = Promise.resolve(executor);
}
executor
.then((result) => {
if (isDestroying(destroyable)) {
this._state = 'CANCELED';
this._reject(
new DestroyableCanceledPromise(
'The object this promise was attached to was destroyed while the promise was still outstanding'
)
);
} else {
this._state = 'RESOLVED';
this._resolve(result);
}
})
.catch((e) => {
if (e instanceof CanceledPromise) {
this._state = 'CANCELED';
} else if (e instanceof DestroyableCanceledPromise) {
this._state = 'CANCELED';
} else {
this._state = 'ERROR';
}
this._reject(e);
});
} else {
executor(
// resolve fn
(data) => {
if (isDestroying(destroyable)) {
this._state = 'CANCELED';
this._reject(
new DestroyableCanceledPromise(
'The object this promise was attached to was destroyed'
)
);
} else {
this._state = 'RESOLVED';
this._resolve(data);
}
},
// reject fn
(err) => {
if (err instanceof CanceledPromise) {
this._state = 'CANCELED';
} else if (err instanceof DestroyableCanceledPromise) {
this._state = 'CANCELED';
} else {
this._state = 'ERROR';
}
this._reject(err);
}
);
}
return this;
}
get isRunning() {
return this._state === 'RUNNING';
}
get isResolved() {
return this._state === 'RESOLVED';
}
get isError() {
return this._state === 'ERROR';
}
get isCanceled() {
return this._state === 'CANCELED';
}
}