mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-02-03 05:51:07 +08:00
fix(body-processor): Observe the db to clear cache. Fixes #1133
Summary: Test Plan: Reviewers: Subscribers:
This commit is contained in:
parent
d36470d47f
commit
df9e3b7464
3 changed files with 155 additions and 109 deletions
|
@ -22,20 +22,12 @@ class MessageItemBody extends React.Component
|
||||||
processedBody: undefined
|
processedBody: undefined
|
||||||
|
|
||||||
componentWillMount: =>
|
componentWillMount: =>
|
||||||
@_prepareBody(@props)
|
@_unsub = MessageBodyProcessor.processAndSubscribe(@props.message, @_onBodyChanged)
|
||||||
|
|
||||||
shouldComponentUpdate: (nextProps, nextState) ->
|
componentWillReceiveProps: (nextProps) ->
|
||||||
not Utils.isEqualReact(nextProps, @props) or
|
if nextProps.message.id isnt @props.message.id
|
||||||
not Utils.isEqualReact(nextState, @state)
|
@_unsub?()
|
||||||
|
@_unsub = MessageBodyProcessor.processAndSubscribe(nextProps.message, @_onBodyChanged)
|
||||||
componentWillUpdate: (nextProps, nextState) =>
|
|
||||||
if not Utils.isEqualReact(nextProps.message, @props.message)
|
|
||||||
@_prepareBody(nextProps)
|
|
||||||
|
|
||||||
_prepareBody: (props) ->
|
|
||||||
MessageBodyProcessor.resetCache(props.message)
|
|
||||||
@_unsub?()
|
|
||||||
@_unsub = MessageBodyProcessor.processAndSubscribe(props.message, @_onBodyChanged)
|
|
||||||
|
|
||||||
componentWillUnmount: =>
|
componentWillUnmount: =>
|
||||||
@_unsub?()
|
@_unsub?()
|
||||||
|
|
|
@ -1,96 +0,0 @@
|
||||||
_ = require "underscore"
|
|
||||||
crypto = require "crypto"
|
|
||||||
MessageUtils = require '../models/message-utils'
|
|
||||||
MessageStore = require './message-store'
|
|
||||||
|
|
||||||
MessageBodyWidth = 740
|
|
||||||
|
|
||||||
class MessageBodyProcessor
|
|
||||||
|
|
||||||
constructor: ->
|
|
||||||
@_subscriptions = []
|
|
||||||
@resetCache()
|
|
||||||
|
|
||||||
resetCache: (msg) ->
|
|
||||||
if msg
|
|
||||||
key = @_key(msg)
|
|
||||||
delete @_recentlyProcessedD[key]
|
|
||||||
else
|
|
||||||
# Store an object for recently processed items. Put the item reference into
|
|
||||||
# both data structures so we can access it in O(1) and also delete in O(1)
|
|
||||||
@_recentlyProcessedA = []
|
|
||||||
@_recentlyProcessedD = {}
|
|
||||||
for {message, callback} in @_subscriptions
|
|
||||||
callback(@process(message))
|
|
||||||
|
|
||||||
# It's far safer to key off the hash of the body then the [id, version]
|
|
||||||
# pair. This is because it's theoretically possible for the body to
|
|
||||||
# change without the version updating. When drafts sent N1 used to
|
|
||||||
# optimistically display the message before the latest changes
|
|
||||||
# persisted.
|
|
||||||
_key: (message) ->
|
|
||||||
return message.id + crypto.createHash('md5').update(message.body ? "").digest('hex')
|
|
||||||
|
|
||||||
version: ->
|
|
||||||
@_version
|
|
||||||
|
|
||||||
processAndSubscribe: (message, callback) =>
|
|
||||||
_.defer => callback(@process(message))
|
|
||||||
sub = {message, callback}
|
|
||||||
@_subscriptions.push(sub)
|
|
||||||
return =>
|
|
||||||
@_subscriptions.splice(@_subscriptions.indexOf(sub), 1)
|
|
||||||
|
|
||||||
process: (message) =>
|
|
||||||
body = message.body
|
|
||||||
return "" unless _.isString message.body
|
|
||||||
|
|
||||||
key = @_key(message)
|
|
||||||
if @_recentlyProcessedD[key]
|
|
||||||
return @_recentlyProcessedD[key].body
|
|
||||||
|
|
||||||
# Give each extension the message object to process the body, but don't
|
|
||||||
# allow them to modify anything but the body for the time being.
|
|
||||||
for extension in MessageStore.extensions()
|
|
||||||
continue unless extension.formatMessageBody
|
|
||||||
latestBody = body
|
|
||||||
try
|
|
||||||
virtual = message.clone()
|
|
||||||
virtual.body = body
|
|
||||||
extension.formatMessageBody({message: virtual})
|
|
||||||
body = virtual.body
|
|
||||||
catch err
|
|
||||||
NylasEnv.reportError(err)
|
|
||||||
body = latestBody
|
|
||||||
|
|
||||||
# Find inline images and give them a calculated CSS height based on
|
|
||||||
# html width and height, when available. This means nothing changes size
|
|
||||||
# as the image is loaded, and we can estimate final height correctly.
|
|
||||||
# Note that MessageBodyWidth must be updated if the UI is changed!
|
|
||||||
|
|
||||||
while (result = MessageUtils.cidRegex.exec(body)) isnt null
|
|
||||||
imgstart = body.lastIndexOf('<', result.index)
|
|
||||||
imgend = body.indexOf('/>', result.index)
|
|
||||||
|
|
||||||
if imgstart != -1 and imgend > imgstart
|
|
||||||
imgtag = body.substr(imgstart, imgend - imgstart)
|
|
||||||
width = imgtag.match(/width[ ]?=[ ]?['"]?(\d*)['"]?/)?[1]
|
|
||||||
height = imgtag.match(/height[ ]?=[ ]?['"]?(\d*)['"]?/)?[1]
|
|
||||||
if width and height
|
|
||||||
scale = Math.min(1, MessageBodyWidth / width)
|
|
||||||
style = " style=\"height:#{height * scale}px;\" "
|
|
||||||
body = body.substr(0, imgend) + style + body.substr(imgend)
|
|
||||||
|
|
||||||
@addToCache(key, body)
|
|
||||||
body
|
|
||||||
|
|
||||||
addToCache: (key, body) ->
|
|
||||||
if @_recentlyProcessedA.length > 50
|
|
||||||
removed = @_recentlyProcessedA.pop()
|
|
||||||
delete @_recentlyProcessedD[removed.key]
|
|
||||||
item = {key, body}
|
|
||||||
@_recentlyProcessedA.unshift(item)
|
|
||||||
@_recentlyProcessedD[key] = item
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = new MessageBodyProcessor()
|
|
150
src/flux/stores/message-body-processor.es6
Normal file
150
src/flux/stores/message-body-processor.es6
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
import _ from 'underscore';
|
||||||
|
import Message from '../models/message';
|
||||||
|
import MessageUtils from '../models/message-utils';
|
||||||
|
import MessageStore from './message-store';
|
||||||
|
import DatabaseStore from './database-store';
|
||||||
|
|
||||||
|
const MessageBodyWidth = 740;
|
||||||
|
|
||||||
|
class MessageBodyProcessor {
|
||||||
|
constructor() {
|
||||||
|
this._subscriptions = [];
|
||||||
|
this.resetCache();
|
||||||
|
|
||||||
|
DatabaseStore.listen((change) => {
|
||||||
|
if (change.objectClass === Message.name) {
|
||||||
|
change.objects.forEach(this.updateCacheForMessage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resetCache() {
|
||||||
|
// Store an object for recently processed items. Put the item reference into
|
||||||
|
// both data structures so we can access it in O(1) and also delete in O(1)
|
||||||
|
this._recentlyProcessedA = [];
|
||||||
|
this._recentlyProcessedD = {};
|
||||||
|
for (const {message, callback} of this._subscriptions) {
|
||||||
|
callback(this.process(message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCacheForMessage = (changedMessage) => {
|
||||||
|
// check that the message exists in the cache
|
||||||
|
const changedKey = this._key(changedMessage);
|
||||||
|
if (!this._recentlyProcessedD[changedKey]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove the message from the cache
|
||||||
|
delete this._recentlyProcessedD[changedKey];
|
||||||
|
this._recentlyProcessedA = this._recentlyProcessedA.filter(({key}) =>
|
||||||
|
key !== changedKey
|
||||||
|
);
|
||||||
|
|
||||||
|
// reprocess any subscription using the new message data. Note that
|
||||||
|
// changedMessage may not have a loaded body if it wasn't changed. In
|
||||||
|
// that case, we use the previous body.
|
||||||
|
const subscriptions = this._subscriptions.filter(({message}) =>
|
||||||
|
message.id === changedMessage.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (subscriptions.length > 0) {
|
||||||
|
const updatedMessage = changedMessage.clone();
|
||||||
|
updatedMessage.body = updatedMessage.body || subscriptions[0].message.body;
|
||||||
|
const output = this.process(updatedMessage);
|
||||||
|
for (const subscription of subscriptions) {
|
||||||
|
subscription.callback(output);
|
||||||
|
subscription.message = updatedMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_key(message) {
|
||||||
|
// It's safe to key off of the message ID alone because we invalidate the
|
||||||
|
// cache whenever the message is persisted to the database.
|
||||||
|
return message.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
version() {
|
||||||
|
return this._version;
|
||||||
|
}
|
||||||
|
|
||||||
|
processAndSubscribe(message, callback) {
|
||||||
|
_.defer(() => callback(this.process(message)));
|
||||||
|
const sub = {message, callback}
|
||||||
|
this._subscriptions.push(sub);
|
||||||
|
return () => {
|
||||||
|
this._subscriptions.splice(this._subscriptions.indexOf(sub), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
process(message) {
|
||||||
|
let body = message.body;
|
||||||
|
if (!_.isString(message.body)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = this._key(message);
|
||||||
|
if (this._recentlyProcessedD[key]) {
|
||||||
|
return this._recentlyProcessedD[key].body;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give each extension the message object to process the body, but don't
|
||||||
|
// allow them to modify anything but the body for the time being.
|
||||||
|
for (const extension of MessageStore.extensions()) {
|
||||||
|
if (!extension.formatMessageBody) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const previousBody = body;
|
||||||
|
try {
|
||||||
|
const virtual = message.clone();
|
||||||
|
virtual.body = body;
|
||||||
|
extension.formatMessageBody({message: virtual});
|
||||||
|
body = virtual.body;
|
||||||
|
} catch (err) {
|
||||||
|
NylasEnv.reportError(err);
|
||||||
|
body = previousBody;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find inline images and give them a calculated CSS height based on
|
||||||
|
// html width and height, when available. This means nothing changes size
|
||||||
|
// as the image is loaded, and we can estimate final height correctly.
|
||||||
|
// Note that MessageBodyWidth must be updated if the UI is changed!
|
||||||
|
let result = MessageUtils.cidRegex.exec(body);
|
||||||
|
|
||||||
|
while (result !== null) {
|
||||||
|
const imgstart = body.lastIndexOf('<', result.index);
|
||||||
|
const imgend = body.indexOf('/>', result.index);
|
||||||
|
|
||||||
|
if ((imgstart !== -1) && (imgend > imgstart)) {
|
||||||
|
const imgtag = body.substr(imgstart, imgend - imgstart);
|
||||||
|
const widthMatch = imgtag.match(/width[ ]?=[ ]?['"]?(\d*)['"]?/);
|
||||||
|
const width = widthMatch ? widthMatch[1] : null;
|
||||||
|
const heightMatch = imgtag.match(/height[ ]?=[ ]?['"]?(\d*)['"]?/);
|
||||||
|
const height = heightMatch ? heightMatch[1] : null;
|
||||||
|
if (width && height) {
|
||||||
|
const scale = Math.min(1, MessageBodyWidth / width);
|
||||||
|
const style = ` style="height:${height * scale}px;" `
|
||||||
|
body = body.substr(0, imgend) + style + body.substr(imgend);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = MessageUtils.cidRegex.exec(body);
|
||||||
|
}
|
||||||
|
this.addToCache(key, body);
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
addToCache(key, body) {
|
||||||
|
if (this._recentlyProcessedA.length > 50) {
|
||||||
|
const removed = this._recentlyProcessedA.pop()
|
||||||
|
delete this._recentlyProcessedD[removed.key]
|
||||||
|
}
|
||||||
|
const item = {key, body};
|
||||||
|
this._recentlyProcessedA.unshift(item);
|
||||||
|
this._recentlyProcessedD[key] = item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new MessageBodyProcessor();
|
Loading…
Reference in a new issue