Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure that all imported notes are sent to the server #986

Merged
merged 10 commits into from
Nov 6, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion lib/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import NavigationBar from './navigation-bar';
import AppLayout from './app-layout';
import Auth from './auth';
import DialogRenderer from './dialog-renderer';
import { activityHooks, nudgeUnsynced } from './utils/sync';
import analytics from './analytics';
import classNames from 'classnames';
import {
Expand Down Expand Up @@ -162,7 +163,9 @@ export const App = connect(mapStateToProps, mapDispatchToProps)(

this.props.client
.on('authorized', this.onAuthChanged)
.on('unauthorized', this.onAuthChanged);
.on('unauthorized', this.onAuthChanged)
.on('message', this.syncActivityHooks)
.on('send', this.syncActivityHooks);

this.onNotesIndex();
this.onTagsIndex();
Expand Down Expand Up @@ -347,6 +350,18 @@ export const App = connect(mapStateToProps, mapDispatchToProps)(
return Math.max(filteredNotes.findIndex(noteIndex) - 1, 0);
};

syncActivityHooks = data => {
activityHooks(data, {
onIdle: () => {
nudgeUnsynced({
client: this.props.client,
noteBucket: this.props.noteBucket,
notes: this.props.appState.notes,
});
},
});
};

toggleShortcuts = doEnable => {
if (doEnable) {
window.addEventListener('keydown', this.handleShortcut, true);
Expand Down
10 changes: 6 additions & 4 deletions lib/boot.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import App from './app';
import Modal from 'react-modal';
import Debug from 'debug';
import getConfig from '../get-config';
import simperium from './simperium';
import store from './state';
Expand Down Expand Up @@ -77,17 +78,18 @@ const client = simperium({
});

const l = msg => {
const debug = Debug('client');

return function() {
// if (window.loggingEnabled)
console.log.apply(console, [msg].concat([].slice.call(arguments))); // eslint-disable-line no-console
debug.apply(debug, [msg].concat([].slice.call(arguments)));
};
};

client
.on('connect', l('Connected'))
.on('disconnect', l('Not connected'))
// .on( 'message', l('<=') )
// .on( 'send', l('=>') )
.on('message', l('<='))
.on('send', l('=>'))
.on('unauthorized', l('Not authorized'));

client.on('unauthorized', () => {
Expand Down
4 changes: 4 additions & 0 deletions lib/flux/app-state.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { get, partition, some } from 'lodash';
import update from 'react-addons-update';
import Debug from 'debug';
import ActionMap from './action-map';
import isEmailTag from '../utils/is-email-tag';
import filterNotes from '../utils/filter-notes';
import throttle from '../utils/throttle';
import analytics from '../analytics';
import { util as simperiumUtil } from 'simperium';

const debug = Debug('appState');
const typingThrottle = 3000;

export const actionMap = new ActionMap({
Expand Down Expand Up @@ -318,6 +320,7 @@ export const actionMap = new ActionMap({
const settings = getState().settings;
const { sortType, sortReversed } = settings;
var sortOrder;
debug('loadNotes');

if (sortType === 'alphabetical') {
sortOrder = sortReversed ? 'prev' : 'next';
Expand All @@ -337,6 +340,7 @@ export const actionMap = new ActionMap({
notes.push(cursor.value);
cursor.continue();
} else {
debug(`noteCount: ${notes.length}`);
dispatch(this.action('notesLoaded', { notes: notes }));
}
};
Expand Down
6 changes: 3 additions & 3 deletions lib/utils/import/simplenote/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ class SimplenoteImporter extends EventEmitter {
return;
}

// Limit file size we will read to 1mb
if (file.size > 1000000) {
this.emit('status', 'error', 'File should be less than 1 MB.');
// Limit file size we will read to 5mb
if (file.size > 5000000) {
this.emit('status', 'error', 'File should be less than 5 MB.');
return;
}

Expand Down
49 changes: 49 additions & 0 deletions lib/utils/sync/activity-hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { debounce, isFunction, noop, startsWith } from 'lodash';
import Debug from 'debug';

const debug = Debug('sync:activityHooks');

export const debounceWait = 500;

const onSyncActive = debounce(
(data, onActive) => {
debug('Sync active...');

if (isFunction(onActive)) {
onActive();
}
},
debounceWait,
{
leading: true,
trailing: false,
}
);

const onSyncIdle = debounce((data, onIdle) => {
debug('Sync idle');

if (isFunction(onIdle)) {
onIdle();
}
}, debounceWait);

/**
* Hook functions to sync active/idle events
*
* @param {string} data - The data emitted by the simperium client's 'message' or 'send' event.
* @param {Object} hooks - The functions to hook.
* @param {function} onActive - The function to call when syncing becomes active.
* @param {function} onIdle - The function to call when syncing becomes idle.
*/
const activityHooks = (data, { onActive = noop, onIdle = noop }) => {
// Ignore if heartbeat
if (startsWith(data, 'h:')) {
return;
}

onSyncActive(data, onActive);
onSyncIdle(data, onIdle);
};

export default activityHooks;
39 changes: 39 additions & 0 deletions lib/utils/sync/activity-hooks.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import activityHooks, { debounceWait } from './activity-hooks';
import { times } from 'lodash';

describe('sync:activityHooks', () => {
let order, hooks;

beforeEach(() => {
order = [];
hooks = {
onActive: jest.fn(() => order.push('active')),
onIdle: jest.fn(() => order.push('idle')),
};
});

it('should call the appropriate hook when syncing becomes active/idle', done => {
const myActivityHooks = activityHooks('0:mockdata', hooks);

times(3, myActivityHooks);

window.setTimeout(() => {
expect(hooks.onActive).toHaveBeenCalledTimes(1);
expect(hooks.onIdle).toHaveBeenCalledTimes(1);
expect(order).toEqual(['active', 'idle']);
done();
}, debounceWait);
});

it('should ignore heartbeats', done => {
const myActivityHooks = activityHooks('h:1', hooks);

times(3, myActivityHooks);

window.setTimeout(() => {
expect(hooks.onActive).toHaveBeenCalledTimes(0);
expect(hooks.onIdle).toHaveBeenCalledTimes(0);
done();
}, debounceWait);
});
});
4 changes: 4 additions & 0 deletions lib/utils/sync/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import activityHooks from './activity-hooks';
import nudgeUnsynced from './nudge-unsynced';

export { activityHooks, nudgeUnsynced };
43 changes: 43 additions & 0 deletions lib/utils/sync/nudge-unsynced.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* Ensure that all imported notes are sent to the server
*
* Importing over 350-400 notes at once can cause the syncing to get
* stuck. This function checks if there are any local notes which have not
* been acknowledged by the server yet, and if so, reconnects the client
* to resume the syncing.
*/
import Debug from 'debug';

const debug = Debug('sync:nudgeUnsynced');

const nudgeUnsynced = ({ noteBucket, notes, client }) => {
const noteHasSynced = note =>
new Promise(resolve =>
noteBucket.getVersion(note.id, (e, v) => {
const result = {
id: note.id,
data: note.data,
unsynced: e || v === 0,
};
resolve(result);
})
);

return Promise.all(notes.map(noteHasSynced)).then(result => {
let unsyncedNoteCount = 0;
const hasUnsyncedNotes = result.some(note => note.unsynced);

if (hasUnsyncedNotes && debug.enabled) {
unsyncedNoteCount = result.filter(note => note.unsynced).length;
}

debug(`${unsyncedNoteCount} unsynced notes`);

// Re-init the client and prod the syncing to resume
if (hasUnsyncedNotes) {
client.client.connect();
}
});
};

export default nudgeUnsynced;
21 changes: 21 additions & 0 deletions lib/utils/sync/nudge-unsynced.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import nudgeUnsynced from './nudge-unsynced';

describe('sync:nudgeUnsynced', () => {
it('should reconnect the client when an unsynced note exists', () => {
const mockArg = {
noteBucket: {
getVersion: (_, callback) => {
callback(undefined, 0); // no error, v === 0
},
},
notes: [{}],
client: {
client: { connect: jest.fn() },
},
};

nudgeUnsynced(mockArg).then(() => {
expect(mockArg.client.client.connect).toHaveBeenCalled();
});
});
});
Loading