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.
This commit is contained in:
parent
93e3a48004
commit
b38dea0597
25901 changed files with 9982925 additions and 369 deletions
419
gaia/apps/email/js/ext/cronsync.js
Executable file
419
gaia/apps/email/js/ext/cronsync.js
Executable file
|
|
@ -0,0 +1,419 @@
|
|||
/*global define, console */
|
||||
/**
|
||||
* Drives periodic synchronization, covering the scheduling, deciding what
|
||||
* folders to sync, and generating notifications to relay to the UI. More
|
||||
* specifically, we have two goals:
|
||||
*
|
||||
* 1) Generate notifications about new messages.
|
||||
*
|
||||
* 2) Cause the device to synchronize its offline store periodically with the
|
||||
* server for general responsiveness and so the user can use the device
|
||||
* offline.
|
||||
*
|
||||
* We use mozAlarm to schedule ourselves to wake up when our next
|
||||
* synchronization should occur.
|
||||
*
|
||||
* All synchronization occurs in parallel because we want the interval that we
|
||||
* force the device's radio into higher power modes to be as short as possible.
|
||||
*
|
||||
* This logic is part of the back-end, not the front-end. We want to notify
|
||||
* the front-end of new messages, but we want the front-end to be the one that
|
||||
* displays and services them to the user.
|
||||
**/
|
||||
|
||||
define(function(require, exports) {
|
||||
'use strict';
|
||||
|
||||
var logic = require('logic'),
|
||||
router = require('./worker-router'),
|
||||
mailslice = require('./mailslice'),
|
||||
syncbase = require('./syncbase'),
|
||||
allback = require( './allback');
|
||||
|
||||
function debug(str) {
|
||||
console.log('cronsync: ' + str + '\n');
|
||||
}
|
||||
|
||||
var SliceBridgeProxy = require('./slice_bridge_proxy').SliceBridgeProxy;
|
||||
|
||||
/**
|
||||
* Create a specialized sync slice via clobbering that accumulates a list of
|
||||
* new headers and invokes a callback when the sync has fully completed.
|
||||
*
|
||||
* Fully completed includes:
|
||||
* - The account update has been fully saved to disk.
|
||||
*
|
||||
* New header semantics are:
|
||||
* - Header is as new or newer than the newest header we previously knew about.
|
||||
* Specifically we're using SINCE which is >=, so a message that arrived at
|
||||
* the same second as the other message still counts as new. Because the new
|
||||
* message will inherently have a higher id than the other message, this
|
||||
* meets with our other ordering semantics, although I'm thinking it wasn't
|
||||
* totally intentional.
|
||||
* - Header is unread. (AKA Not \Seen)
|
||||
*
|
||||
* "Clobbering" in this case means this is a little hacky. What we do is:
|
||||
* - Take a normal slice and hook it up to a normal SliceBridgeProxy, but
|
||||
* give the proxy a fake bridge that never sends any data anywhere. This
|
||||
* is reasonably future-proof/safe.
|
||||
*
|
||||
* - Put an onNewHeader method on the slice to accumulate the new headers.
|
||||
* The new headers are all we care about. The rest of the headers loaded/etc.
|
||||
* are boring to us and do not matter. However, there's relatively little
|
||||
* memory or CPU overhead to letting that stuff get populated/retained since
|
||||
* it's in memory already anyways and at worst we're only delaying the GC by
|
||||
* a little bit. (This is not to say there aren't pathological situations
|
||||
* possible, but they'd be largely the same if the user triggered the sync.
|
||||
* The main difference cronsync currently will definitely not shrink the
|
||||
* slice.
|
||||
*
|
||||
* - Clobber proy.sendStatus to know when the sync has completed via the
|
||||
* same signal the front-end uses to know when the sync is over.
|
||||
*
|
||||
* You as the caller need to:
|
||||
* - Make sure to kill the slice in a timely fashion after we invoke the
|
||||
* callback. Since killing the slice can result in the connection immediately
|
||||
* being closed, you want to make sure that if you're doing anything like
|
||||
* scheduling snippet downloads that you do that first.
|
||||
*/
|
||||
function makeHackedUpSlice(storage, callback) {
|
||||
var fakeBridgeThatEatsStuff = {
|
||||
__sendMessage: function() {}
|
||||
},
|
||||
proxy = new SliceBridgeProxy(fakeBridgeThatEatsStuff, 'cron'),
|
||||
slice = new mailslice.MailSlice(proxy, storage),
|
||||
oldStatusMethod = proxy.sendStatus,
|
||||
newHeaders = [];
|
||||
|
||||
slice.onNewHeader = function(header) {
|
||||
console.log('onNewHeader: ' + header);
|
||||
newHeaders.push(header);
|
||||
};
|
||||
|
||||
proxy.sendStatus = function(status, requested, moreExpected,
|
||||
progress, newEmailCount) {
|
||||
// (maintain normal behaviour)
|
||||
oldStatusMethod.apply(this, arguments);
|
||||
|
||||
// We do not want to declare victory until the sync process has fully
|
||||
// completed which (significantly!) includes waiting for the save to have
|
||||
// completed.
|
||||
// (Only fire completion once.)
|
||||
if (callback) {
|
||||
switch (status) {
|
||||
// normal success and failure
|
||||
case 'synced':
|
||||
case 'syncfailed':
|
||||
// ActiveSync specific edge-case where syncFolderList has not yet
|
||||
// completed. If the slice is still alive when syncFolderList completes
|
||||
// the slice will auto-refresh itself. We don't want or need this,
|
||||
// which is fine since we kill the slice in the callback.
|
||||
case 'syncblocked':
|
||||
try {
|
||||
callback(newHeaders);
|
||||
}
|
||||
catch (ex) {
|
||||
console.error('cronsync callback error:', ex, '\n', ex.stack);
|
||||
callback = null;
|
||||
throw ex;
|
||||
}
|
||||
callback = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return slice;
|
||||
}
|
||||
|
||||
/**
|
||||
* The brains behind periodic account synchronization; only created by the
|
||||
* universe once it has loaded its configuration and accounts.
|
||||
*/
|
||||
function CronSync(universe) {
|
||||
this._universe = universe;
|
||||
|
||||
logic.defineScope(this, 'CronSync');
|
||||
|
||||
this._ensureSyncResolve = null;
|
||||
|
||||
this.sendCronSync = router.registerSimple('cronsync', (data) => {
|
||||
var args = data.args;
|
||||
switch (data.cmd) {
|
||||
case 'alarm':
|
||||
debug('received an alarm via a message handler');
|
||||
this.onAlarm.apply(this, args);
|
||||
break;
|
||||
case 'syncEnsured':
|
||||
debug('received an syncEnsured via a message handler');
|
||||
this.onSyncEnsured.apply(this, args);
|
||||
break;
|
||||
}
|
||||
});
|
||||
this.sendCronSync('hello');
|
||||
|
||||
this.ensureSync();
|
||||
}
|
||||
|
||||
exports.CronSync = CronSync;
|
||||
CronSync.prototype = {
|
||||
/**
|
||||
* Makes sure there is a sync timer set up for all accounts.
|
||||
*/
|
||||
ensureSync: function() {
|
||||
// Only execute ensureSync if it is not already in progress. Otherwise, due
|
||||
// to async timing of mozAlarm setting, could end up with two sync tasks for
|
||||
// the same ID.
|
||||
if (this._ensureSyncResolve) {
|
||||
return;
|
||||
}
|
||||
|
||||
logic(this, 'ensureSync_begin');
|
||||
|
||||
this._ensureSyncPromise = new Promise((resolve) => {
|
||||
// No error pathway for the bridge hop, so just tracking resolve.
|
||||
this._ensureSyncResolve = resolve;
|
||||
});
|
||||
|
||||
debug('ensureSync called');
|
||||
|
||||
var accounts = this._universe.accounts,
|
||||
syncData = {};
|
||||
|
||||
accounts.forEach(function(account) {
|
||||
// Store data by interval, use a more obvious string key instead of just
|
||||
// stringifying a number, which could be confused with an array construct.
|
||||
var interval = account.accountDef.syncInterval,
|
||||
intervalKey = 'interval' + interval;
|
||||
|
||||
if (!syncData.hasOwnProperty(intervalKey)) {
|
||||
syncData[intervalKey] = [];
|
||||
}
|
||||
syncData[intervalKey].push(account.id);
|
||||
});
|
||||
|
||||
this.sendCronSync('ensureSync', [syncData]);
|
||||
},
|
||||
|
||||
/**
|
||||
* Called from cronsync-main once ensureSync as set any alarms needed. Need to
|
||||
* wait for it before signaling sync is done because otherwise the app could
|
||||
* get closed down before the alarm additions succeed.
|
||||
*/
|
||||
onSyncEnsured: function() {
|
||||
this._ensureSyncResolve();
|
||||
this._ensureSyncResolve = null;
|
||||
logic(this, 'ensureSync_end');
|
||||
},
|
||||
|
||||
/**
|
||||
* Synchronize the given account. This fetches new messages for the inbox, and
|
||||
* attempts to send pending outbox messages (if applicable). The callback
|
||||
* occurs after both of those operations have completed.
|
||||
*/
|
||||
syncAccount: function(account) {
|
||||
return new Promise((resolve) => {
|
||||
var scope = logic.subscope(this, { accountId: account.id });
|
||||
|
||||
// - Skip syncing if we are offline or the account is disabled
|
||||
if (!this._universe.online || !account.enabled) {
|
||||
debug('syncAccount early exit: online: ' +
|
||||
this._universe.online + ', enabled: ' + account.enabled);
|
||||
logic(scope, 'syncSkipped');
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
var latch = allback.latch();
|
||||
var inboxDone = latch.defer('inbox');
|
||||
|
||||
var inboxFolder = account.getFirstFolderWithType('inbox');
|
||||
var storage = account.getFolderStorageForFolderId(inboxFolder.id);
|
||||
|
||||
// XXX check when the folder was most recently synchronized and skip this
|
||||
// sync if it is sufficiently recent.
|
||||
|
||||
// - Initiate a sync of the folder covering the desired time range.
|
||||
logic(scope, 'syncAccount_begin');
|
||||
logic(scope, 'syncAccountHeaders_begin');
|
||||
|
||||
var slice = makeHackedUpSlice(storage, (newHeaders) => {
|
||||
logic(scope, 'syncAccountHeaders_end', { headers: newHeaders });
|
||||
|
||||
// Reduce headers to the minimum number and data set needed for
|
||||
// notifications.
|
||||
var notifyHeaders = [];
|
||||
newHeaders.some(function(header, i) {
|
||||
notifyHeaders.push({
|
||||
date: header.date,
|
||||
from: header.author.name || header.author.address,
|
||||
subject: header.subject,
|
||||
accountId: account.id,
|
||||
messageSuid: header.suid
|
||||
});
|
||||
|
||||
if (i === syncbase.CRONSYNC_MAX_MESSAGES_TO_REPORT_PER_ACCOUNT - 1) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if (newHeaders.length) {
|
||||
debug('Asking for snippets for ' + notifyHeaders.length + ' headers');
|
||||
// POP3 downloads snippets as part of the sync process, there is no
|
||||
// need to call downloadBodies.
|
||||
if (account.accountDef.type === 'pop3+smtp') {
|
||||
logic(scope, 'syncAccount_end');
|
||||
inboxDone([newHeaders.length, notifyHeaders]);
|
||||
} else if (this._universe.online) {
|
||||
logic(scope, 'syncAccountSnippets_begin');
|
||||
this._universe.downloadBodies(
|
||||
newHeaders.slice(
|
||||
0, syncbase.CRONSYNC_MAX_SNIPPETS_TO_FETCH_PER_ACCOUNT),
|
||||
{
|
||||
maximumBytesToFetch: syncbase.MAX_SNIPPET_BYTES
|
||||
},
|
||||
() => {
|
||||
debug('Notifying for ' + newHeaders.length + ' headers');
|
||||
logic(scope, 'syncAccountSnippets_end');
|
||||
logic(scope, 'syncAccount_end');
|
||||
inboxDone([newHeaders.length, notifyHeaders]);
|
||||
});
|
||||
} else {
|
||||
logic(scope, 'syncAccount_end');
|
||||
debug('UNIVERSE OFFLINE. Notifying for ' + newHeaders.length +
|
||||
' headers');
|
||||
inboxDone([newHeaders.length, notifyHeaders]);
|
||||
}
|
||||
} else {
|
||||
logic(scope, 'syncAccount_end');
|
||||
inboxDone();
|
||||
}
|
||||
|
||||
// Kill the slice. This will release the connection and result in its
|
||||
// death if we didn't schedule snippet downloads above.
|
||||
slice.die();
|
||||
});
|
||||
|
||||
// Pass true to force contacting the server.
|
||||
storage.sliceOpenMostRecent(slice, true);
|
||||
|
||||
// Check the outbox; if it has pending messages, attempt to send them.
|
||||
var outboxFolder = account.getFirstFolderWithType('outbox');
|
||||
if (outboxFolder) {
|
||||
var outboxStorage = account
|
||||
.getFolderStorageForFolderId(outboxFolder.id);
|
||||
if (outboxStorage.getKnownMessageCount() > 0) {
|
||||
var outboxDone = latch.defer('outbox');
|
||||
logic(scope, 'sendOutbox_begin');
|
||||
this._universe.sendOutboxMessages(
|
||||
account,
|
||||
{
|
||||
reason: 'syncAccount'
|
||||
},
|
||||
() => {
|
||||
logic(scope, 'sendOutbox_end');
|
||||
outboxDone();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// After both inbox and outbox syncing are algorithmically done, wait for
|
||||
// any ongoing job operations to complete so that the app is not killed in
|
||||
// the middle of a sync.
|
||||
latch.then((latchResults) => {
|
||||
// Right now, we ignore the outbox sync's results; we only care about
|
||||
// the inbox.
|
||||
var inboxResult = latchResults.inbox[0];
|
||||
this._universe.waitForAccountOps(account, function() {
|
||||
// Also wait for any account save to finish. Most likely failure will
|
||||
// be new message headers not getting saved if the callback is not
|
||||
// fired until after account saves.
|
||||
account.runAfterSaves(function() {
|
||||
resolve(inboxResult);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
onAlarm: function(accountIds) {
|
||||
logic(this, 'alarmFired', { accountIds: accountIds });
|
||||
|
||||
if (!accountIds) {
|
||||
return;
|
||||
}
|
||||
|
||||
var accounts = this._universe.accounts,
|
||||
targetAccounts = [],
|
||||
ids = [];
|
||||
|
||||
logic(this, 'cronSync_begin');
|
||||
this._universe.__notifyStartedCronSync(accountIds);
|
||||
|
||||
// Make sure the acount IDs are still valid. This is to protect against an
|
||||
// account deletion that did not clean up any alarms correctly.
|
||||
accountIds.forEach(function(id) {
|
||||
accounts.some(function(account) {
|
||||
if (account.id === id) {
|
||||
targetAccounts.push(account);
|
||||
ids.push(id);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Make sure next alarm is set up. In the case of a cold start background
|
||||
// sync, this is a bit redundant in that the startup of the mailuniverse
|
||||
// would trigger this work. However, if the app is already running, need to
|
||||
// be sure next alarm is set up, so ensure the next sync is set up here. Do
|
||||
// it here instead of after a sync in case an error in sync would prevent
|
||||
// the next sync from getting scheduled.
|
||||
this.ensureSync();
|
||||
|
||||
var syncResults = [];
|
||||
var accountsResults = {
|
||||
accountIds: accountIds
|
||||
};
|
||||
|
||||
var done = () => {
|
||||
// Make sure the ensure work is done before wrapping up.
|
||||
this._ensureSyncPromise.then(() => {
|
||||
if (syncResults.length) {
|
||||
accountsResults.updates = syncResults;
|
||||
}
|
||||
|
||||
this._universe.__notifyStoppedCronSync(accountsResults);
|
||||
logic(this, 'syncAccounts_end', { accountsResults: accountsResults });
|
||||
logic(this, 'cronSync_end');
|
||||
});
|
||||
};
|
||||
|
||||
// Nothing new to sync, probably old accounts. Just return and indicate that
|
||||
// syncing is done.
|
||||
if (!ids.length) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
logic(this, 'syncAccounts_begin');
|
||||
Promise.all(targetAccounts.map((account) => {
|
||||
return this.syncAccount(account).then((result) => {
|
||||
if (result) {
|
||||
syncResults.push({
|
||||
id: account.id,
|
||||
address: account.identities[0].address,
|
||||
count: result[0],
|
||||
latestMessageInfos: result[1]
|
||||
});
|
||||
}
|
||||
});
|
||||
}))
|
||||
.then(done);
|
||||
},
|
||||
|
||||
shutdown: function() {
|
||||
router.unregister('cronsync');
|
||||
}
|
||||
};
|
||||
|
||||
}); // end define
|
||||
Loading…
Reference in a new issue