more WIP and breakage

This commit is contained in:
Bruno Windels 2020-04-09 23:19:49 +02:00
parent ef267ca331
commit 378b75c98a
11 changed files with 326 additions and 91 deletions

View file

@ -20,6 +20,7 @@ export default class BrawlViewModel extends EventEmitter {
this._loading = false; this._loading = false;
this._error = null; this._error = null;
this._sessionViewModel = null; this._sessionViewModel = null;
this._sessionSubscription = null;
this._loginViewModel = null; this._loginViewModel = null;
this._sessionPickerViewModel = null; this._sessionPickerViewModel = null;
} }
@ -33,45 +34,35 @@ export default class BrawlViewModel extends EventEmitter {
} }
async _showPicker() { async _showPicker() {
this._clearSections(); this._setSection(() => {
this._sessionPickerViewModel = new SessionPickerViewModel({ this._sessionPickerViewModel = new SessionPickerViewModel({
sessionStore: this._sessionStore, sessionStore: this._sessionStore,
storageFactory: this._storageFactory, storageFactory: this._storageFactory,
sessionCallback: sessionInfo => this._onSessionPicked(sessionInfo) sessionCallback: sessionInfo => this._onSessionPicked(sessionInfo)
});
}); });
this.emit("change", "activeSection");
try { try {
await this._sessionPickerViewModel.load(); await this._sessionPickerViewModel.load();
} catch (err) { } catch (err) {
this._clearSections(); this._setSection(() => this._error = err);
this._error = err;
this.emit("change", "activeSection");
} }
} }
_showLogin() { _showLogin() {
this._clearSections(); this._setSection(() => {
this._loginViewModel = new LoginViewModel({ this._loginViewModel = new LoginViewModel({
createHsApi: this._createHsApi, createHsApi: this._createHsApi,
defaultHomeServer: "https://matrix.org", defaultHomeServer: "https://matrix.org",
loginCallback: loginData => this._onLoginFinished(loginData) loginCallback: loginData => this._onLoginFinished(loginData)
}); });
this.emit("change", "activeSection"); })
} }
_showSession(session, sync) { _showSession(session, sync) {
this._clearSections(); this._setSection(() => {
this._sessionViewModel = new SessionViewModel({session, sync}); this._sessionViewModel = new SessionViewModel({session, sync});
this.emit("change", "activeSection"); });
}
_clearSections() {
this._error = null;
this._loading = false;
this._sessionViewModel = null;
this._loginViewModel = null;
this._sessionPickerViewModel = null;
} }
get activeSection() { get activeSection() {
@ -88,6 +79,25 @@ export default class BrawlViewModel extends EventEmitter {
} }
} }
_setSection(setter) {
const oldSection = this.activeSection;
// clear all members the activeSection depends on
this._error = null;
this._loading = false;
this._sessionViewModel = null;
this._loginViewModel = null;
this._sessionPickerViewModel = null;
// now set it again
setter();
const newSection = this.activeSection;
// remove session subscription when navigating away
if (oldSection === "session" && newSection !== oldSection) {
this._sessionSubscription();
this._sessionSubscription = null;
}
this.emit("change", "activeSection");
}
get loadingText() { return this._loadingText; } get loadingText() { return this._loadingText; }
get sessionViewModel() { return this._sessionViewModel; } get sessionViewModel() { return this._sessionViewModel; }
get loginViewModel() { return this._loginViewModel; } get loginViewModel() { return this._loginViewModel; }
@ -123,51 +133,12 @@ export default class BrawlViewModel extends EventEmitter {
} }
async _loadSession(sessionInfo) { async _loadSession(sessionInfo) {
try { this._setSection(() => {
this._loading = true; // TODO this is pseudo code-ish
this._loadingText = "Loading your conversations…"; const container = this._createSessionContainer();
const reconnector = new Reconnector( this._sessionViewModel = new SessionViewModel({session, sync});
new ExponentialRetryDelay(2000, this._clock.createTimeout), this._sessionSubscription = this._activeSessionContainer.subscribe(this._updateSessionState);
this._clock.createMeasure this._activeSessionContainer.start(sessionInfo);
); });
const hsApi = this._createHsApi(sessionInfo.homeServer, sessionInfo.accessToken, reconnector);
const storage = await this._storageFactory.create(sessionInfo.id);
// no need to pass access token to session
const filteredSessionInfo = {
deviceId: sessionInfo.deviceId,
userId: sessionInfo.userId,
homeServer: sessionInfo.homeServer,
};
const session = new Session({storage, sessionInfo: filteredSessionInfo, hsApi});
// show spinner now, with title loading stored data?
this.emit("change", "activeSection");
await session.load();
const sync = new Sync({hsApi, storage, session});
reconnector.on("state", state => {
if (state === ConnectionState.Online) {
sync.start();
session.notifyNetworkAvailable(reconnector.lastVersionsResponse);
}
});
const needsInitialSync = !session.syncToken;
if (!needsInitialSync) {
this._showSession(session, sync);
}
this._loadingText = "Getting your conversations from the server…";
this.emit("change", "loadingText");
// update spinner title to initial sync
await sync.start();
if (needsInitialSync) {
this._showSession(session, sync);
}
// start sending pending messages
session.notifyNetworkAvailable();
} catch (err) {
console.error(err);
this._error = err;
}
this.emit("change", "activeSection");
} }
} }

