diff --git a/CHANGELOG.md b/CHANGELOG.md index 86c9b82dc..82902fc22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,47 @@ +Changes in [0.12.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.12.0) (2018-10-16) +================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.12.0-rc.1...v0.12.0) + + * No changes since rc.1 + +Changes in [0.12.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.12.0-rc.1) (2018-10-11) +============================================================================================================ +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.11.1...v0.12.0-rc.1) + +BREAKING CHANGES +---------------- + * If js-sdk finds data in the store that is incompatible with the options currently being used, + it will emit sync state ERROR with an error of type InvalidStoreError. It will also stop trying + to sync in this situation: the app must stop the client and then either clear the store or + change the options (in this case, enable or disable lazy loading of members) and then start + the client again. + +All Changes +----------- + + * never replace /sync'ed memberships with OOB ones + [\#760](https://github.com/matrix-org/matrix-js-sdk/pull/760) + * Don't fail to start up if lazy load check fails + [\#759](https://github.com/matrix-org/matrix-js-sdk/pull/759) + * Make e2e work on Edge + [\#754](https://github.com/matrix-org/matrix-js-sdk/pull/754) + * throw error with same name and message over idb worker boundary + [\#758](https://github.com/matrix-org/matrix-js-sdk/pull/758) + * Default to a room version of 1 when there is no room create event + [\#755](https://github.com/matrix-org/matrix-js-sdk/pull/755) + * Silence bluebird warnings + [\#757](https://github.com/matrix-org/matrix-js-sdk/pull/757) + * allow non-ff merge from release branch into master + [\#750](https://github.com/matrix-org/matrix-js-sdk/pull/750) + * Reject with the actual error on indexeddb error + [\#751](https://github.com/matrix-org/matrix-js-sdk/pull/751) + * Update mocha to v5 + [\#744](https://github.com/matrix-org/matrix-js-sdk/pull/744) + * disable lazy loading for guests as they cant create filters + [\#748](https://github.com/matrix-org/matrix-js-sdk/pull/748) + * Revert "Add getMediaLimits to client" + [\#745](https://github.com/matrix-org/matrix-js-sdk/pull/745) + Changes in [0.11.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.11.1) (2018-10-01) ================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.11.1-rc.1...v0.11.1) diff --git a/README.md b/README.md index 6ba36d944..be2fa0f86 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,61 @@ In Node.js console.log("Public Rooms: %s", JSON.stringify(data)); }); ``` + See below for how to include libolm to enable end-to-end-encryption. Please check [the Node.js terminal app](examples/node) for a more complex example. +To start the client: + +```javascript +await client.startClient({initialSyncLimit: 10}); +``` + +You can perform a call to `/sync` to get the current state of the client: + +```javascript +client.once('sync', function(state, prevState, res) { + if(state === 'PREPARED') { + console.log("prepared"); + } else { + console.log(state); + process.exit(1); + } +}); +``` + +To send a message: + +```javascript +var content = { + "body": "message text", + "msgtype": "m.text" +}; +client.sendEvent("roomId", "m.room.message", content, "", (err, res) => { + console.log(err); +}); +``` + +To listen for message events: + +```javascript +client.on("Room.timeline", function(event, room, toStartOfTimeline) { + if (event.getType() !== "m.room.message") { + return; // only use messages + } + console.log(event.event.content.body); +}); +``` + +By default, the `matrix-js-sdk` client uses the `MatrixInMemoryStore` to store events as they are received. For example to iterate through the currently stored timeline for a room: + +```javascript +Object.keys(client.store.rooms).forEach((roomId) => { + client.getRoom(roomId).timeline.forEach(t => { + console.log(t.event); + }); +}); +``` What does this SDK do? ---------------------- diff --git a/package.json b/package.json index db0b14bb1..88f87f90b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "0.11.1", + "version": "0.12.0", "description": "Matrix Client-Server SDK for Javascript", "main": "index.js", "scripts": { @@ -21,6 +21,7 @@ "prepublish": "npm run clean && npm run build && git rev-parse HEAD > git-revision.txt" }, "repository": { + "type": "git", "url": "https://github.com/matrix-org/matrix-js-sdk" }, "keywords": [ diff --git a/spec/unit/room-member.spec.js b/spec/unit/room-member.spec.js index 5212282b1..298771128 100644 --- a/spec/unit/room-member.spec.js +++ b/spec/unit/room-member.spec.js @@ -201,22 +201,6 @@ describe("RoomMember", function() { }); }); - describe("supersedesOutOfBand", function() { - it("should be set by markSupersedesOutOfBand", function() { - const member = new RoomMember(); - expect(member.supersedesOutOfBand()).toEqual(false); - member.markSupersedesOutOfBand(); - expect(member.supersedesOutOfBand()).toEqual(true); - }); - it("should be cleared by clearSupersedesOutOfBand", function() { - const member = new RoomMember(); - member.markSupersedesOutOfBand(); - expect(member.supersedesOutOfBand()).toEqual(true); - member.clearSupersedesOutOfBand(); - expect(member.supersedesOutOfBand()).toEqual(false); - }); - }); - describe("setMembershipEvent", function() { const joinEvent = utils.mkMembership({ event: true, diff --git a/spec/unit/room-state.spec.js b/spec/unit/room-state.spec.js index 6619c64a7..baa602781 100644 --- a/spec/unit/room-state.spec.js +++ b/spec/unit/room-state.spec.js @@ -299,41 +299,25 @@ describe("RoomState", function() { expect(eventReceived).toEqual(true); }); - it("should overwrite existing members", function() { + it("should never overwrite existing members", function() { const oobMemberEvent = utils.mkMembership({ user: userA, mship: "join", room: roomId, event: true, }); state.markOutOfBandMembersStarted(); state.setOutOfBandMembers([oobMemberEvent]); const memberA = state.getMember(userA); - expect(memberA.events.member.getId()).toEqual(oobMemberEvent.getId()); - expect(memberA.isOutOfBand()).toEqual(true); - }); - - it("should allow later state events to overwrite", function() { - const oobMemberEvent = utils.mkMembership({ - user: userA, mship: "join", room: roomId, event: true, - }); - const memberEvent = utils.mkMembership({ - user: userA, mship: "join", room: roomId, event: true, - }); - - state.markOutOfBandMembersStarted(); - state.setOutOfBandMembers([oobMemberEvent]); - state.setStateEvents([memberEvent]); - - const memberA = state.getMember(userA); - expect(memberA.events.member.getId()).toEqual(memberEvent.getId()); + expect(memberA.events.member.getId()).toNotEqual(oobMemberEvent.getId()); expect(memberA.isOutOfBand()).toEqual(false); }); it("should emit members when updating a member", function() { + const doesntExistYetUserId = "@doesntexistyet:hs"; const oobMemberEvent = utils.mkMembership({ - user: userA, mship: "join", room: roomId, event: true, + user: doesntExistYetUserId, mship: "join", room: roomId, event: true, }); let eventReceived = false; state.once('RoomState.members', (_, __, member) => { - expect(member.userId).toEqual(userA); + expect(member.userId).toEqual(doesntExistYetUserId); eventReceived = true; }); @@ -341,28 +325,6 @@ describe("RoomState", function() { state.setOutOfBandMembers([oobMemberEvent]); expect(eventReceived).toEqual(true); }); - - - it("should not overwrite members updated since starting loading oob", - function() { - const oobMemberEvent = utils.mkMembership({ - user: userA, mship: "join", room: roomId, event: true, - }); - - const existingMemberEvent = utils.mkMembership({ - user: userA, mship: "join", room: roomId, event: true, - }); - - state.markOutOfBandMembersStarted(); - state.setStateEvents([existingMemberEvent]); - expect(state.getMember(userA).supersedesOutOfBand()).toEqual(true); - state.setOutOfBandMembers([oobMemberEvent]); - - const memberA = state.getMember(userA); - expect(memberA.events.member.getId()).toEqual(existingMemberEvent.getId()); - expect(memberA.isOutOfBand()).toEqual(false); - expect(memberA.supersedesOutOfBand()).toEqual(false); - }); }); describe("clone", function() { @@ -386,20 +348,6 @@ describe("RoomState", function() { expect(state.getJoinedMemberCount()).toEqual(copy.getJoinedMemberCount()); }); - it("should copy supersedes flag when OOB loading is progress", - function() { - // include OOB members in copy - state.markOutOfBandMembersStarted(); - state.setStateEvents([utils.mkMembership({ - user: userA, mship: "join", room: roomId, event: true, - })]); - const copy = state.clone(); - const memberA = state.getMember(userA); - const memberACopy = copy.getMember(userA); - expect(memberA.supersedesOutOfBand()).toEqual(true); - expect(memberACopy.supersedesOutOfBand()).toEqual(true); - }); - it("should mark old copy as not waiting for out of band anymore", function() { state.markOutOfBandMembersStarted(); const copy = state.clone(); diff --git a/src/client.js b/src/client.js index bc3c5a1c5..2de0e119b 100644 --- a/src/client.js +++ b/src/client.js @@ -44,7 +44,6 @@ const ContentHelpers = require("./content-helpers"); import ReEmitter from './ReEmitter'; import RoomList from './crypto/RoomList'; -import {InvalidStoreError} from './errors'; import Crypto from './crypto'; import { isCryptoAvailable } from './crypto'; @@ -53,16 +52,6 @@ import { isCryptoAvailable } from './crypto'; // and need to migrate, but they spam the console with warnings. Promise.config({warnings: false}); -const LAZY_LOADING_MESSAGES_FILTER = { - lazy_load_members: true, -}; - -const LAZY_LOADING_SYNC_FILTER = { - room: { - state: LAZY_LOADING_MESSAGES_FILTER, - }, -}; - const SCROLLBACK_DELAY_MS = 3000; const CRYPTO_ENABLED = isCryptoAvailable(); @@ -772,6 +761,17 @@ MatrixClient.prototype.getGroups = function() { return this.store.getGroups(); }; +/** + * Get the config for the media repository. + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves with an object containing the config. + */ +MatrixClient.prototype.getMediaConfig = function(callback) { + return this._http.requestWithPrefix( + callback, "GET", "/config", undefined, undefined, httpApi.PREFIX_MEDIA_R0, + ); +}; + // Room ops // ======== @@ -2068,7 +2068,7 @@ MatrixClient.prototype.getEventTimeline = function(timelineSet, eventId) { let params = undefined; if (this._clientOpts.lazyLoadMembers) { - params = {filter: JSON.stringify(LAZY_LOADING_MESSAGES_FILTER)}; + params = {filter: JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER)}; } // TODO: we should implement a backoff (as per scrollback()) to deal more @@ -2149,7 +2149,7 @@ function(roomId, fromToken, limit, dir, timelineFilter = undefined) { if (this._clientOpts.lazyLoadMembers) { // create a shallow copy of LAZY_LOADING_MESSAGES_FILTER, // so the timelineFilter doesn't get written into it below - filter = Object.assign({}, LAZY_LOADING_MESSAGES_FILTER); + filter = Object.assign({}, Filter.LAZY_LOADING_MESSAGES_FILTER); } if (timelineFilter) { // XXX: it's horrific that /messages' filter parameter doesn't match @@ -3106,29 +3106,6 @@ MatrixClient.prototype.startClient = async function(opts) { // shallow-copy the opts dict before modifying and storing it opts = Object.assign({}, opts); - if (opts.lazyLoadMembers && this.isGuest()) { - opts.lazyLoadMembers = false; - } - if (opts.lazyLoadMembers) { - const supported = await this.doesServerSupportLazyLoading(); - if (supported) { - opts.filter = await this.createFilter(LAZY_LOADING_SYNC_FILTER); - } else { - console.log("LL: lazy loading requested but not supported " + - "by server, so disabling"); - opts.lazyLoadMembers = false; - } - } - // need to vape the store when enabling LL and wasn't enabled before - const shouldClear = await this._wasLazyLoadingToggled(opts.lazyLoadMembers); - if (shouldClear) { - const reason = InvalidStoreError.TOGGLED_LAZY_LOADING; - throw new InvalidStoreError(reason, !!opts.lazyLoadMembers); - } - if (opts.lazyLoadMembers && this._crypto) { - this._crypto.enableLazyLoading(); - } - opts.crypto = this._crypto; opts.canResetEntireTimeline = (roomId) => { if (!this._canResetTimelineCallback) { @@ -3137,40 +3114,19 @@ MatrixClient.prototype.startClient = async function(opts) { return this._canResetTimelineCallback(roomId); }; this._clientOpts = opts; - await this._storeClientOptions(this._clientOpts); this._syncApi = new SyncApi(this, opts); this._syncApi.sync(); }; -/** - * Is the lazy loading option different than in previous session? - * @param {bool} lazyLoadMembers current options for lazy loading - * @return {bool} whether or not the option has changed compared to the previous session */ -MatrixClient.prototype._wasLazyLoadingToggled = async function(lazyLoadMembers) { - lazyLoadMembers = !!lazyLoadMembers; - // assume it was turned off before - // if we don't know any better - let lazyLoadMembersBefore = false; - const isStoreNewlyCreated = await this.store.isNewlyCreated(); - if (!isStoreNewlyCreated) { - const prevClientOptions = await this.store.getClientOptions(); - if (prevClientOptions) { - lazyLoadMembersBefore = !!prevClientOptions.lazyLoadMembers; - } - return lazyLoadMembersBefore !== lazyLoadMembers; - } - return false; -}; - /** * store client options with boolean/string/numeric values * to know in the next session what flags the sync data was * created with (e.g. lazy loading) * @param {object} opts the complete set of client options * @return {Promise} for store operation */ -MatrixClient.prototype._storeClientOptions = function(opts) { +MatrixClient.prototype._storeClientOptions = function() { const primTypes = ["boolean", "string", "number"]; - const serializableOpts = Object.entries(opts) + const serializableOpts = Object.entries(this._clientOpts) .filter(([key, value]) => { return primTypes.includes(typeof value); }) diff --git a/src/crypto/store/indexeddb-crypto-store.js b/src/crypto/store/indexeddb-crypto-store.js index 249b29b63..0e0654deb 100644 --- a/src/crypto/store/indexeddb-crypto-store.js +++ b/src/crypto/store/indexeddb-crypto-store.js @@ -92,6 +92,19 @@ export default class IndexedDBCryptoStore { console.log(`connected to indexeddb ${this._dbName}`); resolve(new IndexedDBCryptoStoreBackend.Backend(db)); }; + }).then((backend) => { + // Edge has IndexedDB but doesn't support compund keys which we use fairly extensively. + // Try a dummy query which will fail if the browser doesn't support compund keys, so + // we can fall back to a different backend. + return backend.doTxn( + 'readonly', + [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], + (txn) => { + backend.getEndToEndInboundGroupSession('', '', txn, () => {}); + }).then(() => { + return backend; + }, + ); }).catch((e) => { console.warn( `unable to connect to indexeddb ${this._dbName}` + diff --git a/src/errors.js b/src/errors.js index 04e14f2c8..409fafd9c 100644 --- a/src/errors.js +++ b/src/errors.js @@ -2,7 +2,7 @@ // because of http://babeljs.io/docs/usage/caveats/#classes function InvalidStoreError(reason, value) { const message = `Store is invalid because ${reason}, ` + - `please delete all data and retry`; + `please stop the client, delete all data and start the client again`; const instance = Reflect.construct(Error, [message]); Reflect.setPrototypeOf(instance, Reflect.getPrototypeOf(this)); instance.reason = reason; diff --git a/src/filter.js b/src/filter.js index e0f03daa7..a63fcee13 100644 --- a/src/filter.js +++ b/src/filter.js @@ -51,6 +51,17 @@ function Filter(userId, filterId) { this.definition = {}; } +Filter.LAZY_LOADING_MESSAGES_FILTER = { + lazy_load_members: true, +}; + +Filter.LAZY_LOADING_SYNC_FILTER = { + room: { + state: Filter.LAZY_LOADING_MESSAGES_FILTER, + }, +}; + + /** * Get the ID of this filter on your homeserver (if known) * @return {?Number} The filter ID diff --git a/src/models/room-member.js b/src/models/room-member.js index 598cae398..e7a4bf88c 100644 --- a/src/models/room-member.js +++ b/src/models/room-member.js @@ -59,7 +59,6 @@ function RoomMember(roomId, userId) { member: null, }; this._isOutOfBand = false; - this._supersedesOutOfBand = false; this._updateModifiedTime(); } utils.inherits(RoomMember, EventEmitter); @@ -80,31 +79,6 @@ RoomMember.prototype.isOutOfBand = function() { return this._isOutOfBand; }; -/** - * Does the member supersede an incoming out-of-band - * member? If so the out-of-band member should be ignored. - * @return {bool} - */ -RoomMember.prototype.supersedesOutOfBand = function() { - return this._supersedesOutOfBand; -}; - -/** - * Mark the member as superseding the future incoming - * out-of-band members. - */ -RoomMember.prototype.markSupersedesOutOfBand = function() { - this._supersedesOutOfBand = true; -}; - -/** - * Clear the member superseding the future incoming - * out-of-band members, as loading finished or failed. - */ -RoomMember.prototype.clearSupersedesOutOfBand = function() { - this._supersedesOutOfBand = false; -}; - /** * Update this room member's membership event. May fire "RoomMember.name" if * this event updates this member's name. diff --git a/src/models/room-state.js b/src/models/room-state.js index 52bc5ed36..ac5e20077 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -220,8 +220,7 @@ RoomState.prototype.clone = function() { // if loading is in progress (through _oobMemberFlags) // since these are not new members, we're merely copying them // set the status to not started - // after copying, we set back the status and - // copy the superseding flag from the current state + // after copying, we set back the status const status = this._oobMemberFlags.status; this._oobMemberFlags.status = OOB_STATUS_NOTSTARTED; @@ -249,14 +248,6 @@ RoomState.prototype.clone = function() { copyMember.markOutOfBand(); } }); - } else if (this._oobMemberFlags.status == OOB_STATUS_INPROGRESS) { - // copy markSupersedesOutOfBand flags - this.getMembers().forEach((member) => { - if (member.supersedesOutOfBand()) { - const copyMember = copy.getMember(member.userId); - copyMember.markSupersedesOutOfBand(); - } - }); } return copy; @@ -341,11 +332,6 @@ RoomState.prototype.setStateEvents = function(stateEvents) { const member = self._getOrCreateMember(userId, event); member.setMembershipEvent(event, self); - // if out of band members are loading, - // mark the member as more recent - if (self._oobMemberFlags.status == OOB_STATUS_INPROGRESS) { - member.markSupersedesOutOfBand(); - } self._updateMember(member); self.emit("RoomState.members", event, self, member); @@ -434,12 +420,6 @@ RoomState.prototype.markOutOfBandMembersFailed = function() { if (this._oobMemberFlags.status !== OOB_STATUS_INPROGRESS) { return; } - // the request failed, there is nothing to supersede - // in case of a retry, these event would not supersede the - // retry anymore. - this.getMembers().forEach((m) => { - m.clearSupersedesOutOfBand(); - }); this._oobMemberFlags.status = OOB_STATUS_NOTSTARTED; }; @@ -483,27 +463,15 @@ RoomState.prototype._setOutOfBandMember = function(stateEvent) { } const userId = stateEvent.getStateKey(); const existingMember = this.getMember(userId); - if (existingMember) { - const existingMemberEvent = existingMember.events.member; - // ignore out of band members with events we are - // already aware of. - if (existingMemberEvent.getId() === stateEvent.getId()) { - return; - } - // this member was updated since we started - // loading the out of band members. - // Ignore the out of band member and clear - // the "supersedes" flag as the out of members are now loaded - if (existingMember.supersedesOutOfBand()) { - existingMember.clearSupersedesOutOfBand(); - return; - } + // never replace members received as part of the sync + if (existingMember && !existingMember.isOutOfBand()) { + return; } const member = this._getOrCreateMember(userId, stateEvent); member.setMembershipEvent(stateEvent, this); // needed to know which members need to be stored seperately - // as the are not part of the sync accumulator + // as they are not part of the sync accumulator // this is cleared by setMembershipEvent so when it's updated through /sync member.markOutOfBand(); diff --git a/src/sync.js b/src/sync.js index 39e82abe4..76122874b 100644 --- a/src/sync.js +++ b/src/sync.js @@ -33,6 +33,8 @@ const utils = require("./utils"); const Filter = require("./filter"); const EventTimeline = require("./models/event-timeline"); +import {InvalidStoreError} from './errors'; + const DEBUG = true; // /sync requests allow you to set a timeout= but the request may continue @@ -100,6 +102,7 @@ function SyncApi(client, opts) { this._connectionReturnedDefer = null; this._notifEvents = []; // accumulator of sync events in the current sync response this._failedSyncCount = 0; // Number of consecutive failed /sync requests + this._storeIsInvalid = false; // flag set if the store needs to be cleared before we can start if (client.getNotifTimelineSet()) { client.reEmitter.reEmit(client.getNotifTimelineSet(), @@ -422,6 +425,26 @@ SyncApi.prototype.recoverFromSyncStartupError = async function(savedSyncPromise, await keepaliveProm; }; +/** + * Is the lazy loading option different than in previous session? + * @param {bool} lazyLoadMembers current options for lazy loading + * @return {bool} whether or not the option has changed compared to the previous session */ +SyncApi.prototype._wasLazyLoadingToggled = async function(lazyLoadMembers) { + lazyLoadMembers = !!lazyLoadMembers; + // assume it was turned off before + // if we don't know any better + let lazyLoadMembersBefore = false; + const isStoreNewlyCreated = await this.client.store.isNewlyCreated(); + if (!isStoreNewlyCreated) { + const prevClientOptions = await this.client.store.getClientOptions(); + if (prevClientOptions) { + lazyLoadMembersBefore = !!prevClientOptions.lazyLoadMembers; + } + return lazyLoadMembersBefore !== lazyLoadMembers; + } + return false; +}; + /** * Main entry point */ @@ -444,6 +467,8 @@ SyncApi.prototype.sync = function() { // 1) We need to get push rules so we can check if events should bing as we get // them from /sync. // 2) We need to get/create a filter which we can use for /sync. + // 3) We need to check the lazy loading option matches what was used in the + // stored sync. If it doesn't, we can't use the stored sync. async function getPushRules() { try { @@ -458,9 +483,47 @@ SyncApi.prototype.sync = function() { getPushRules(); return; } - getFilter(); // Now get the filter and start syncing + checkLazyLoadStatus(); // advance to the next stage } + const checkLazyLoadStatus = async () => { + if (this.opts.lazyLoadMembers && client.isGuest()) { + this.opts.lazyLoadMembers = false; + } + if (this.opts.lazyLoadMembers) { + const supported = await client.doesServerSupportLazyLoading(); + if (supported) { + this.opts.filter = await client.createFilter( + Filter.LAZY_LOADING_SYNC_FILTER, + ); + } else { + console.log("LL: lazy loading requested but not supported " + + "by server, so disabling"); + this.opts.lazyLoadMembers = false; + } + } + // need to vape the store when enabling LL and wasn't enabled before + const shouldClear = await this._wasLazyLoadingToggled(this.opts.lazyLoadMembers); + if (shouldClear) { + this._storeIsInvalid = true; + const reason = InvalidStoreError.TOGGLED_LAZY_LOADING; + const error = new InvalidStoreError(reason, !!this.opts.lazyLoadMembers); + this._updateSyncState("ERROR", { error }); + // bail out of the sync loop now: the app needs to respond to this error. + // we leave the state as 'ERROR' which isn't great since this normally means + // we're retrying. The client must be stopped before clearing the stores anyway + // so the app should stop the client, clear the store and start it again. + console.warn("InvalidStoreError: store is not usable: stopping sync."); + return; + } + if (this.opts.lazyLoadMembers && this._crypto) { + this.opts.crypto.enableLazyLoading(); + } + await this.client._storeClientOptions(); + + getFilter(); // Now get the filter and start syncing + }; + async function getFilter() { let filter; if (self.opts.filter) { @@ -588,7 +651,12 @@ SyncApi.prototype._syncFromCache = async function(savedSync) { console.error("Error processing cached sync", e.stack || e); } - this._updateSyncState("PREPARED", syncEventData); + // Don't emit a prepared if we've bailed because the store is invalid: + // in this case the client will not be usable until stopped & restarted + // so this would be useless and misleading. + if (!this._storeIsInvalid) { + this._updateSyncState("PREPARED", syncEventData); + } }; /**