Fuck it - tasks don’t performLocal either

This commit is contained in:
Ben Gotow 2017-06-23 14:57:52 -07:00
parent 6d7fe519d9
commit 0f6fdb4256
15 changed files with 78 additions and 676 deletions

View file

@ -98,6 +98,7 @@ export default class CategoryPickerPopover extends Component {
}
const item = category.toJSON()
item.category = category
item.displayName = category.displayName
item.backgroundColor = LabelColorizer.backgroundColorDark(category)
item.usage = usageCount[category.id] || 0
item.numThreads = numThreads
@ -316,7 +317,7 @@ export default class CategoryPickerPopover extends Component {
<div className="category-item">
{icon}
<div className="category-display-name">
<BoldedSearchResult value={item.display_name} query={this.state.searchValue || ""} />
<BoldedSearchResult value={item.displayName} query={this.state.searchValue || ""} />
</div>
</div>
)

View file

@ -21,9 +21,9 @@ describe 'Category', ->
describe 'fromJSON', ->
it "should strip the INBOX. prefix from FastMail folders", ->
foo = (new Category()).fromJSON({display_name: 'INBOX.Foo'})
foo = (new Category()).fromJSON({displayName: 'INBOX.Foo'})
expect(foo.displayName).toEqual('Foo')
foo = (new Category()).fromJSON({display_name: 'INBOX'})
foo = (new Category()).fromJSON({displayName: 'INBOX'})
expect(foo.displayName).toEqual('Inbox')
describe 'category types', ->

View file

@ -9,7 +9,7 @@ describe 'Thread', ->
describe 'serialization performance', ->
xit '1,000,000 iterations', ->
iterations = 0
json = '[{"client_id":"local-76c370af-65de","server_id":"f0vkowp7zxt7djue7ifylb940","object":"thread","account_id":"1r6w6qiq3sb0o9fiwin6v87dd","snippet":"http://itunestandc.tumblr.com/tagged/itunes-terms-and-conditions/chrono _______________________________________________ http://www.macgroup.com/mailman/listinfo/smartfriends-chat","subject":"iTunes Terms And Conditions as you\'ve never seen them before","unread":true,"starred":false,"version":1,"folders":[],"labels":[{"server_id":"8cf4fn20k9pjjhjawrv3xrxo0","name":"all","display_name":"All Mail","id":"8cf4fn20k9pjjhjawrv3xrxo0"},{"server_id":"f1lq8faw8vv06m67y8f3xdf84","name":"inbox","display_name":"Inbox","id":"f1lq8faw8vv06m67y8f3xdf84"}],"participants":[{"name":"Andrew Stadler","email":"stadler@gmail.com","thirdPartyData":{}},{"name":"Smart Friends™ Chat","email":"smartfriends-chat@macgroup.com","thirdPartyData":{}}],"has_attachments":false,"last_message_received_timestamp":1446600615,"id":"f0vkowp7zxt7djue7ifylb940"}]'
json = '[{"client_id":"local-76c370af-65de","server_id":"f0vkowp7zxt7djue7ifylb940","object":"thread","account_id":"1r6w6qiq3sb0o9fiwin6v87dd","snippet":"http://itunestandc.tumblr.com/tagged/itunes-terms-and-conditions/chrono _______________________________________________ http://www.macgroup.com/mailman/listinfo/smartfriends-chat","subject":"iTunes Terms And Conditions as you\'ve never seen them before","unread":true,"starred":false,"version":1,"folders":[],"labels":[{"server_id":"8cf4fn20k9pjjhjawrv3xrxo0","name":"all","displayName":"All Mail","id":"8cf4fn20k9pjjhjawrv3xrxo0"},{"server_id":"f1lq8faw8vv06m67y8f3xdf84","name":"inbox","display_name":"Inbox","id":"f1lq8faw8vv06m67y8f3xdf84"}],"participants":[{"name":"Andrew Stadler","email":"stadler@gmail.com","thirdPartyData":{}},{"name":"Smart Friends™ Chat","email":"smartfriends-chat@macgroup.com","thirdPartyData":{}}],"has_attachments":false,"last_message_received_timestamp":1446600615,"id":"f0vkowp7zxt7djue7ifylb940"}]'
start = Date.now()
while iterations < 1000000
if _.isString(json)

