From 78a20232a57ba4b20b81cfab9942fda839323e93 Mon Sep 17 00:00:00 2001 From: Devin Weaver Date: Thu, 16 Mar 2017 10:18:20 -0400 Subject: [PATCH] Introduce ember-concurrency with examples (#977) * 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. --- app/admin/address/route.js | 48 ++++---- app/patients/delete/controller.js | 183 +++++++++++++++--------------- package.json | 1 + 3 files changed, 123 insertions(+), 109 deletions(-) diff --git a/app/admin/address/route.js b/app/admin/address/route.js index f1fe6c8dcd..17a5b15a77 100644 --- a/app/admin/address/route.js +++ b/app/admin/address/route.js @@ -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 + } }); - } + }) }); diff --git a/app/patients/delete/controller.js b/app/patients/delete/controller.js index 71ec0d466a..e92ca64a5c 100644 --- a/app/patients/delete/controller.js +++ b/app/patients/delete/controller.js @@ -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); } } }); diff --git a/package.json b/package.json index 5501868aed..7b94fbe736 100644 --- a/package.json +++ b/package.json @@ -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",