19
src/domain/ViewModel.js Normal file
View file

@ -0,0 +1,19 @@
export class ViewModel extends ObservableValue {
constructor(options) {
super();
this.disposables = new Disposables();
this._options = options;
}
childOptions(explicitOptions) {
return Object.assign({}, this._options, explicitOptions);
}
track(disposable) {
this.disposables.track(disposable);
}
dispose() {
this.disposables.dispose();
}
}

View file

@ -0,0 +1,43 @@
import EventEmitter from "../../EventEmitter.js";
import RoomTileViewModel from "./roomlist/RoomTileViewModel.js";
import RoomViewModel from "./room/RoomViewModel.js";
import SyncStatusViewModel from "./SyncStatusViewModel.js";
export default class SessionLoadViewModel extends ViewModel {
constructor(options) {
super(options);
this._sessionContainer = options.sessionContainer;
this._updateState();
}
onSubscribeFirst() {
this.track(this._sessionContainer.subscribe(this._updateState));
}
_updateState(previousState) {
const state = this._sessionContainer.state;
if (previousState !== LoadState.Ready && state === LoadState.Ready) {
this._sessionViewModel = new SessionViewModel(this.childOptions({
sessionContainer: this._sessionContainer
}));
this.track(this._sessionViewModel);
} else if (previousState === LoadState.Ready && state !== LoadState.Ready) {
this.disposables.disposeTracked(this._sessionViewModel);
this._sessionViewModel = null;
}
this.emit();
}
get isLoading() {
const state = this._sessionContainer.state;
return state === LoadState.Loading || state === LoadState.InitialSync;
}
get loadingLabel() {
switch (this._sessionContainer.state) {
case LoadState.Loading: return "Loading your conversations…";
case LoadState.InitialSync: return "Getting your conversations from the server…";
default: return null;
}
}
}

View file