View file

@ -20,7 +20,7 @@ xdescribe "DestroyCategoryTask", ->
fn.calls[0].args[0].accountId
nameOf = (fn) ->
fn.calls[0].args[0].body.display_name
fn.calls[0].args[0].body.displayName
makeAccount = ({usesFolders, usesLabels} = {}) ->
spyOn(AccountStore, "accountForId").andReturn {

View file

@ -1,18 +1,10 @@
import _ from 'underscore';
import net from 'net';
import fs from 'fs';
import Actions from './actions';
import DatabaseStore from './stores/database-store';
import DatabaseChangeRecord from './stores/database-change-record';
import Utils from './models/utils';
const Message = {
DATABASE_STORE_TRIGGER: 'db-store-trigger',
};
const printToConsole = false;
class ActionBridgeCPP {
constructor() {
@ -27,10 +19,12 @@ class ActionBridgeCPP {
console.info(err);
}
this.clients = [];
// This server listens on a Unix socket at /var/run/mysocket
const unixServer = net.createServer((c) => {
// Do something with the client connection
console.log('client connected');
this.clients.push(c);
c.on('data', (d) => {
this.onIncomingMessage(d.toString());
});
@ -43,9 +37,8 @@ class ActionBridgeCPP {
c.on('end', () => {
console.log('client disconnected');
this.clients = this.clients.filter((o) => o !== c);
});
// c.write('hello\r\n');
// c.pipe(c);
});
unixServer.listen('/tmp/cmail.sock', () => {
@ -77,6 +70,19 @@ class ActionBridgeCPP {
}
}
onTellClients(json) {
const msg = JSON.stringify(json, Utils.registeredObjectReplacer);
const headerBuffer = new Buffer(4);
const contentBuffer = Buffer.from(msg);
headerBuffer.fill(0);
headerBuffer.writeUInt32LE(contentBuffer.length, 0);
for (const c of this.clients) {
c.write(headerBuffer);
c.write(contentBuffer);
}
}
onBeforeUnload(readyToUnload) {
// Unfortunately, if you call ipc.send and then immediately close the window,
// Electron won't actually send the message. To work around this, we wait an

View file

@ -87,14 +87,13 @@ export default class Category extends Model {
queryable: true,
modelKey: 'path',
}),
imapName: Attributes.String({
modelKey: 'imapName',
jsonKey: 'imap_name',
}),
syncProgress: Attributes.Object({
modelKey: 'syncProgress',
jsonKey: 'sync_progress',
}),
_refcount: Attributes.Number({
modelKey: '_refcount',
}),
});
static Types = {

View file

@ -147,6 +147,10 @@ export default class Message extends ModelWithMetadata {
modelKey: 'subject',
}),
folderImapUID: Attributes.Number({
modelKey: 'folderImapUID',
}),
draft: Attributes.Boolean({
modelKey: 'draft',
queryable: true,

View file

@ -30,16 +30,14 @@ class RecentlyReadStore extends NylasStore {
tasks.filter(task =>
task instanceof ChangeUnreadTask
).forEach(({threads}) => {
const threadIds = threads.map(t => (t.id ? t.id : t));
).forEach(({threadIds}) => {
this.ids = this.ids.concat(threadIds);
changed = true;
});
tasks.filter(task =>
task instanceof ChangeLabelsTask || task instanceof ChangeFolderTask
).forEach(({threads}) => {
const threadIds = threads.map(t => (t.id ? t.id : t));
).forEach(({threadIds}) => {
this.ids = this.ids.filter(id => !threadIds.includes(id));
changed = true;
});

View file

@ -124,20 +124,14 @@ class TaskQueue extends NylasStore {
console.log(task);
throw new Error("Tasks must have an ID prior to being queued. Check that your Task constructor is calling `super`");
}
if (!task.queueState) {
console.log(task);
throw new Error("Tasks must have a queueState prior to being queued. Check that your Task constructor is calling `super`");
}
task.sequentialId = ++this._currentSequentialId;
task.runLocal().then(() => {
DatabaseStore.inTransaction((t) => {
return t.persistModel(task);
});
});
task.status = 'local';
NylasEnv.actionBridgeCpp.onTellClients({type: 'task-queued', task: task});
}
enqueueUndoOfTaskId = (taskId) => {
const task = this._queue.find(t => t.id == taskId) || this._completed.find(t => t.id == taskId);
const task = this._queue.find(t => t.id === taskId) || this._completed.find(t => t.id === taskId);
if (task) {
this.enqueue(task.createUndoTask());
}

View file

@ -1,11 +1,5 @@
import _ from 'underscore';
import Thread from '../models/thread';
import Category from '../models/category';
import Message from '../models/message';
import Actions from '../actions'
import DatabaseStore from '../stores/database-store';
import ChangeMailTask from './change-mail-task';
import SyncbackCategoryTask from './syncback-category-task';
// Public: Create a new task to apply labels to a message or thread.
//
@ -34,10 +28,6 @@ export default class ChangeFolderTask extends ChangeMailTask {
return "Moving to folder";
}
categoriesToAdd() {
return [this.folder];
}
description() {
if (this.taskDescription) {
return this.taskDescription;
@ -48,26 +38,22 @@ export default class ChangeFolderTask extends ChangeMailTask {
folderText = ` to ${this.folder.displayName}`;
}
if (this.threads.length > 1) {
return `Moved ${this.threads.length} threads${folderText}`;
} else if (this.messages.length > 1) {
return `Moved ${this.messages.length} messages${folderText}`;
if (this.threadIds.length > 1) {
return `Moved ${this.threadIds.length} threads${folderText}`;
} else if (this.messageIds.length > 1) {
return `Moved ${this.messageIds.length} messages${folderText}`;
}
return `Moved${folderText}`;
}
isDependentOnTask(other) {
return super.isDependentOnTask(other) || (other instanceof SyncbackCategoryTask);
}
performLocal() {
if (!this.folder) {
return Promise.reject(new Error("Must specify a `folder`"))
}
if (this.threads.length > 0 && this.messages.length > 0) {
if (this.threadIds.length > 0 && this.messageIds.length > 0) {
return Promise.reject(new Error("ChangeFolderTask: You can move `threads` or `messages` but not both"))
}
if (this.threads.length === 0 && this.messages.length === 0) {
if (this.threadIds.length === 0 && this.messageIds.length === 0) {
return Promise.reject(new Error("ChangeFolderTask: You must provide a `threads` or `messages` Array of models or IDs."))
}
@ -77,64 +63,4 @@ export default class ChangeFolderTask extends ChangeMailTask {
_isArchive() {
return this.folder.name === "archive" || this.folder.name === "all"
}
recordUserEvent() {
if (this.source === "Mail Rules") {
return
}
Actions.recordUserEvent("Threads Moved to Folder", {
source: this.source,
isArchive: this._isArchive(),
folderType: this.folder.name || "custom",
folderDisplayName: this.folder.displayName,
numThreads: this.threads.length,
numMessages: this.messages.length,
description: this.description(),
isUndo: this._isUndoTask,
})
}
retrieveModels() {
return Promise.props({
folder: DatabaseStore.modelify(Category, [this.folder]),
threads: DatabaseStore.modelify(Thread, this.threads),
messages: DatabaseStore.modelify(Message, this.messages),
}).then(({folder, threads, messages}) => {
// Remove any objects we weren't able to find. This can happen pretty easily
// if (you undo an action && other things have happened.)
this.folder = folder[0];
this.threads = _.compact(threads);
this.messages = _.compact(messages);
if (!this.folder) {
return Promise.reject(new Error("The specified folder could not be found."));
}
return Promise.resolve();
});
}
processNestedMessages() {
return false;
}
changesToModel(model) {
if (model instanceof Thread) {
return {categories: [this.folder]}
}
if (model instanceof Message) {
return {categories: [this.folder]}
}
return null;
}
requestBodyForModel(model) {
if (model instanceof Thread) {
return {folder: model.folders[0] ? model.folders[0].id : null};
}
if (model instanceof Message) {
return {folder: model.folder ? model.folder.id : null};
}
return null;
}
}

View file

@ -1,13 +1,5 @@
import _ from 'underscore';
import Thread from '../models/thread';
import Message from '../models/message';
import Actions from '../actions'
import Category from '../models/category';
import DatabaseStore from '../stores/database-store';
import CategoryStore from '../stores/category-store';
import AccountStore from '../stores/account-store';
import ChangeMailTask from './change-mail-task';
import SyncbackCategoryTask from './syncback-category-task';
// Public: Create a new task to apply labels to a message or thread.
//
@ -44,8 +36,8 @@ export default class ChangeLabelsTask extends ChangeMailTask {
}
let countString = "";
if (this.threads.length > 1) {
countString = ` ${this.threads.length} threads`;
if (this.threadIds.length > 1) {
countString = ` ${this.threadIds.length} threads`;
}
const removed = this.labelsToRemove[0];
@ -88,163 +80,8 @@ export default class ChangeLabelsTask extends ChangeMailTask {
return `Changed labels${countString ? ' on' : ''}${countString}`;
}
isDependentOnTask(other) {
return super.isDependentOnTask(other) || (other instanceof SyncbackCategoryTask);
}
// In Gmail all threads /must/ belong to either All Mail, Trash and Spam, and
// they are mutually exclusive, so we need to make sure that any add/remove
// label operation still guarantees that constraint
_ensureAndUpdateLabels(account, existingLabelsToAdd, existingLabelsToRemove = {}) {
const labelsToAdd = existingLabelsToAdd;
let labelsToRemove = existingLabelsToRemove;
const setToAdd = new Set(_.compact(_.pluck(labelsToAdd, 'name')));
const setToRemove = new Set(_.compact(_.pluck(labelsToRemove, 'name')));
if (setToRemove.has('all')) {
if (!setToAdd.has('spam') && !setToAdd.has('trash')) {
labelsToRemove = _.reject(labelsToRemove, label => label.name === 'all');
}
} else if (setToAdd.has('all')) {
if (!setToRemove.has('trash')) {
labelsToRemove.push(CategoryStore.getTrashCategory(account));
}
if (!setToRemove.has('spam')) {
labelsToRemove.push(CategoryStore.getSpamCategory(account));
}
}
if (setToRemove.has('trash')) {
if (!setToAdd.has('spam') && !setToAdd.has('all')) {
labelsToAdd.push(CategoryStore.getAllMailCategory(account));
}
} else if (setToAdd.has('trash')) {
if (!setToRemove.has('all')) {
labelsToRemove.push(CategoryStore.getAllMailCategory(account))
}
if (!setToRemove.has('spam')) {
labelsToRemove.push(CategoryStore.getSpamCategory(account))
}
}
if (setToRemove.has('spam')) {
if (!setToAdd.has('trash') && !setToAdd.has('all')) {
labelsToAdd.push(CategoryStore.getAllMailCategory(account));
}
} else if (setToAdd.has('spam')) {
if (!setToRemove.has('all')) {
labelsToRemove.push(CategoryStore.getAllMailCategory(account))
}
if (!setToRemove.has('trash')) {
labelsToRemove.push(CategoryStore.getTrashCategory(account))
}
}
// This should technically not be possible, but we like to keep it safe
return {
labelsToAdd: _.compact(labelsToAdd),
labelsToRemove: _.compact(labelsToRemove),
};
}
performLocal() {
if (this.messages.length > 0) {
return Promise.reject(new Error("ChangeLabelsTask: N1 does not support viewing or changing labels on individual messages."))
}
if (this.labelsToAdd.length === 0 && this.labelsToRemove.length === 0) {
return Promise.reject(new Error("ChangeLabelsTask: Must specify `labelsToAdd` or `labelsToRemove`"))
}
if (this.threads.length > 0 && this.messages.length > 0) {
return Promise.reject(new Error("ChangeLabelsTask: You can move `threads` or `messages` but not both"))
}
if (this.threads.length === 0 && this.messages.length === 0) {
return Promise.reject(new Error("ChangeLabelsTask: You must provide a `threads` or `messages` Array of models or IDs."))
}
return super.performLocal();
}
_isArchive() {
const toAdd = this.labelsToAdd.map(l => l.name)
return toAdd.includes("all") || toAdd.includes("archive")
}
recordUserEvent() {
if (this.source === "Mail Rules") {
return
}
Actions.recordUserEvent("Threads Changed Labels", {
source: this.source,
isArchive: this._isArchive(),
labelTypesToAdd: this.labelsToAdd.map(l => l.name || "custom"),
labelTypesToRemove: this.labelsToRemove.map(l => l.name || "custom"),
labelDisplayNamesToAdd: this.labelsToAdd.map(l => l.displayName),
labelDisplayNamesToRemove: this.labelsToRemove.map(l => l.displayName),
numThreads: this.threads.length,
numMessages: this.messages.length,
description: this.description(),
isUndo: this._isUndoTask,
})
}
retrieveModels() {
// Convert arrays of IDs or models to models.
// modelify returns immediately if (no work is required)
return Promise.props({
labelsToAdd: DatabaseStore.modelify(Category, this.labelsToAdd),
labelsToRemove: DatabaseStore.modelify(Category, this.labelsToRemove),
threads: DatabaseStore.modelify(Thread, this.threads),
messages: DatabaseStore.modelify(Message, this.messages),
}).then(({labelsToAdd, labelsToRemove, threads, messages}) => {
if (_.any([].concat(labelsToAdd, labelsToRemove), _.isUndefined)) {
return Promise.reject(new Error("One or more of the specified labels could not be found."))
}
const account = AccountStore.accountForItems(threads);
if (!account) {
return Promise.reject(new Error("ChangeLabelsTask: You must provide a set of `threads` from the same Account"))
}
// In Gmail all threads /must/ belong to either All Mail, Trash and Spam, and
// they are mutually exclusive, so we need to make sure that any add/remove
// label operation still guarantees that constraint
const updated = this._ensureAndUpdateLabels(account, labelsToAdd, labelsToRemove)
// Remove any objects we weren't able to find. This can happen pretty easily
// if (you undo an action && other things have happened.)
this.labelsToAdd = updated.labelsToAdd;
this.labelsToRemove = updated.labelsToRemove;
this.threads = _.compact(threads);
this.messages = _.compact(messages);
// The base class does the heavy lifting and calls changesToModel
return Promise.resolve();
});
}
processNestedMessages() {
return false;
}
changesToModel(model) {
const labelsToRemoveIds = _.pluck(this.labelsToRemove, 'id')
let labels = _.reject(model.labels, ({id}) => labelsToRemoveIds.includes(id));
labels = labels.concat(this.labelsToAdd);
labels = _.uniq(labels, false, label => label.id);
return {labels};
}
requestBodyForModel(model) {
const folder = model.labels.find(l => l.object === 'folder')
const labels = model.labels.filter(l => l.object === 'label')
if (folder) {
return {
folder: folder.id,
labels: labels.map(l => l.id),
}
}
return {labels};
}
}

View file

@ -2,10 +2,6 @@ import _ from 'underscore';
import Task from './task';
import Thread from '../models/thread';
import Message from '../models/message';
import NylasAPI from '../nylas-api';
import DatabaseStore from '../stores/database-store';
import EnsureMessageInSentFolderTask from './ensure-message-in-sent-folder-task'
import BaseDraftTask from './base-draft-task'
/*
Public: The ChangeMailTask is a base class for all tasks that modify sets
@ -30,164 +26,18 @@ export default class ChangeMailTask extends Task {
constructor({threads, thread, messages, message} = {}) {
super();
this.threads = threads || [];
const t = threads || [];
if (thread) {
this.threads.push(thread);
t.push(thread);
}
this.messages = messages || [];
const m = messages || [];
if (message) {
this.messages.push(message);
}
}
// Functions for subclasses
// Public: Override this method and return an object with key-value pairs
// representing changed values. For example, if (your task sets unread:)
// false, return {unread: false}.
//
// - `model` an individual {Thread} or {Message}
//
// Returns an object whos key-value pairs represent the desired changed
// object.
changesToModel() {
throw new Error("You must override this method.");
}
// Public: Override this method and return an object that will be the
// request body used for saving changes to `model`.
//
// - `model` an individual {Thread} or {Message}
//
// Returns an object that will be passed as the `body` to the actual API
// `request` object
requestBodyForModel() {
throw new Error("You must override this method.");
}
// Public: Override to indicate whether actions need to be taken for all
// messages of each thread.
//
// Generally, you cannot provide both messages and threads at the same
// time. However, ChangeMailTask runs for provided threads first and then
// messages. Override and return true, and you will receive
// `changesToModel` for messages in changed threads, and any changes you
// make will be written to the database and undone during undo.
//
// Note that API requests are only made for threads if (threads are)
// present.
processNestedMessages() {
return false;
}
// Public: Returns categories that this task will add to the set of threads
// Must be overriden
categoriesToAdd() {
return [];
}
// Public: Returns categories that this task will remove the set of threads
// Must be overriden
categoriesToRemove() {
return [];
}
// Public: Subclasses should override `performLocal` and call super once
// they've prepared the data they need and verified that requirements are
// met.
// See {Task::performLocal} for more usage info
performLocal() {
if (this._isUndoTask && !this._restoreValues) {
return Promise.reject(new Error("ChangeMailTask: No _restoreValues provided for undo task."))
}
// Lock the models with the optimistic change tracker so they aren't reverted
// while the user is seeing our optimistic changes.
if (!this._isReverting) {
this._lockAll();
m.push(message);
}
return DatabaseStore.inTransaction((t) => {
return this.retrieveModels().then(() => {
return this._performLocalThreads(t)
}).then(() => {
return this._performLocalMessages(t)
})
}).then(() => {
try {
this.recordUserEvent()
} catch (err) {
NylasEnv.reportError(err);
// don't throw
}
});
}
recordUserEvent() {
throw new Error("Override recordUserEvent")
}
retrieveModels() {
// Note: Currently, *ALL* subclasses must use `DatabaseStore.modelify`
// to convert `threads` and `messages` from models or ids to models.
return Promise.resolve();
}
_performLocalThreads(transaction) {
const changed = this._applyChanges(this.threads);
const changedIds = _.pluck(changed, 'id');
if (changed.length === 0) {
return Promise.resolve();
}
return transaction.persistModels(changed).then(() => {
if (!this.processNestedMessages()) {
return Promise.resolve();
}
return DatabaseStore.findAll(Message).where(Message.attributes.threadId.in(changedIds)).then((messages) => {
this.messages = [].concat(messages, this.messages);
return Promise.resolve()
})
});
}
_performLocalMessages(transaction) {
const changed = this._applyChanges(this.messages);
return (changed.length > 0) ? transaction.persistModels(changed) : Promise.resolve();
}
_applyChanges(modelArray) {
const changed = [];
if (this._shouldChangeBackwards()) {
modelArray.forEach((model, idx) => {
if (this._restoreValues[model.id]) {
const updated = _.extend(model.clone(), this._restoreValues[model.id]);
modelArray[idx] = updated;
changed.push(updated);
}
});
} else {
this._restoreValues = this._restoreValues || {};
modelArray.forEach((model, idx) => {
const fieldsNew = this.changesToModel(model);
const fieldsCurrent = _.pick(model, Object.keys(fieldsNew));
if (!_.isEqual(fieldsCurrent, fieldsNew)) {
this._restoreValues[model.id] = fieldsCurrent;
const updated = _.extend(model.clone(), fieldsNew);
modelArray[idx] = updated;
changed.push(updated);
}
});
}
return changed;
}
_shouldChangeBackwards() {
return this._isReverting || this._isUndoTask;
// we actually only keep a small bit of data now
this.threadIds = t.map(i => i.id);
this.messageIds = m.map(i => i.id);
}
// Task lifecycle
@ -215,50 +65,19 @@ export default class ChangeMailTask extends Task {
}
createIdenticalTask() {
const task = new this.constructor(this);
// Never give the undo task the Model objects - make it look them up!
// This ensures that they never revert other fields
const toIds = (arr) => _.map(arr, v => (_.isString(v) ? v : v.id));
task.threads = toIds(this.threads);
task.messages = (this.threads.length > 0) ? [] : toIds(this.messages);
return task;
return new this.constructor(this);
}
objectIds() {
return [].concat(this.threads, this.messages).map((v) =>
(_.isString(v) ? v : v.id)
);
return [].concat(this.threadIds, this.messageIds);
}
objectClass() {
return (this.threads && this.threads.length) ? Thread : Message;
}
objectArray() {
return (this.threads && this.threads.length) ? this.threads : this.messages;
return (this.threadIds && this.threadIds.length) ? Thread : Message;
}
numberOfImpactedItems() {
return this.objectArray().length;
}
// Helpers used in subclasses
_lockAll() {
const klass = this.objectClass();
this._locked = this._locked || {};
for (const item of this.objectArray()) {
this._locked[item.id] = this._locked[item.id] || 0;
this._locked[item.id] += 1;
NylasAPI.incrementRemoteChangeLock(klass, item.id);
}
}
_removeLock(item) {
const klass = this.objectClass();
NylasAPI.decrementRemoteChangeLock(klass, item.id);
this._locked[item.id] -= 1;
return this.objectIds().length;
}
}

View file

@ -17,7 +17,7 @@ export default class ChangeStarredTask extends ChangeMailTask {
}
description() {
const count = this.threads.length;
const count = this.threadIds.length;
const type = count > 1 ? "threads" : "thread";
if (this._isUndoTask) {
@ -32,7 +32,7 @@ export default class ChangeStarredTask extends ChangeMailTask {
}
performLocal() {
if (this.threads.length === 0) {
if (this.threadIds.length === 0) {
return Promise.reject(new Error("ChangeStarredTask: You must provide a `threads` Array of models or IDs."));
}
return super.performLocal();
@ -45,26 +45,9 @@ export default class ChangeStarredTask extends ChangeMailTask {
const eventName = this.unread ? "Starred" : "Unstarred";
Actions.recordUserEvent(`Threads ${eventName}`, {
source: this.source,
numThreads: this.threads.length,
numThreads: this.threadIds.length,
description: this.description(),
isUndo: this._isUndoTask,
})
}
retrieveModels() {
return Promise.props({
threads: DatabaseStore.modelify(Thread, this.threads),
}).then(({threads}) => {
this.threads = _.compact(threads);
return Promise.resolve();
})
}
changesToModel(model) {
return {starred: this.starred};
}
requestBodyForModel(model) {
return {starred: model.starred};
}
}

View file

@ -18,7 +18,7 @@ export default class ChangeUnreadTask extends ChangeMailTask {
}
description() {
const count = this.threads.length;
const count = this.threadIds.length;
const type = count > 1 ? 'threads' : 'thread';
if (this._isUndoTask) {
@ -38,47 +38,4 @@ export default class ChangeUnreadTask extends ChangeMailTask {
}
return this._canBeUndone
}
performLocal() {
if (this.threads.length === 0) {
return Promise.reject(new Error("ChangeUnreadTask: You must provide a `threads` Array of models or IDs."))
}
return super.performLocal();
}
recordUserEvent() {
if (this.source === "Mail Rules") {
return
}
const eventName = this.unread ? "Unread" : "Read";
Actions.recordUserEvent(`Threads Marked as ${eventName}`, {
source: this.source,
numThreads: this.threads.length,
description: this.description(),
isUndo: this._isUndoTask,
})
}
retrieveModels() {
// Convert arrays of IDs or models to models.
// modelify returns immediately if (no work is required)
return Promise.props({
threads: DatabaseStore.modelify(Thread, this.threads),
}).then(({threads}) => {
this.threads = _.compact(threads);
return Promise.resolve();
});
}
processNestedMessages() {
return true;
}
changesToModel(model) {
return {unread: this.unread};
}
requestBodyForModel(model) {
return {unread: model.unread};
}
}

View file

@ -1,6 +1,7 @@
/* eslint no-unused-vars: 0*/
import _ from 'underscore';
import Model from '../models/model';
import Attributes from '../attributes';
import {generateTempId} from '../models/utils';
import {PermanentErrorCodes} from '../nylas-api';
import {APIError} from '../errors';
@ -13,10 +14,23 @@ const TaskStatus = {
};
export default class Task extends Model {
static Status = TaskStatus;
static SubclassesUseModelTable = Task;
static attributes = Object.assign({}, Model.attributes, {
version: Attributes.String({
queryable: true,
modelKey: 'version',
}),
status: Attributes.String({
queryable: true,
modelKey: 'status',
}),
accountId: Attributes.String({
modelKey: 'accountId',
}),
});
// Public: Override the constructor to pass initial args to your Task and
// initialize instance variables.
//
@ -26,147 +40,11 @@ export default class Task extends Model {
// On construction, all Tasks instances are given a unique `id`.
constructor() {
super();
this.version = 1;
this._rememberedToCallSuper = true;
this.id = generateTempId();
this.accountId = "1"; //TODO BG hack
this.sequentialId = null; // set when queued
this.queueState = {
isProcessing: false,
localError: null,
localComplete: false,
remoteError: null,
remoteAttempts: 0,
remoteComplete: false,
status: null,
};
}
// Private: This is a internal wrapper around `performLocal`
runLocal() {
if (!this._rememberedToCallSuper) {
throw new Error("Your must call `super` from your Task's constructors");
}
if (this.queueState.localComplete) {
return Promise.resolve();
}
try {
return this.performLocal()
.then(() => {
this.queueState.localComplete = true;
this.queueState.localError = null;
return Promise.resolve();
})
.catch(this._handleLocalError);
} catch (err) {
return this._handleLocalError(err);
}
}
_handleLocalError = (err) => {
this.queueState.localError = err;
this.queueState.status = Task.Status.Failed;
NylasEnv.reportError(err);
return Promise.reject(err);
}
// HELPER METHODS
validateRequiredFields = (fields = []) => {
for (const field of fields) {
if (!this[field]) {
throw new Error(`Must pass ${field}`);
}
}
}
// METHODS TO OBSERVE
//
// Public: **Required** | Override to perform local, optimistic updates.
//
// Most tasks will put code in here that updates the {DatabaseStore}
//
// You should also implement the rollback behavior inside of
// `performLocal` or in some helper method. It's common practice (but not
// automatic) for `performLocal` to be re-called at the end of an API
// failure from `performRemote`.
//
// That rollback behavior is also likely the same when you want to undo a
// task. It's common practice (but not automatic) for `createUndoTask` to
// set some flag that `performLocal` will recognize to implement the
// rollback behavior.
//
// `performLocal` will complete BEFORE the task actually enters the
// {TaskQueue}.
//
// if (you would like to do work after `performLocal` has run, you can use)
// {TaskQueue::waitForPerformLocal}. Pass it the task and it
// will return a Promise that resolves once the local action has
// completed. This is contained in the {TaskQueue} so you can
// listen to tasks across windows.
//
// ## Examples
//
// ### Simple Optimistic Updating
//
// ```js
// class MyTask extends Task {
// performLocal() {
// this.updatedModel = this._myModelUpdateCode()
// return DatabaseStore.inTransaction((t) => persistModel(this.updatedModel));
// }
// }
// ```
//
// ### Handling rollback on API failure
//
// ```js
// class MyTask extends Task
// performLocal() {
// if (this._reverting) {
// this.updatedModel = this._myModelRollbackCode();
// } else {
// this.updatedModel = this._myModelUpdateCode();
// }
// return DatabaseStore.inTransaction((t) => persistModel(this.updatedModel));
// }
// performRemote() {
// return this._APIPutHelperMethod(this.updatedModel).catch((apiError) => {
// if (apiError.statusCode === 500) {
// this._reverting = true;
// return this.performLocal();
// }
// }
// }
// }
// ```
//
// ### Handling an undo task
//
// ```js
// class MyTask extends Task {
// performLocal() {
// if (this._isUndoTask) {
// this.updatedModel = this._myModelRollbackCode();
// } else {
// this.updatedModel = this._myModelUpdateCode();
// }
// return DatabaseStore.inTransaction((t) => persistModel(this.updatedModel));
// }
//
// createUndoTask() {
// undoTask = this.createIdenticalTask();
// undoTask._isUndoTask = true;
// return undoTask;
// }
// }
// ```
//
// Also see the documentation on the required undo methods
//
// Returns a {Promise} that resolves when your updates are complete.
performLocal() {
return Promise.resolve();
}
// Public: It's up to you to determine how you want to indicate whether
@ -196,7 +74,7 @@ export default class Task extends Model {
// Public: Return a deep-cloned task to be used for an undo task
createIdenticalTask() {
const json = this.toJSON();
delete json.queueState;
delete json.status;
return (new this.constructor()).fromJSON(json);
}
@ -224,7 +102,7 @@ export default class Task extends Model {
// Private: Allows for serialization of tasks
toJSON() {
return this;
return Object.assign(super.toJSON(), this);
}
// Private: Allows for deserialization of tasks