VulpesOS/gaia/apps/email/js/ext/accountmixins.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

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