Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
379 lines
13 KiB
JavaScript
Executable file
379 lines
13 KiB
JavaScript
Executable file
/**
|
|
* Draft jobs: save/delete drafts, attach/remove attachments. These gets mixed
|
|
* into the specific JobDriver implementations.
|
|
**/
|
|
|
|
define(function(require, exports) {
|
|
|
|
var mailRep = require('../db/mail_rep');
|
|
var draftRep = require('../drafts/draft_rep');
|
|
var base64 = require('safe-base64');
|
|
var asyncFetchBlobAsUint8Array =
|
|
require('../async_blob_fetcher').asyncFetchBlobAsUint8Array;
|
|
|
|
var draftsMixins = exports.draftsMixins = {};
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// attachBlobToDraft
|
|
|
|
/**
|
|
* How big a chunk of an attachment should we encode in a single read? Because
|
|
* we want our base64-encoded lines to be 76 bytes long (before newlines) and
|
|
* there's a 4/3 expansion factor, we want to read a multiple of 57 bytes.
|
|
*
|
|
* I initially chose the largest value just under 1MiB. This appeared too
|
|
* chunky on the ZTE open, so I'm halving to just under 512KiB. Calculated via
|
|
* Math.floor(512 * 1024 / 57) = 9198. The encoded size of this ends up to be
|
|
* 9198 * 78 which is ~700 KiB. So together that's ~1.2 megs if we don't
|
|
* generate a ton of garbage by creating a lot of intermediary strings.
|
|
*
|
|
* This seems reasonable given goals of not requiring the GC to run after every
|
|
* block and not having us tie up the CPU too long during our encoding.
|
|
*/
|
|
draftsMixins.BLOB_BASE64_BATCH_CONVERT_SIZE = 9198 * 57;
|
|
|
|
/**
|
|
* Incrementally convert an attachment into its base64 encoded attachment form
|
|
* which we save in chunks to IndexedDB to avoid using too much memory now or
|
|
* during the sending process.
|
|
*
|
|
* - Retrieve the body the draft is persisted to,
|
|
* - Repeat until the attachment is fully attached:
|
|
* - take a chunk of the source attachment
|
|
* - base64 encode it into a Blob by creating a Uint8Array and manually
|
|
* encoding into that. (We need to put a \r\n after every 76 bytes, and
|
|
* doing that using window.btoa is going to create a lot of garbage. And
|
|
* addressing that is no longer premature optimization.)
|
|
* - update the body with that Blob
|
|
* - trigger a save of the account so that IndexedDB writes the account to
|
|
* disk.
|
|
* - force the body block to be discarded from the cache and then re-get the
|
|
* body. We won't be saving any memory until the Blob has been written to
|
|
* disk and we have forgotten all references to the in-memory Blob we wrote
|
|
* to the database. (The Blob does not magically get turned into a
|
|
* reference to the database.)
|
|
* - Be done. Note that we leave the "small" Blobs independent; we do not
|
|
* create a super Blob.
|
|
*
|
|
* ## Logging ##
|
|
*
|
|
* We log at:
|
|
* - The start of the process.
|
|
* - For each block.
|
|
* - The end of the process.
|
|
*/
|
|
draftsMixins.local_do_attachBlobToDraft = function(op, callback) {
|
|
var localDraftsFolder = this.account.getFirstFolderWithType('localdrafts');
|
|
if (!localDraftsFolder) {
|
|
callback('moot');
|
|
return;
|
|
}
|
|
var self = this;
|
|
this._accessFolderForMutation(
|
|
localDraftsFolder.id, /* needConn*/ false,
|
|
function(nullFolderConn, folderStorage) {
|
|
var wholeBlob = op.attachmentDef.blob;
|
|
|
|
// - Retrieve the message
|
|
var header, body;
|
|
console.log('attachBlobToDraft: retrieving message');
|
|
folderStorage.getMessage(
|
|
op.existingNamer.suid, op.existingNamer.date, {}, gotMessage);
|
|
function gotMessage(records) {
|
|
header = records.header;
|
|
body = records.body;
|
|
|
|
if (!header || !body) {
|
|
// No header/body suggests either some major invariant is busted or
|
|
// one or more UIs issued attach commands after the draft was mooted.
|
|
callback('failure-give-up');
|
|
return;
|
|
}
|
|
|
|
body.attaching = mailRep.makeAttachmentPart({
|
|
name: op.attachmentDef.name,
|
|
type: wholeBlob.type,
|
|
sizeEstimate: wholeBlob.size,
|
|
// this is where we put the Blob segments...
|
|
file: [],
|
|
});
|
|
|
|
convertNextChunk(body);
|
|
}
|
|
|
|
var blobOffset = 0;
|
|
function convertNextChunk(refreshedBody) {
|
|
body = refreshedBody;
|
|
var nextOffset =
|
|
Math.min(wholeBlob.size,
|
|
blobOffset + self.BLOB_BASE64_BATCH_CONVERT_SIZE);
|
|
console.log('attachBlobToDraft: fetching', blobOffset, 'to',
|
|
nextOffset, 'of', wholeBlob.size);
|
|
var slicedBlob = wholeBlob.slice(blobOffset, nextOffset);
|
|
blobOffset = nextOffset;
|
|
|
|
asyncFetchBlobAsUint8Array(slicedBlob, gotChunk);
|
|
}
|
|
|
|
function gotChunk(err, binaryDataU8) {
|
|
console.log('attachBlobToDraft: fetched');
|
|
// The Blob really should not be disappear out from under us, but it
|
|
// could happen.
|
|
if (err) {
|
|
callback('failure-give-up');
|
|
return;
|
|
}
|
|
|
|
var lastChunk = (blobOffset >= wholeBlob.size);
|
|
var encodedU8 = base64.mimeStyleBase64Encode(binaryDataU8);
|
|
body.attaching.file.push(new Blob([encodedU8],
|
|
{ type: wholeBlob.type }));
|
|
|
|
var eventDetails;
|
|
if (lastChunk) {
|
|
var attachmentIndex = body.attachments.length;
|
|
body.attachments.push(body.attaching);
|
|
delete body.attaching; // bad news for shapes, but drafts are rare.
|
|
|
|
eventDetails = {
|
|
changeDetails: {
|
|
attachments: [attachmentIndex]
|
|
}
|
|
};
|
|
}
|
|
else {
|
|
// Do not generate an event for intermediary states; there is nothing
|
|
// to observe.
|
|
eventDetails = null;
|
|
}
|
|
|
|
console.log('attachBlobToDraft: flushing');
|
|
folderStorage.updateMessageBody(
|
|
header, body, { flushBecause: 'blobs' }, eventDetails,
|
|
lastChunk ? bodyUpdatedAllDone : convertNextChunk);
|
|
body = null;
|
|
}
|
|
|
|
function bodyUpdatedAllDone(newBodyInfo) {
|
|
console.log('attachBlobToDraft: blob fully attached');
|
|
callback(null);
|
|
}
|
|
},
|
|
/* no conn => no deathback required */ null,
|
|
'attachBlobToDraft');
|
|
};
|
|
draftsMixins.do_attachBlobToDraft = function(op, callback) {
|
|
// there is no server component for this
|
|
callback(null);
|
|
};
|
|
draftsMixins.check_attachBlobToDraft = function(op, callback) {
|
|
callback(null, 'moot');
|
|
};
|
|
draftsMixins.local_undo_attachBlobToDraft = function(op, callback) {
|
|
callback(null);
|
|
};
|
|
draftsMixins.undo_attachBlobToDraft = function(op, callback) {
|
|
callback(null);
|
|
};
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// detachAttachmentFromDraft
|
|
|
|
draftsMixins.local_do_detachAttachmentFromDraft = function(op, callback) {
|
|
var localDraftsFolder = this.account.getFirstFolderWithType('localdrafts');
|
|
if (!localDraftsFolder) {
|
|
callback('moot');
|
|
return;
|
|
}
|
|
var self = this;
|
|
this._accessFolderForMutation(
|
|
localDraftsFolder.id, /* needConn*/ false,
|
|
function(nullFolderConn, folderStorage) {
|
|
// - Retrieve the message
|
|
var header, body;
|
|
console.log('detachAttachmentFromDraft: retrieving message');
|
|
folderStorage.getMessage(
|
|
op.existingNamer.suid, op.existingNamer.date, {}, gotMessage);
|
|
function gotMessage(records) {
|
|
header = records.header;
|
|
body = records.body;
|
|
|
|
if (!header || !body) {
|
|
// No header/body suggests either some major invariant is busted or
|
|
// one or more UIs issued attach commands after the draft was mooted.
|
|
callback('failure-give-up');
|
|
return;
|
|
}
|
|
|
|
// Just forget about the attachment. Splice handles insane indices.
|
|
body.attachments.splice(op.attachmentIndex, 1);
|
|
|
|
console.log('detachAttachmentFromDraft: flushing');
|
|
folderStorage.updateMessageBody(
|
|
header, body,
|
|
{ flushBecause: 'blobs' },
|
|
{
|
|
changeDetails: {
|
|
detachedAttachments: [op.attachmentIndex]
|
|
}
|
|
},
|
|
bodyUpdatedAllDone);
|
|
}
|
|
|
|
function bodyUpdatedAllDone(newBodyInfo) {
|
|
console.log('detachAttachmentFromDraft: blob fully detached');
|
|
callback(null);
|
|
}
|
|
},
|
|
/* no conn => no deathback required */ null,
|
|
'detachAttachmentFromDraft');
|
|
};
|
|
|
|
draftsMixins.do_detachAttachmentFromDraft = function(op, callback) {
|
|
// there is no server component for this at this time.
|
|
callback(null);
|
|
};
|
|
|
|
draftsMixins.check_detachAttachmentFromDraft = function(op, callback) {
|
|
callback(null);
|
|
};
|
|
|
|
draftsMixins.local_undo_detachAttachmentFromDraft = function(op, callback) {
|
|
callback(null);
|
|
};
|
|
|
|
draftsMixins.undo_detachAttachmentFromDraft = function(op, callback) {
|
|
callback(null);
|
|
};
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// saveDraft
|
|
|
|
/**
|
|
* Save a draft; if there already was a draft, it gets replaced. The new
|
|
* draft gets a new date and id/SUID so it is logically distinct. However,
|
|
* we will propagate attachment and on-server information between drafts.
|
|
*/
|
|
draftsMixins.local_do_saveDraft = function(op, callback) {
|
|
var localDraftsFolder = this.account.getFirstFolderWithType('localdrafts');
|
|
if (!localDraftsFolder) {
|
|
callback('moot');
|
|
return;
|
|
}
|
|
var self = this;
|
|
this._accessFolderForMutation(
|
|
localDraftsFolder.id, /* needConn*/ false,
|
|
function(nullFolderConn, folderStorage) {
|
|
// there's always a header add and a body add
|
|
var waitingForDbMods = 2;
|
|
function gotMessage(oldRecords) {
|
|
var newRecords = draftRep.mergeDraftStates(
|
|
oldRecords.header, oldRecords.body,
|
|
op.draftRep,
|
|
op.newDraftInfo,
|
|
self.account.universe);
|
|
|
|
// If there already was a draft saved, delete it.
|
|
// Note that ordering of the removal and the addition doesn't really
|
|
// matter here because of our use of transactions.
|
|
if (op.existingNamer) {
|
|
waitingForDbMods++;
|
|
folderStorage.deleteMessageHeaderAndBody(
|
|
op.existingNamer.suid, op.existingNamer.date, dbModCompleted);
|
|
}
|
|
|
|
folderStorage.addMessageHeader(newRecords.header, newRecords.body,
|
|
dbModCompleted);
|
|
folderStorage.addMessageBody(newRecords.header, newRecords.body,
|
|
dbModCompleted);
|
|
function dbModCompleted() {
|
|
if (--waitingForDbMods === 0) {
|
|
callback(
|
|
/* no error */ null,
|
|
/* result */ newRecords,
|
|
/* save account */ true);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (op.existingNamer) {
|
|
folderStorage.getMessage(
|
|
op.existingNamer.suid, op.existingNamer.date, null, gotMessage);
|
|
}
|
|
else {
|
|
gotMessage({ header: null, body: null });
|
|
}
|
|
},
|
|
/* no conn => no deathback required */ null,
|
|
'saveDraft');
|
|
};
|
|
|
|
/**
|
|
* FUTURE WORK: Save a draft to the server; this is inherently IMAP only.
|
|
* Tracked on: https://bugzilla.mozilla.org/show_bug.cgi?id=799822
|
|
*
|
|
* It is very possible that we will save local drafts faster / more frequently
|
|
* than we can update our server state. It only makes sense to upload the
|
|
* latest draft state to the server. Because we delete our old local drafts,
|
|
* it's obvious when we should skip out on updating the server draft for
|
|
* something.
|
|
*
|
|
* Because IMAP drafts have to replace the prior drafts, we use our old 'srvid'
|
|
* to know what message to delete as well as what message to pull attachments
|
|
* from when we're in a mode where we upload attachments to drafts and CATENATE
|
|
* is available.
|
|
*/
|
|
draftsMixins.do_saveDraft = function(op, callback) {
|
|
callback(null);
|
|
};
|
|
draftsMixins.check_saveDraft = function(op, callback) {
|
|
callback(null, 'moot');
|
|
};
|
|
draftsMixins.local_undo_saveDraft = function(op, callback) {
|
|
callback(null);
|
|
};
|
|
draftsMixins.undo_saveDraft = function(op, callback) {
|
|
callback(null);
|
|
};
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// deleteDraft
|
|
|
|
draftsMixins.local_do_deleteDraft = function(op, callback) {
|
|
var localDraftsFolder = this.account.getFirstFolderWithType('localdrafts');
|
|
if (!localDraftsFolder) {
|
|
callback('moot');
|
|
return;
|
|
}
|
|
var self = this;
|
|
this._accessFolderForMutation(
|
|
localDraftsFolder.id, /* needConn*/ false,
|
|
function(nullFolderConn, folderStorage) {
|
|
folderStorage.deleteMessageHeaderAndBody(
|
|
op.messageNamer.suid, op.messageNamer.date,
|
|
function() {
|
|
callback(null, null, /* save account */ true);
|
|
});
|
|
},
|
|
/* no conn => no deathback required */ null,
|
|
'deleteDraft');
|
|
};
|
|
|
|
draftsMixins.do_deleteDraft = function(op, callback) {
|
|
// there is no server component for this
|
|
callback(null);
|
|
};
|
|
draftsMixins.check_deleteDraft = function(op, callback) {
|
|
callback(null, 'moot');
|
|
};
|
|
draftsMixins.local_undo_deleteDraft = function(op, callback) {
|
|
callback(null);
|
|
};
|
|
draftsMixins.undo_deleteDraft = function(op, callback) {
|
|
callback(null);
|
|
};
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
}); // end define
|