Skip to content

Commit b508aa9

Browse files
authored
Merge pull request #746 from matrix-org/bwindels/resynconlltoggle
Detect when lazy loading has been toggled in client.startClient
2 parents fcebe89 + 5e76345 commit b508aa9

File tree

11 files changed

+214
-39
lines changed

11 files changed

+214
-39
lines changed

spec/integ/matrix-client-opts.spec.js

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ describe("MatrixClient opts", function() {
9494
httpBackend.flush("/txn1", 1);
9595
});
9696

97-
it("should be able to sync / get new events", function(done) {
97+
it("should be able to sync / get new events", async function() {
9898
const expectedEventTypes = [ // from /initialSync
9999
"m.room.message", "m.room.name", "m.room.member", "m.room.member",
100100
"m.room.create",
@@ -110,20 +110,16 @@ describe("MatrixClient opts", function() {
110110
httpBackend.when("GET", "/pushrules").respond(200, {});
111111
httpBackend.when("POST", "/filter").respond(200, { filter_id: "foo" });
112112
httpBackend.when("GET", "/sync").respond(200, syncData);
113-
client.startClient();
114-
httpBackend.flush("/pushrules", 1).then(function() {
115-
return httpBackend.flush("/filter", 1);
116-
}).then(function() {
117-
return Promise.all([
118-
httpBackend.flush("/sync", 1),
119-
utils.syncPromise(client),
120-
]);
121-
}).done(function() {
122-
expect(expectedEventTypes.length).toEqual(
123-
0, "Expected to see event types: " + expectedEventTypes,
124-
);
125-
done();
126-
});
113+
await client.startClient();
114+
await httpBackend.flush("/pushrules", 1);
115+
await httpBackend.flush("/filter", 1);
116+
await Promise.all([
117+
httpBackend.flush("/sync", 1),
118+
utils.syncPromise(client),
119+
]);
120+
expect(expectedEventTypes.length).toEqual(
121+
0, "Expected to see event types: " + expectedEventTypes,
122+
);
127123
});
128124
});
129125

spec/unit/matrix-client.spec.js

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,9 @@ describe("MatrixClient", function() {
139139
store.getSavedSync = expect.createSpy().andReturn(Promise.resolve(null));
140140
store.getSavedSyncToken = expect.createSpy().andReturn(Promise.resolve(null));
141141
store.setSyncData = expect.createSpy().andReturn(Promise.resolve(null));
142+
store.getClientOptions = expect.createSpy().andReturn(Promise.resolve(null));
143+
store.storeClientOptions = expect.createSpy().andReturn(Promise.resolve(null));
144+
store.isNewlyCreated = expect.createSpy().andReturn(Promise.resolve(true));
142145
client = new MatrixClient({
143146
baseUrl: "https://my.home.server",
144147
idBaseUrl: identityServerUrl,
@@ -182,7 +185,7 @@ describe("MatrixClient", function() {
182185
});
183186
});
184187

185-
it("should not POST /filter if a matching filter already exists", function(done) {
188+
it("should not POST /filter if a matching filter already exists", async function() {
186189
httpLookups = [];
187190
httpLookups.push(PUSH_RULES_RESPONSE);
188191
httpLookups.push(SYNC_RESPONSE);
@@ -191,31 +194,38 @@ describe("MatrixClient", function() {
191194
const filter = new sdk.Filter(0, filterId);
192195
filter.setDefinition({"room": {"timeline": {"limit": 8}}});
193196
store.getFilter.andReturn(filter);
194-
client.startClient();
195-
196-
client.on("sync", function syncListener(state) {
197-
if (state === "SYNCING") {
198-
expect(httpLookups.length).toEqual(0);
199-
client.removeListener("sync", syncListener);
200-
done();
201-
}
197+
const syncPromise = new Promise((resolve, reject) => {
198+
client.on("sync", function syncListener(state) {
199+
if (state === "SYNCING") {
200+
expect(httpLookups.length).toEqual(0);
201+
client.removeListener("sync", syncListener);
202+
resolve();
203+
} else if (state === "ERROR") {
204+
reject(new Error("sync error"));
205+
}
206+
});
202207
});
208+
await client.startClient();
209+
await syncPromise;
203210
});
204211

205212
describe("getSyncState", function() {
206213
it("should return null if the client isn't started", function() {
207214
expect(client.getSyncState()).toBe(null);
208215
});
209216

210-
it("should return the same sync state as emitted sync events", function(done) {
211-
client.on("sync", function syncListener(state) {
212-
expect(state).toEqual(client.getSyncState());
213-
if (state === "SYNCING") {
214-
client.removeListener("sync", syncListener);
215-
done();
216-
}
217+
it("should return the same sync state as emitted sync events", async function() {
218+
const syncingPromise = new Promise((resolve) => {
219+
client.on("sync", function syncListener(state) {
220+
expect(state).toEqual(client.getSyncState());
221+
if (state === "SYNCING") {
222+
client.removeListener("sync", syncListener);
223+
resolve();
224+
}
225+
});
217226
});
218-
client.startClient();
227+
await client.startClient();
228+
await syncingPromise;
219229
});
220230
});
221231

@@ -258,8 +268,8 @@ describe("MatrixClient", function() {
258268
});
259269

260270
describe("retryImmediately", function() {
261-
it("should return false if there is no request waiting", function() {
262-
client.startClient();
271+
it("should return false if there is no request waiting", async function() {
272+
await client.startClient();
263273
expect(client.retryImmediately()).toBe(false);
264274
});
265275

src/client.js

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ const ContentHelpers = require("./content-helpers");
4444

4545
import ReEmitter from './ReEmitter';
4646
import RoomList from './crypto/RoomList';
47+
import {InvalidStoreError} from './errors';
4748

4849

4950
const LAZY_LOADING_MESSAGES_FILTER = {
@@ -3126,7 +3127,12 @@ MatrixClient.prototype.startClient = async function(opts) {
31263127
opts.lazyLoadMembers = false;
31273128
}
31283129
}
3129-
3130+
// need to vape the store when enabling LL and wasn't enabled before
3131+
const shouldClear = await this._wasLazyLoadingToggled(opts.lazyLoadMembers);
3132+
if (shouldClear) {
3133+
const reason = InvalidStoreError.TOGGLED_LAZY_LOADING;
3134+
throw new InvalidStoreError(reason, !!opts.lazyLoadMembers);
3135+
}
31303136
if (opts.lazyLoadMembers && this._crypto) {
31313137
this._crypto.enableLazyLoading();
31323138
}
@@ -3139,11 +3145,50 @@ MatrixClient.prototype.startClient = async function(opts) {
31393145
return this._canResetTimelineCallback(roomId);
31403146
};
31413147
this._clientOpts = opts;
3142-
3148+
await this._storeClientOptions(this._clientOpts);
31433149
this._syncApi = new SyncApi(this, opts);
31443150
this._syncApi.sync();
31453151
};
31463152

3153+
/**
3154+
* Is the lazy loading option different than in previous session?
3155+
* @param {bool} lazyLoadMembers current options for lazy loading
3156+
* @return {bool} whether or not the option has changed compared to the previous session */
3157+
MatrixClient.prototype._wasLazyLoadingToggled = async function(lazyLoadMembers) {
3158+
lazyLoadMembers = !!lazyLoadMembers;
3159+
// assume it was turned off before
3160+
// if we don't know any better
3161+
let lazyLoadMembersBefore = false;
3162+
const isStoreNewlyCreated = await this.store.isNewlyCreated();
3163+
if (!isStoreNewlyCreated) {
3164+
const prevClientOptions = await this.store.getClientOptions();
3165+
if (prevClientOptions) {
3166+
lazyLoadMembersBefore = !!prevClientOptions.lazyLoadMembers;
3167+
}
3168+
return lazyLoadMembersBefore !== lazyLoadMembers;
3169+
}
3170+
return false;
3171+
};
3172+
3173+
/**
3174+
* store client options with boolean/string/numeric values
3175+
* to know in the next session what flags the sync data was
3176+
* created with (e.g. lazy loading)
3177+
* @param {object} opts the complete set of client options
3178+
* @return {Promise} for store operation */
3179+
MatrixClient.prototype._storeClientOptions = function(opts) {
3180+
const primTypes = ["boolean", "string", "number"];
3181+
const serializableOpts = Object.entries(opts)
3182+
.filter(([key, value]) => {
3183+
return primTypes.includes(typeof value);
3184+
})
3185+
.reduce((obj, [key, value]) => {
3186+
obj[key] = value;
3187+
return obj;
3188+
}, {});
3189+
return this.store.storeClientOptions(serializableOpts);
3190+
};
3191+
31473192
/**
31483193
* High level helper method to stop the client from polling and allow a
31493194
* clean shutdown.

src/errors.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// can't just do InvalidStoreError extends Error
2+
// because of http://babeljs.io/docs/usage/caveats/#classes
3+
function InvalidStoreError(reason, value) {
4+
const message = `Store is invalid because ${reason}, ` +
5+
`please delete all data and retry`;
6+
const instance = Reflect.construct(Error, [message]);
7+
Reflect.setPrototypeOf(instance, Reflect.getPrototypeOf(this));
8+
instance.reason = reason;
9+
instance.value = value;
10+
return instance;
11+
}
12+
13+
InvalidStoreError.TOGGLED_LAZY_LOADING = "TOGGLED_LAZY_LOADING";
14+
15+
InvalidStoreError.prototype = Object.create(Error.prototype, {
16+
constructor: {
17+
value: Error,
18+
enumerable: false,
19+
writable: true,
20+
configurable: true,
21+
},
22+
});
23+
Reflect.setPrototypeOf(InvalidStoreError, Error);
24+
25+
module.exports.InvalidStoreError = InvalidStoreError;

src/matrix.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ module.exports.SyncAccumulator = require("./sync-accumulator");
3434
module.exports.MatrixHttpApi = require("./http-api").MatrixHttpApi;
3535
/** The {@link module:http-api.MatrixError|MatrixError} class. */
3636
module.exports.MatrixError = require("./http-api").MatrixError;
37+
/** The {@link module:errors.InvalidStoreError|InvalidStoreError} class. */
38+
module.exports.InvalidStoreError = require("./errors").InvalidStoreError;
3739
/** The {@link module:client.MatrixClient|MatrixClient} class. */
3840
module.exports.MatrixClient = require("./client").MatrixClient;
3941
/** The {@link module:models/room|Room} class. */

src/store/indexeddb-local-backend.js

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import Promise from 'bluebird';
1919
import SyncAccumulator from "../sync-accumulator";
2020
import utils from "../utils";
2121

22-
const VERSION = 2;
22+
const VERSION = 3;
2323

2424
function createDatabase(db) {
2525
// Make user store, clobber based on user ID. (userId property of User objects)
@@ -41,6 +41,12 @@ function upgradeSchemaV2(db) {
4141
oobMembersStore.createIndex("room", "room_id");
4242
}
4343

44+
function upgradeSchemaV3(db) {
45+
db.createObjectStore("client_options",
46+
{ keyPath: ["clobber"]});
47+
}
48+
49+
4450
/**
4551
* Helper method to collect results from a Cursor and promiseify it.
4652
* @param {ObjectStore|Index} store The store to perform openCursor on.
@@ -123,6 +129,7 @@ const LocalIndexedDBStoreBackend = function LocalIndexedDBStoreBackend(
123129
this.db = null;
124130
this._disconnected = true;
125131
this._syncAccumulator = new SyncAccumulator();
132+
this._isNewlyCreated = false;
126133
};
127134

128135

@@ -153,11 +160,15 @@ LocalIndexedDBStoreBackend.prototype = {
153160
`LocalIndexedDBStoreBackend.connect: upgrading from ${oldVersion}`,
154161
);
155162
if (oldVersion < 1) { // The database did not previously exist.
163+
this._isNewlyCreated = true;
156164
createDatabase(db);
157165
}
158166
if (oldVersion < 2) {
159167
upgradeSchemaV2(db);
160168
}
169+
if (oldVersion < 3) {
170+
upgradeSchemaV3(db);
171+
}
161172
// Expand as needed.
162173
};
163174

@@ -185,6 +196,10 @@ LocalIndexedDBStoreBackend.prototype = {
185196
return this._init();
186197
});
187198
},
199+
/** @return {bool} whether or not the database was newly created in this session. */
200+
isNewlyCreated: function() {
201+
return Promise.resolve(this._isNewlyCreated);
202+
},
188203

189204
/**
190205
* Having connected, load initial data from the database and prepare for use
@@ -529,6 +544,28 @@ LocalIndexedDBStoreBackend.prototype = {
529544
});
530545
});
531546
},
547+
548+
getClientOptions: function() {
549+
return Promise.resolve().then(() => {
550+
const txn = this.db.transaction(["client_options"], "readonly");
551+
const store = txn.objectStore("client_options");
552+
return selectQuery(store, undefined, (cursor) => {
553+
if (cursor.value && cursor.value && cursor.value.options) {
554+
return cursor.value.options;
555+
}
556+
}).then((results) => results[0]);
557+
});
558+
},
559+
560+
storeClientOptions: async function(options) {
561+
const txn = this.db.transaction(["client_options"], "readwrite");
562+
const store = txn.objectStore("client_options");
563+
store.put({
564+
clobber: "-", // constant key so will always clobber
565+
options: options,
566+
}); // put == UPSERT
567+
await txnAsPromise(txn);
568+
},
532569
};
533570

534571
export default LocalIndexedDBStoreBackend;

src/store/indexeddb-remote-backend.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,10 @@ RemoteIndexedDBStoreBackend.prototype = {
6565
clearDatabase: function() {
6666
return this._ensureStarted().then(() => this._doCmd('clearDatabase'));
6767
},
68-
68+
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
69+
isNewlyCreated: function() {
70+
return this._doCmd('isNewlyCreated');
71+
},
6972
/**
7073
* @return {Promise} Resolves with a sync response to restore the
7174
* client state to where it was at the last save, or null if there
@@ -114,6 +117,14 @@ RemoteIndexedDBStoreBackend.prototype = {
114117
return this._doCmd('clearOutOfBandMembers', [roomId]);
115118
},
116119

120+
getClientOptions: function() {
121+
return this._doCmd('getClientOptions');
122+
},
123+
124+
storeClientOptions: function(options) {
125+
return this._doCmd('storeClientOptions', [options]);
126+
},
127+
117128
/**
118129
* Load all user presence events from the database. This is not cached.
119130
* @return {Promise<Object[]>} A list of presence events in their raw form.

src/store/indexeddb-store-worker.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ class IndexedDBStoreWorker {
6767
case 'connect':
6868
prom = this.backend.connect();
6969
break;
70+
case 'isNewlyCreated':
71+
prom = this.backend.isNewlyCreated();
72+
break;
7073
case 'clearDatabase':
7174
prom = this.backend.clearDatabase().then((result) => {
7275
// This returns special classes which can't be cloned
@@ -101,10 +104,16 @@ class IndexedDBStoreWorker {
101104
case 'setOutOfBandMembers':
102105
prom = this.backend.setOutOfBandMembers(msg.args[0], msg.args[1]);
103106
break;
107+
case 'getClientOptions':
108+
prom = this.backend.getClientOptions();
109+
break;
110+
case 'storeClientOptions':
111+
prom = this.backend.storeClientOptions(msg.args[0]);
112+
break;
104113
}
105114

106115
if (prom === undefined) {
107-
postMessage({
116+
this.postMessage({
108117
command: 'cmd_fail',
109118
seq: msg.seq,
110119
// Can't be an Error because they're not structured cloneable

src/store/indexeddb.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,11 @@ IndexedDBStore.prototype.getSavedSync = function() {
146146
return this.backend.getSavedSync();
147147
};
148148

149+
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
150+
IndexedDBStore.prototype.isNewlyCreated = function() {
151+
return this.backend.isNewlyCreated();
152+
};
153+
149154
/**
150155
* @return {Promise} If there is a saved sync, the nextBatch token
151156
* for this sync, otherwise null.
@@ -246,4 +251,12 @@ IndexedDBStore.prototype.clearOutOfBandMembers = function(roomId) {
246251
return this.backend.clearOutOfBandMembers(roomId);
247252
};
248253

254+
IndexedDBStore.prototype.getClientOptions = function() {
255+
return this.backend.getClientOptions();
256+
};
257+
258+
IndexedDBStore.prototype.storeClientOptions = function(options) {
259+
return this.backend.storeClientOptions(options);
260+
};
261+
249262
module.exports.IndexedDBStore = IndexedDBStore;

0 commit comments

Comments
 (0)