Skip to content
This repository has been archived by the owner on Jan 9, 2023. It is now read-only.

Commit

Permalink
Introduce ember-concurrency with examples (#977)
Browse files Browse the repository at this point in the history
* Add ember-concurrency addon version 0.7.19

* Refactor admin/address/route to use e-c

This is the example I used in issue #969. It is illustrates how to use
ember-concurrency tasks in routes.

* Refactor patients/delete/controller to use e-c

Example of using ember-concurrency tasks to manage a complex promise
chain of events. I picked this one mainly because I wanted to illustrate
the simplicity of using e-c tasks to manage what was otherwise a very
complex promise chain.

I tried to preserve some of the concurrency described in the previous
promise based code. However, after some analysis and discussion on the
Ember Slack channels difference between preserving the concurrency and
just running the resolutions serially are likely very small. In this
case the use of `all()` could likely be removed without a significant
impact on performance. I mention this later optimisation as a way to
make the code even easier to grok. I'll leave the debate to further
discussion.
  • Loading branch information
sukima authored and jkleinsc committed Mar 16, 2017
1 parent 24665f1 commit 78a2023
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 109 deletions.
48 changes: 28 additions & 20 deletions app/admin/address/route.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,39 @@
import AbstractEditRoute from 'hospitalrun/routes/abstract-edit-route';
import Ember from 'ember';
import AbstractEditRoute from 'hospitalrun/routes/abstract-edit-route';
import { task } from 'ember-concurrency';
import { translationMacro as t } from 'ember-i18n';
import UnauthorizedError from 'hospitalrun/utils/unauthorized-error';

const { computed } = Ember;

export default AbstractEditRoute.extend({
hideNewButton: true,
newTitle: t('admin.address.newTitle'),
editTitle: t('admin.address.editTitle'),

model() {
return new Ember.RSVP.Promise((resolve, reject) => {
this.get('store').find('option', 'address_options').then((addressOptions) => {
resolve(addressOptions);
}, (err) => {
if (err instanceof UnauthorizedError) {
reject(err);
} else {
let store = this.get('store');
let newConfig = store.push(store.normalize('option', {
id: 'address_options',
value: {
address1Label: this.get('i18n').t('admin.address.addressLabel'),
address1Include: true
}
}));
resolve(newConfig);
}
});
return this.get('fetchAddressOptions').perform();
},

fetchAddressOptions: task(function* () {
let store = this.get('store');
try {
return yield store.find('option', 'address_options');
} catch(err) {
if (err instanceof UnauthorizedError) {
throw err;
}
return store.push(this.get('defaultAddressOption'));
}
}).keepLatest().cancelOn('deactivate'),

defaultAddressOption: computed(function() {
return this.get('store').normalize('option', {
id: 'address_options',
value: {
address1Label: this.get('i18n').t('admin.address.addressLabel'),
address1Include: true
}
});
}
})
});
183 changes: 94 additions & 89 deletions app/patients/delete/controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,116 +6,121 @@ import PouchDbMixin from 'hospitalrun/mixins/pouchdb';
import ProgressDialog from 'hospitalrun/mixins/progress-dialog';
import Ember from 'ember';
import { translationMacro as t } from 'ember-i18n';
import { task, taskGroup, all } from 'ember-concurrency';

function deleteMany(manyArray) {
if (!manyArray) {
return Ember.RSVP.resolve();
}
if (manyArray.then) {
// recursive call after resolving async model
return manyArray.then(deleteMany);
}
let recordsCount = manyArray.get('length');
if (!recordsCount) {
// empty array: no records to delete
return Ember.RSVP.resolve();
}
let archivePromises = manyArray.map((recordToDelete) => {
recordToDelete.set('archived', true);
return recordToDelete.save().then(() => {
return recordToDelete.unloadRecord();
});
});
return Ember.RSVP.all(archivePromises, 'async array deletion');
}
const MAX_CONCURRENCY = 5;

export default AbstractDeleteController.extend(PatientVisitsMixin, PatientInvoicesMixin, PouchDbMixin, ProgressDialog, PatientAppointmentsMixin, {
title: t('patients.titles.delete'),
progressTitle: t('patients.titles.deletePatientRecord'),
progressMessage: t('patients.messages.deletingPatient'),
deleting: taskGroup(),

deleteMany(manyArray) {
return this.get('deleteManyTask').perform(manyArray);
},

deleteManyTask: task(function* (manyArray) {
if (!manyArray) {
return;
}
let resolvedArray = yield manyArray;
if (Ember.isEmpty(resolvedArray)) {
// empty array: no records to delete
return;
}
let deleteRecordTask = this.get('deleteRecordTask');
let archivePromises = [];
for (let recordToDelete of resolvedArray) {
archivePromises.push(deleteRecordTask.perform(recordToDelete));
}
return yield all(archivePromises, 'async array deletion');
}).group('deleting'),

deleteRecordTask: task(function* (recordToDelete) {
recordToDelete.set('archived', true);
yield recordToDelete.save();
return yield recordToDelete.unloadRecord();
}).maxConcurrency(MAX_CONCURRENCY).enqueue().group('deleting'),

// Override delete action on controller; we must delete
// all related records before deleting patient record
// otherwise errors will occur
deletePatient() {
let controller = this;
let patient = this.get('model');
let visits = this.getPatientVisits(patient);
let invoices = this.getPatientInvoices(patient);
let appointments = this.getPatientAppointments(patient);
let payments = patient.get('payments');
// resolve all async models first since they reference each other, then delete
return Ember.RSVP.all([visits, invoices, appointments, payments]).then(function(records) {
let promises = [];
promises.push(controller.deleteVisits(records[0]));
promises.push(controller.deleteInvoices(records[1]));
promises.push(deleteMany(records[2])); // appointments
promises.push(deleteMany(records[3])); // payments
return Ember.RSVP.all(promises)
.then(function() {
return patient.destroyRecord();
});
});
return this.get('deletePatientTask').perform();
},

deletePatientTask: task(function* () {
let patient = this.get('model');
let visits = yield this.getPatientVisits(patient);
let invoices = yield this.getPatientInvoices(patient);
let appointments = yield this.getPatientAppointments(patient);
let payments = yield patient.get('payments');
yield all([
this.deleteVisits(visits),
this.deleteInvoices(invoices),
this.deleteMany(appointments),
this.deleteMany(payments)
]);
return yield patient.destroyRecord();
}).group('deleting'),

deleteVisits(visits) {
let promises = [];
visits.forEach(function(visit) {
let labs = visit.get('labs');
let procedures = visit.get('procedures');
let imaging = visit.get('imaging');
let procCharges = procedures.then(function(p) {
return p.get('charges');
});
let labCharges = labs.then(function(l) {
return l.get('charges');
});
let imagingCharges = imaging.then(function(i) {
return i.get('charges');
});
let visitCharges = visit.get('charges');
promises.push(deleteMany(labs));
promises.push(deleteMany(labCharges));
promises.push(deleteMany(visit.get('patientNotes')));
promises.push(deleteMany(visit.get('vitals')));
promises.push(deleteMany(procedures));
promises.push(deleteMany(procCharges));
promises.push(deleteMany(visit.get('medication')));
promises.push(deleteMany(imaging));
promises.push(deleteMany(imagingCharges));
promises.push(deleteMany(visitCharges));
});
return Ember.RSVP.all(promises).then(function() {
return deleteMany(visits);
});
return this.get('deleteVisitsTask').perform(visits);
},

deleteVisitsTask: task(function* (visits) {
let pendingTasks = [];
for (let visit of visits) {
let labs = yield visit.get('labs');
let procedures = yield visit.get('procedures');
let imaging = yield visit.get('imaging');
let procCharges = procedures.get('charges');
let labCharges = labs.get('charges');
let imagingCharges = imaging.get('charges');
let visitCharges = visit.get('charges');
pendingTasks.push(this.deleteMany(labs));
pendingTasks.push(this.deleteMany(labCharges));
pendingTasks.push(this.deleteMany(visit.get('patientNotes')));
pendingTasks.push(this.deleteMany(visit.get('vitals')));
pendingTasks.push(this.deleteMany(procedures));
pendingTasks.push(this.deleteMany(procCharges));
pendingTasks.push(this.deleteMany(visit.get('medication')));
pendingTasks.push(this.deleteMany(imaging));
pendingTasks.push(this.deleteMany(imagingCharges));
pendingTasks.push(this.deleteMany(visitCharges));
}
yield all(pendingTasks);
return yield this.deleteMany(visits);
}).group('deleting'),

deleteInvoices(patientInvoices) {
return Ember.RSVP.resolve(patientInvoices).then(function(invoices) {
let lineItems = Ember.A();
invoices.forEach(function(i) {
lineItems.addObjects(i.get('lineItems'));
});
let lineItemDetails = Ember.A();
lineItems.forEach(function(li) {
lineItemDetails.addObjects(li.get('details'));
});
return Ember.RSVP.all([lineItems, lineItemDetails]).then(function() {
return Ember.RSVP.all([deleteMany(invoices), deleteMany(lineItems), deleteMany(lineItemDetails)]);
});
});
return this.get('deleteInvoicesTask').perform(patientInvoices);
},

actions: {
deleteInvoicesTask: task(function* (patientInvoices) {
let invoices = yield patientInvoices;
let lineItems = yield all(invoices.mapBy('lineItems'));
let lineItemDetails = yield all(lineItems.mapBy('details'));
return yield all([
this.deleteMany(invoices),
this.deleteMany(lineItems),
this.deleteMany(lineItemDetails)
]);
}).group('deleting'),

deleteActionTask: task(function* (patient) {
// delete related records without modal dialogs
this.send('closeModal');
this.showProgressModal();
yield this.deletePatient(patient);
this.closeProgressModal();
this.send(this.get('afterDeleteAction'), patient);
}).drop(),

actions: {
delete(patient) {
let controller = this;
this.send('closeModal');
this.showProgressModal();
this.deletePatient(patient).then(function() {
controller.closeProgressModal();
controller.send(controller.get('afterDeleteAction'), patient);
});
this.get('deleteActionTask').perform(patient);
}
}
});
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"ember-cli-template-lint": "0.4.12",
"ember-cli-test-loader": "^1.1.0",
"ember-cli-uglify": "^1.2.0",
"ember-concurrency": "0.7.19",
"ember-data": "^2.10.0",
"ember-export-application-global": "^1.0.5",
"ember-fullcalendar": "1.3.0",
Expand Down

0 comments on commit 78a2023

Please sign in to comment.