diff --git a/src/matrix/Sync.js b/src/matrix/Sync.js index 8588393d..2e1d577e 100644 --- a/src/matrix/Sync.js +++ b/src/matrix/Sync.js @@ -1,5 +1,6 @@ /* Copyright 2020 Bruno Windels +Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,6 +18,7 @@ limitations under the License. import {AbortError} from "./error.js"; import {ObservableValue} from "../observable/ObservableValue.js"; import {createEnum} from "../utils/enum.js"; +import {readPath, Type} from "../utils/validate.js"; const INCREMENTAL_TIMEOUT = 30000; const SYNC_EVENT_LIMIT = 10; @@ -43,6 +45,15 @@ function parseRooms(roomsSection, roomCallback) { return []; } +function timelineIsEmpty(roomResponse) { + try { + const events = readPath(roomResponse, ["timeline", "events"], Type.Array); + return events.length === 0; + } catch (err) { + return true; + } +} + export class Sync { constructor({hsApi, session, storage}) { this._hsApi = hsApi; @@ -102,6 +113,7 @@ export class Sync { const totalRequestTimeout = timeout + (80 * 1000); // same as riot-web, don't get stuck on wedged long requests this._currentRequest = this._hsApi.sync(syncToken, syncFilterId, timeout, {timeout: totalRequestTimeout}); const response = await this._currentRequest.response(); + const isInitialSync = !syncToken; syncToken = response.next_batch; const storeNames = this._storage.storeNames; const syncTxn = await this._storage.readWriteTxn([ @@ -120,6 +132,11 @@ export class Sync { // presence if (response.rooms) { const promises = parseRooms(response.rooms, async (roomId, roomResponse, membership) => { + // ignore rooms with empty timelines during initial sync, + // see https://github.com/vector-im/hydrogen-web/issues/15 + if (isInitialSync && timelineIsEmpty(roomResponse)) { + return; + } let room = this._session.rooms.get(roomId); if (!room) { room = this._session.createRoom(roomId); diff --git a/src/utils/validate.js b/src/utils/validate.js new file mode 100644 index 00000000..c185ad5c --- /dev/null +++ b/src/utils/validate.js @@ -0,0 +1,117 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export class InvalidPathError extends Error { + constructor(obj, path, field) { + super(`Could not read path ${path.join("/")}, stopped at ${field}. Base value is ${obj}`); + } + + get name() { + return "InvalidPathError"; + } +} + +export class InvalidTypeError extends Error { + constructor(path, fieldValue, validator) { + super(`Value ${path.join("/")} is not of type ${getTypeName(validator)} but is: ${fieldValue}`); + } + + get name() { + return "InvalidTypeError"; + } +} + +function getTypeName(validator) { + if (validator === Type.Array) { + return "Array"; + } + if (validator === Type.Integer) { + return "Integer"; + } + if (validator === Type.String) { + return "String"; + } + if (validator === Type.Object) { + return "Object"; + } + if (typeof validator === "function") { + return "Custom"; + } + return "None"; +} + +export function readPath(obj, path, typeOrDefaultValue) { + if (!obj) { + throw new InvalidPathError(obj, path); + } + const hasDefaultValue = typeof typeOrDefaultValue !== "function"; + let currentValue = obj; + for (const field of path) { + currentValue = currentValue[field]; + if (typeof currentValue === "undefined") { + if (hasDefaultValue) { + return typeOrDefaultValue; + } else { + throw new InvalidPathError(obj, path, field); + } + } + } + if (!hasDefaultValue) { + const validator = typeOrDefaultValue; + if (!validator(currentValue)) { + throw new InvalidTypeError(path, currentValue, validator); + } + } + return currentValue; +} + +export const Type = Object.freeze({ + "Array": Array.isArray, + "Integer": Number.isSafeInteger, + "Boolean": value => value === true || value === false, + "String": value => typeof value === "string", + "Object": value => value !== null && typeof value === "object", +}); + +export function tests() { + return { + "readPath value at top level": assert => { + assert.strictEqual(readPath({a: 5}, ["a"]), 5); + }, + "readPath value at deep level": assert => { + assert.strictEqual(readPath({a: {b: {c: 5}}}, ["a", "b", "c"]), 5); + }, + "readPath value with correct type": assert => { + assert.strictEqual(readPath({a: 5}, ["a"], Type.Integer), 5); + }, + "readPath value with failing type": assert => { + assert.throws( + () => readPath({a: 5}, ["a"], Type.String), + {name: "InvalidTypeError"} + ); + }, + "readPath value with failing path with intermediate field not being an object": assert => { + assert.throws( + () => readPath({a: {b: "bar"}}, ["a", "b", "c"], Type.Integer), + {name: "InvalidPathError"} + ); + }, + "readPath returns default value for incomplete path": assert => { + assert.strictEqual(readPath({a: {b: "bar"}}, ["a", "b", "c"], 5), 5); + }, + + } +}