@ -31,6 +31,13 @@ export default class SessionViewModel extends EventEmitter {
return this._currentRoomViewModel; return this._currentRoomViewModel;
} }
dispose() {
if (this._currentRoomViewModel) {
this._currentRoomViewModel.dispose();
this._currentRoomViewModel = null;
}
}
_closeCurrentRoom() { _closeCurrentRoom() {
if (this._currentRoomViewModel) { if (this._currentRoomViewModel) {
this._currentRoomViewModel.dispose(); this._currentRoomViewModel.dispose();

View file

@ -1,13 +1,3 @@
// need to prevent memory leaks here!
export class DomOnlineDetected {
constructor(reconnector) {
// window.addEventListener('offline', () => appendOnlineStatus(false));
// window.addEventListener('online', () => appendOnlineStatus(true));
// appendOnlineStatus(navigator.onLine);
// on online, reconnector.tryNow()
}
}
export class ExponentialRetryDelay { export class ExponentialRetryDelay {
constructor(start = 2000, createTimeout) { constructor(start = 2000, createTimeout) {
this._start = start; this._start = start;
@ -76,7 +66,7 @@ export const ConnectionState = createEnum(
"Online" "Online"
); );
export class Reconnector { export class Reconnector extends ObservableValue {
constructor({retryDelay, createTimeMeasure}) { constructor({retryDelay, createTimeMeasure}) {
this._online this._online
this._retryDelay = retryDelay; this._retryDelay = retryDelay;
@ -124,7 +114,7 @@ export class Reconnector {
} else { } else {
this._stateSince = null; this._stateSince = null;
} }
this.emit("change", state); this.emit(state);
} }
} }

View file

@ -0,0 +1,95 @@
const factory = {
Clock: () => new DOMClock(),
Request: () => fetchRequest,
Online: () => new DOMOnline(),
HomeServerApi: ()
}
export const LoadState = createEnum(
"Loading",
"InitialSync",
"Migrating", //not used atm, but would fit here
"Error",
"Ready",
);
class SessionContainer extends ObservableValue {
constructor({clock, random, isOnline, request, storageFactory, factory}) {
this.disposables = new Disposables();
}
dispose() {
this.disposables.dispose();
}
get state() {
return this._state;
}
_setState(state) {
if (state !== this._state) {
const previousState = this._state;
this._state = state;
this.emit(previousState);
}
}
get sync() {
return this._sync;
}
get session() {
return this._session;
}
_createReconnector() {
const reconnector = new Reconnector(
new ExponentialRetryDelay(2000, this._clock.createTimeout),
this._clock.createMeasure
);
// retry connection immediatly when online is detected
this.disposables.track(isOnline.subscribe(online => {
if(online) {
reconnector.tryNow();
}
}));
return reconnector;
}
async start(sessionInfo) {
try {
this._setState(LoadState.Loading);
this._reconnector = this._createReconnector();
const hsApi = this._createHsApi(sessionInfo.homeServer, sessionInfo.accessToken, this._reconnector);
const storage = await this._storageFactory.create(sessionInfo.id);
// no need to pass access token to session
const filteredSessionInfo = {
deviceId: sessionInfo.deviceId,
userId: sessionInfo.userId,
homeServer: sessionInfo.homeServer,
};
this._session = new Session({storage, sessionInfo: filteredSessionInfo, hsApi});
await this._session.load();
this._sync = new Sync({hsApi, storage, this._session});
// notify sync and session when back online
this.disposables.track(reconnector.subscribe(state => {
this._sync.start();
session.notifyNetworkAvailable(reconnector.lastVersionsResponse);
}));
const needsInitialSync = !this._session.syncToken;
if (!needsInitialSync) {
this._setState(LoadState.Ready);
} else {
this._setState(LoadState.InitialSync);
}
await this._sync.start();
this._setState(LoadState.Ready);
this._session.notifyNetworkAvailable();
} catch (err) {
this._error = err;
this._setState(LoadState.Error);
}
}
}

View file

@ -31,6 +31,15 @@ export default class BaseObservableCollection {
// Add iterator over handlers here // Add iterator over handlers here
} }
// like an EventEmitter, but doesn't have an event type
export class ObservableValue extends BaseObservableCollection {
emit(argument) {
for (const h of this._handlers) {
h(argument);
}
}
}
export function tests() { export function tests() {
class Collection extends BaseObservableCollection { class Collection extends BaseObservableCollection {
constructor() { constructor() {

View file

@ -1,6 +1,6 @@
import {AbortError} from "./error.js"; import {AbortError} from "../utils/error.js";
class DOMTimeout { class Timeout {
constructor(ms) { constructor(ms) {
this._reject = null; this._reject = null;
this._handle = null; this._handle = null;
@ -27,7 +27,7 @@ class DOMTimeout {
} }
} }
class DOMTimeMeasure { class TimeMeasure {
constructor() { constructor() {
this._start = window.performance.now(); this._start = window.performance.now();
} }
@ -37,13 +37,13 @@ class DOMTimeMeasure {
} }
} }
export class DOMClock { export class Clock {
createMeasure() { createMeasure() {
return new DOMTimeMeasure(); return new TimeMeasure();
} }
createTimeout(ms) { createTimeout(ms) {
return new DOMTimeout(ms); return new Timeout(ms);
} }
now() { now() {

29
src/ui/web/dom/Online.js Normal file
View file

@ -0,0 +1,29 @@
export class Online extends ObservableValue {
constructor() {
super();
this._onOffline = this._onOffline.bind(this);
this._onOnline = this._onOnline.bind(this);
}
_onOffline() {
this.emit(false);
}
_onOnline() {
this.emit(true);
}
get value() {
return navigator.onLine;
}
onSubscribeFirst() {
window.addEventListener('offline', this._onOffline);
window.addEventListener('online', this._onOnline);
}
onUnsubscribeLast() {
window.removeEventListener('offline', this._onOffline);
window.removeEventListener('online', this._onOnline);
}
}

View file

@ -34,3 +34,38 @@ export default class SwitchView {
return this._childView; return this._childView;
} }
} }
// SessionLoadView
// should this be the new switch view?
// and the other one be the BasicSwitchView?
new BoundSwitchView(vm, vm => vm.isLoading, (loading, vm) => {
if (loading) {
return new InlineTemplateView(vm, t => {
return t.div({className: "loading"}, [
t.span({className: "spinner"}),
t.span(vm => vm.loadingText)
]);
});
} else {
return new SessionView(vm.sessionViewModel);
}
});
class BoundSwitchView extends SwitchView {
constructor(value, mapper, viewCreator) {
super(viewCreator(mapper(value), value));
this._mapper = mapper;
this._viewCreator = viewCreator;
this._mappedValue = mapper(value);
}
update(value) {
const mappedValue = this._mapper(value);
if (mappedValue !== this._mappedValue) {
this._mappedValue = mappedValue;
this.switch(this._viewCreator(this._mappedValue, value));
} else {
super.update(value);
}
}
}

37
src/utils/Disposables.js Normal file
View file

@ -0,0 +1,37 @@
function disposeValue(value) {
if (typeof d === "function") {
value();
} else {
value.dispose();
}
}
export class Disposables {
constructor() {
this._disposables = [];
}
track(disposable) {
this._disposables.push(disposable);
}
dispose() {
if (this._disposables) {
for (const d of this._disposables) {
disposeValue(d);
}
this._disposables = null;
}
}
disposeTracked(value) {
const idx = this._disposables.indexOf(value);
if (idx !== -1) {
const [foundValue] = this._disposables.splice(idx, 1);
disposeValue(foundValue);
return true;
}
return false;
}
}