Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
305 lines
10 KiB
JavaScript
Executable file
305 lines
10 KiB
JavaScript
Executable file
define(function(require, exports) {
|
|
|
|
var DisasterRecovery = require('./disaster-recovery');
|
|
var logic = require('logic');
|
|
|
|
/**
|
|
* The no-op operation for job operations that are not implemented.
|
|
* Returns successs in a future turn of the event loop.
|
|
*/
|
|
function unimplementedJobOperation(op, callback) {
|
|
window.setZeroTimeout(function() {
|
|
callback(null, null);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Account Mixins:
|
|
*
|
|
* This mixin function is executed from the constructor of the
|
|
* CompositeAccount and ActiveSyncAccount, with 'this' being bound to
|
|
* the main account instance. If the account has separate receive/send
|
|
* parts, they are passed as arguments. (ActiveSync's receive and send
|
|
* pieces merely reference the root account.)
|
|
*/
|
|
exports.accountConstructorMixin = function(receivePiece, sendPiece) {
|
|
// The following flags are set on the receivePiece, because the
|
|
// receiving side is what manages the job operations (and sending
|
|
// messages from the outbox is a job).
|
|
|
|
// On startup, we need to ignore any stale sendStatus information
|
|
// from messages in the outbox. See `sendOutboxMessages` in
|
|
// jobmixins.js.
|
|
receivePiece.outboxNeedsFreshSync = true;
|
|
// This is a runtime flag, used to temporarily prevent
|
|
// `sendOutboxMessages` from executing, such as when the user is
|
|
// actively trying to edit the list of messages in the Outbox.
|
|
receivePiece.outboxSyncEnabled = true;
|
|
};
|
|
|
|
/**
|
|
* @args[
|
|
* @param[op MailOp]
|
|
* @param[mode @oneof[
|
|
* @case['local_do']{
|
|
* Apply the mutation locally to our database rep.
|
|
* }
|
|
* @case['check']{
|
|
* Check if the manipulation has been performed on the server. There
|
|
* is no need to perform a local check because there is no way our
|
|
* database can be inconsistent in its view of this.
|
|
* }
|
|
* @case['do']{
|
|
* Perform the manipulation on the server.
|
|
* }
|
|
* @case['local_undo']{
|
|
* Undo the mutation locally.
|
|
* }
|
|
* @case['undo']{
|
|
* Undo the mutation on the server.
|
|
* }
|
|
* ]]
|
|
* @param[callback @func[
|
|
* @args[
|
|
* @param[error @oneof[String null]]
|
|
* ]
|
|
* ]]
|
|
* }
|
|
* ]
|
|
*/
|
|
exports.runOp = function runOp(op, mode, callback) {
|
|
console.log('runOp(' + mode + ': ' + JSON.stringify(op).substring(0, 160) +
|
|
')');
|
|
|
|
var methodName = mode + '_' + op.type;
|
|
|
|
// If the job driver doesn't support the operation, assume that it
|
|
// is a moot operation that will succeed. Assign it a no-op callback
|
|
// that completes in the next tick, so as to maintain job ordering.
|
|
var method = this._jobDriver[methodName];
|
|
if (!method) {
|
|
console.warn('Unsupported op:', op.type, 'mode:', mode);
|
|
method = unimplementedJobOperation;
|
|
}
|
|
|
|
var alreadyCompleted = false;
|
|
|
|
var jobCompletedCallback = function(error, resultIfAny, accountSaveSuggested) {
|
|
// If DisasterRecovery already called the completion callback due
|
|
// to an unforeseen error, ensure that we don't try to
|
|
// double-resolve this job.
|
|
if (alreadyCompleted) {
|
|
console.warn('Job already completed, ignoring secondary completion:',
|
|
mode, JSON.stringify(op).substring(0, 160), error, resultIfAny);
|
|
return;
|
|
}
|
|
alreadyCompleted = true;
|
|
|
|
DisasterRecovery.clearCurrentAccountOp(this);
|
|
this._jobDriver.postJobCleanup(error);
|
|
// We used to log the runOp_end here, but this is too early because the
|
|
// book-keeping for the op actually happens in the following callback. So
|
|
// we leave it to _localOpCompleted and _serverOpCompleted to log this.
|
|
// defer the callback to the next tick to avoid deep recursion
|
|
window.setZeroTimeout(function() {
|
|
callback(error, resultIfAny, accountSaveSuggested);
|
|
});
|
|
}.bind(this);
|
|
|
|
DisasterRecovery.setCurrentAccountOp(this, op, jobCompletedCallback);
|
|
|
|
// Legacy tests:
|
|
logic(this, 'runOp_begin', { mode: mode, type: op.type, op: op });
|
|
// New-style tests:
|
|
Object.defineProperty(op, '_logicAsyncEvent', {
|
|
configurable: true,
|
|
enumerable: false, // So that we don't try to JSONify it.
|
|
value: logic.startAsync(this, 'runOp', {
|
|
mode: mode, type: op.type, op: op
|
|
})
|
|
});
|
|
|
|
try {
|
|
method.call(this._jobDriver, op, jobCompletedCallback);
|
|
}
|
|
catch (ex) {
|
|
DisasterRecovery.clearCurrentAccountOp(this);
|
|
logic(this, 'opError', { mode: mode, type: op.type, ex: ex });
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Return the folder metadata for the first folder with the given type, or null
|
|
* if no such folder exists.
|
|
*/
|
|
exports.getFirstFolderWithType = function(type) {
|
|
var folders = this.folders;
|
|
for (var iFolder = 0; iFolder < folders.length; iFolder++) {
|
|
if (folders[iFolder].type === type)
|
|
return folders[iFolder];
|
|
}
|
|
return null;
|
|
};
|
|
exports.getFolderByPath = function(folderPath) {
|
|
var folders = this.folders;
|
|
for (var iFolder = 0; iFolder < folders.length; iFolder++) {
|
|
if (folders[iFolder].path === folderPath)
|
|
return folders[iFolder];
|
|
}
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* Ensure that local-only folders live in a reasonable place in the
|
|
* folder hierarchy by moving them if necessary.
|
|
*
|
|
* We proactively create local-only folders at the root level before
|
|
* we synchronize with the server; if possible, we want these
|
|
* folders to reside as siblings to other system-level folders on
|
|
* the account. This is called at the end of syncFolderList, after
|
|
* we have learned about all existing server folders.
|
|
*/
|
|
exports.normalizeFolderHierarchy = function() {
|
|
// Find a folder for which we'd like to become a sibling.
|
|
var sibling =
|
|
this.getFirstFolderWithType('drafts') ||
|
|
this.getFirstFolderWithType('sent');
|
|
|
|
// If for some reason we can't find those folders yet, that's
|
|
// okay, we will try this again after the next folder sync.
|
|
if (!sibling) {
|
|
return;
|
|
}
|
|
|
|
var parent = this.getFolderMetaForFolderId(sibling.parentId);
|
|
|
|
// NOTE: `parent` may be null if `sibling` is a top-level folder.
|
|
var foldersToMove = [this.getFirstFolderWithType('localdrafts'),
|
|
this.getFirstFolderWithType('outbox')];
|
|
|
|
foldersToMove.forEach(function(folder) {
|
|
// These folders should always exist, but we double-check here
|
|
// for safety. Also, if the folder is already in the right
|
|
// place, we're done.
|
|
if (!folder || folder.parentId === sibling.parentId) {
|
|
return;
|
|
}
|
|
|
|
console.log('Moving folder', folder.name,
|
|
'underneath', parent && parent.name || '(root)');
|
|
|
|
|
|
this.universe.__notifyRemovedFolder(this, folder);
|
|
|
|
// On `delim`: We previously attempted to discover a
|
|
// server-specific root delimiter. ActiveSync hard-codes "/". POP3
|
|
// doesn't even go that far. An empty delimiter would be
|
|
// incorrect, as it could cause folder paths to smush into one
|
|
// another. In the case where our folder doesn't specify a
|
|
// delimiter, fall back to the standard-ish '/'.
|
|
if (parent) {
|
|
folder.path = parent.path + (parent.delim || '/') + folder.name;
|
|
folder.delim = parent.delim || '/';
|
|
folder.parentId = parent.id;
|
|
folder.depth = parent.depth + 1;
|
|
} else {
|
|
folder.path = folder.name;
|
|
folder.delim = '/';
|
|
folder.parentId = null;
|
|
folder.depth = 0;
|
|
}
|
|
|
|
this.universe.__notifyAddedFolder(this, folder);
|
|
|
|
}, this);
|
|
|
|
};
|
|
|
|
/**
|
|
* Save the state of this account to the database. This entails updating all
|
|
* of our highly-volatile state (folderInfos which contains counters, accuracy
|
|
* structures, and our block info structures) as well as any dirty blocks.
|
|
*
|
|
* This should be entirely coherent because the structured clone should occur
|
|
* synchronously during this call, but it's important to keep in mind that if
|
|
* that ever ends up not being the case that we need to cause mutating
|
|
* operations to defer until after that snapshot has occurred.
|
|
*/
|
|
exports.saveAccountState = function(reuseTrans, callback, reason) {
|
|
if (!this._alive) {
|
|
logic(this, 'accountDeleted', { reason: 'saveAccountState' });
|
|
return null;
|
|
}
|
|
|
|
logic(this, 'saveAccountState_begin', { reason: reason,
|
|
folderSaveCount: null });
|
|
|
|
// Indicate save is active, in case something, like
|
|
// signaling the end of a sync, needs to run after
|
|
// a save, via runAfterSaves.
|
|
this._saveAccountStateActive = true;
|
|
if (!this._deferredSaveAccountCalls) {
|
|
this._deferredSaveAccountCalls = [];
|
|
}
|
|
|
|
if (callback)
|
|
this.runAfterSaves(callback);
|
|
|
|
var perFolderStuff = [], self = this;
|
|
for (var iFolder = 0; iFolder < this.folders.length; iFolder++) {
|
|
var folderPub = this.folders[iFolder],
|
|
folderStorage = this._folderStorages[folderPub.id],
|
|
folderStuff = folderStorage.generatePersistenceInfo();
|
|
if (folderStuff)
|
|
perFolderStuff.push(folderStuff);
|
|
}
|
|
var folderSaveCount = perFolderStuff.length;
|
|
var trans = this._db.saveAccountFolderStates(
|
|
this.id, this._folderInfos, perFolderStuff,
|
|
this._deadFolderIds,
|
|
function stateSaved() {
|
|
this._saveAccountStateActive = false;
|
|
logic(this, 'saveAccountState_end', { reason: reason,
|
|
folderSaveCount: folderSaveCount });
|
|
|
|
// NB: we used to log when the save completed, but it ended up being
|
|
// annoying to the unit tests since we don't block our actions on
|
|
// the completion of the save at this time.
|
|
|
|
var callbacks = this._deferredSaveAccountCalls;
|
|
this._deferredSaveAccountCalls = [];
|
|
callbacks.forEach(function(callback) {
|
|
callback();
|
|
});
|
|
}.bind(this),
|
|
reuseTrans);
|
|
// Reduce the length of time perFolderStuff and its contents are kept alive.
|
|
perFolderStuff = null;
|
|
this._deadFolderIds = null;
|
|
return trans;
|
|
};
|
|
|
|
exports.runAfterSaves = function(callback) {
|
|
if (this._saveAccountStateActive || this._saveAccountIsImminent) {
|
|
this._deferredSaveAccountCalls.push(callback);
|
|
} else {
|
|
callback();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* This function goes through each folder storage object in
|
|
* an account and performs the necessary upgrade steps if
|
|
* there is a new version. See upgradeIfNeeded in mailslice.js.
|
|
* Note: This function schedules a job for each folderStorage
|
|
* object in the account.
|
|
*/
|
|
exports.upgradeFolderStoragesIfNeeded = function() {
|
|
for (var key in this._folderStorages) {
|
|
var storage = this._folderStorages[key];
|
|
storage.upgradeIfNeeded();
|
|
}
|
|
}
|
|
|
|
}); // end define
|