wip on collections and listview

This commit is contained in:
Bruno Windels 2019-02-20 23:48:16 +01:00
parent 952f1abddf
commit 5bff41c1ee
14 changed files with 561 additions and 45 deletions

View file

@ -21,6 +21,8 @@
- build a very basic interface with
- a start/stop sync button
- a room list sorted alphabetically
- do some preprocessing on sync response which can then be used by persister, summary, timeline
- support timeline
- clicking on a room list, you see messages (userId -> body)
- send messages
- fill gaps with call to /messages
@ -28,3 +30,4 @@
- lazy loading members
- decide denormalized data in summary vs reading from multiple stores PER room on load
- allow Room/Summary class to be subclassed and store additional data?
- store account data, support read markers

View file

@ -1,17 +1,24 @@
import RoomSummary from "./summary.js";
import RoomPersister from "./persister.js";
import EventEmitter from "../../EventEmitter.js";
export default class Room {
constructor(roomId, storage) {
export default class Room extends EventEmitter {
constructor(roomId, storage, emitCollectionChange) {
super();
this._roomId = roomId;
this._storage = storage;
this._summary = new RoomSummary(roomId);
this._persister = new RoomPersister(roomId);
this._emitCollectionChange = emitCollectionChange;
}
async applySync(roomResponse, membership, txn) {
this._summary.applySync(roomResponse, membership, txn);
const changed = this._summary.applySync(roomResponse, membership, txn);
this._persister.persistSync(roomResponse, txn);
if (changed) {
this.emit("change");
(this._emitCollectionChange)();
}
}
load(summary, txn) {

View file

@ -1,11 +1,12 @@
import Room from "./room/room.js";
import ObservableMap from "../observable/map.js";
export default class Session {
constructor(storage) {
this._storage = storage;
this._session = null;
// use Map here?
this._rooms = {};
this._rooms = new ObservableMap();
}
// should be called before load
// loginData has device_id, user_id, home_server, access_token
@ -37,13 +38,18 @@ export default class Session {
}));
}
get rooms() {
return this._rooms;
}
getRoom(roomId) {
return this._rooms[roomId];
return this._rooms.get(roomId);
}
createRoom(roomId) {
const room = new Room(roomId, this._storage);
this._rooms[roomId] = room;
const updateCallback = (params) => this._rooms.update(roomId, params);
const room = new Room(roomId, this._storage, updateCallback);
this._rooms.add(roomId, room);
return room;
}

View file

@ -3,7 +3,7 @@ import {
HomeServerError,
StorageError
} from "./error.js";
import EventEmitter from "../event-emitter.js";
import EventEmitter from "../EventEmitter.js";
const INCREMENTAL_TIMEOUT = 30000;
const SYNC_EVENT_LIMIT = 10;

55
src/observable/map.js Normal file
View file

@ -0,0 +1,55 @@
import BaseObservableMap from "./map/BaseObservableMap.js";
export default class ObservableMap extends BaseObservableMap {
constructor(initialValues) {
super();
this._values = new Map(initialValues);
}
update(key, params) {
const value = this._values.get(key);
if (value !== undefined) {
this._values.add(key, value);
this.emitChange(key, value, params);
return true;
}
return false; // or return existing value?
}
add(key, value) {
if (!this._values.has(key)) {
this._values.add(key, value);
this.emitAdd(key, value);
return true;
}
return false; // or return existing value?
}
remove(key) {
const value = this._values.get(key);
if (value !== undefined) {
this._values.delete(key);
this.emitRemove(key, value);
return true;
} else {
return false;
}
}
reset() {
this._values.clear();
this.emitReset();
}
get(key) {
return this._values.get(key);
}
get size() {
return this._values.size;
}
[Symbol.iterator]() {
return this._values.entries()[Symbol.iterator];
}
}

View file

@ -0,0 +1,66 @@
import MapOperator from "./operators/MapOperator.js";
import SortOperator from "./operators/SortOperator.js";
export default class BaseObservableMap {
constructor() {
this._handlers = new Set();
}
emitReset() {
for(let h of this._handlers) {
h.onReset();
}
}
// we need batch events, mostly on index based collection though?
// maybe we should get started without?
emitAdd(key, value) {
for(let h of this._handlers) {
h.onAdd(key, value);
}
}
emitChange(key, value, ...params) {
for(let h of this._handlers) {
h.onChange(key, value, ...params);
}
}
emitRemove(key, value) {
for(let h of this._handlers) {
h.onRemove(key, value);
}
}
onSubscribeFirst() {
}
onUnsubscribeLast() {
}
subscribe(handler) {
this._handlers.add(handler);
if (this._handlers.length === 1) {
this.onSubscribeFirst();
}
return () => {
if (handler) {
this._handlers.delete(this._handler);
if (this._handlers.length === 0) {
this.onUnsubscribeLast();
}
handler = null;
}
return null;
};
}
map(mapper, updater) {
return new MapOperator(this, mapper, updater);
}
sort(comparator) {
return new SortOperator(this, comparator);
}
}

View file

@ -0,0 +1,22 @@
// import BaseObservableMap from "./BaseObservableMap.js";
export default class Operator /* extends BaseObservableMap */ {
constructor(source) {
// super();
this._source = source;
}
onSubscribeFirst() {
this._sourceSubscription = this._source.subscribe(this);
}
onUnsubscribeLast() {
this._sourceSubscription();
this._sourceSubscription = null;
}
onRemove(key, value) {}
onAdd(key, value) {}
onChange(key, value, params) {}
onReset() {}
}

View file

@ -0,0 +1,55 @@
import Operator from "../Operator.js";
export default class FilterOperator extends Operator {
constructor(source, mapper, updater) {
super(source);
this._mapper = mapper;
this._updater = updater;
this._mappedValues = new Map();
}
onAdd(key, value) {
const mappedValue = this._mapper(value);
this._mappedValues.set(key, mappedValue);
this.emitAdd(key, mappedValue);
}
onRemove(key, _value) {
const mappedValue = this._mappedValues.get(key);
if (this._mappedValues.delete(key)) {
this.emitRemove(key, mappedValue);
}
}
onChange(key, value, params) {
const mappedValue = this._mappedValues.get(key);
if (mappedValue !== undefined) {
const newParams = this._updater(value, params);
if (newParams !== undefined) {
this.emitChange(key, mappedValue, newParams);
}
}
}
onSubscribeFirst() {
for (let [key, value] of this._source) {
const mappedValue = this._mapper(value);
this._mappedValues.set(key, mappedValue);
}
super.onSubscribeFirst();
}
onUnsubscribeLast() {
super.onUnsubscribeLast();
this._mappedValues.clear();
}
onReset() {
this._mappedValues.clear();
this.emitReset();
}
[Symbol.iterator]() {
return this._mappedValues.entries()[Symbol.iterator];
}
}

View file

@ -0,0 +1,55 @@
import Operator from "../Operator.js";
export default class MapOperator extends Operator {
constructor(source, mapper, updater) {
super(source);
this._mapper = mapper;
this._updater = updater;
this._mappedValues = new Map();
}
onAdd(key, value) {
const mappedValue = this._mapper(value);
this._mappedValues.set(key, mappedValue);
this.emitAdd(key, mappedValue);
}
onRemove(key, _value) {
const mappedValue = this._mappedValues.get(key);
if (this._mappedValues.delete(key)) {
this.emitRemove(key, mappedValue);
}
}
onChange(key, value, params) {
const mappedValue = this._mappedValues.get(key);
if (mappedValue !== undefined) {
const newParams = this._updater(value, params);
if (newParams !== undefined) {
this.emitChange(key, mappedValue, newParams);
}
}
}
onSubscribeFirst() {
for (let [key, value] of this._source) {
const mappedValue = this._mapper(value);
this._mappedValues.set(key, mappedValue);
}
super.onSubscribeFirst();
}
onUnsubscribeLast() {
super.onUnsubscribeLast();
this._mappedValues.clear();
}
onReset() {
this._mappedValues.clear();
this.emitReset();
}
[Symbol.iterator]() {
return this._mappedValues.entries()[Symbol.iterator];
}
}

View file

@ -0,0 +1,119 @@
import Operator from "../Operator.js";
/**
* @license
* Based off baseSortedIndex function in Lodash <https://lodash.com/>
* Copyright JS Foundation and other contributors <https://js.foundation/>
* Released under MIT license <https://lodash.com/license>
* Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE>
* Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
*/
function sortedIndex(array, value, comparator) {
let low = 0;
let high = array.length;
while (low < high) {
let mid = (low + high) >>> 1;
let cmpResult = comparator(value, array[mid]);
if (cmpResult > 0) {
low = mid + 1;
} else if (cmpResult < 0) {
high = mid;
} else {
low = high = mid;
}
}
return high;
}
// TODO: this should not inherit from an BaseObservableMap, as it's a list
export default class SortOperator extends Operator {
constructor(sourceMap, comparator) {
super(sourceMap);
this._comparator = comparator;
this._sortedValues = [];
this._keyIndex = new Map();
}
onAdd(key, value) {
const idx = sortedIndex(this._sortedValues, value, this._comparator);
this._sortedValues.splice(idx, 0, value);
this._keyIndex.set(key, idx);
this.emitAdd(idx, value);
}
onRemove(key, _value) {
const idx = sortedIndex(this._sortedValues, value, this._comparator);
this._sortedValues.splice(idx, 0, value);
this._keyIndex.set(key, idx);
this.emitAdd(idx, value);
}
onChange(key, value, params) {
// index could have moved if other items got added in the meantime
const oldIdx = this._keyIndex.get(key);
this._sortedValues.splice(oldIdx, 1);
const idx = sortedIndex(this._sortedValues, value, this._comparator);
}
onSubscribeFirst() {
this._sortedValues = new Array(this._source.size);
let i = 0;
for (let [key, value] of this._source) {
this._sortedValues[i] = value;
this._keyIndex.set(key, i);
++i;
}
this._sortedValues.sort(this._comparator);
super.onSubscribeFirst();
}
onUnsubscribeLast() {
super.onUnsubscribeLast();
this._sortedValues = null;
}
onReset() {
this._sortedValues = [];
this.emitReset();
}
get length() {
return this._source.size;
}
[Symbol.iterator]() {
return this._sortedValues;
}
}
//#ifdef TESTS
export function tests() {
return {
test_sortIndex(assert) {
let idx = sortedIndex([1, 5, 6, 8], 0, (a, b) => a - b);
assert.equal(idx, 0);
idx = sortedIndex([1, 5, 6, 8], 3, (a, b) => a - b);
assert.equal(idx, 1);
idx = sortedIndex([1, 5, 6, 8], 8, (a, b) => a - b);
assert.equal(idx, 3);
},
test_sortIndex_reverse(assert) {
let idx = sortedIndex([8, 6, 5, 1], 6, (a, b) => b - a);
assert.equal(idx, 1);
},
test_sortIndex_likeArraySort(assert) {
const a = [5, 1, 8, 2];
const cmp = (a, b) => a - b;
a.sort(cmp);
assert.deepEqual(a, [1, 2, 5, 8]);
let idx = sortedIndex(a, 2, cmp);
assert.equal(idx, 1);
}
}
}
//#endif

View file

@ -1,6 +1,4 @@
import EventEmitter from "./event-emitter.js";
class LiveMap {
class ObservableMap {
constructor() {
this._handlers = new Set();
}
@ -30,42 +28,45 @@ class LiveMap {
}
}
onSubscribeFirst() {
}
onUnsubscribeLast() {
}
subscribe(handler) {
this._handlers.add(handler);
if (this._handlers.length === 1) {
this.onSubscribeFirst();
}
return () => {
if (handler) {
this._handlers.delete(this._handler);
if (this._handlers.length === 0) {
this.onUnsubscribeLast();
}
handler = null;
}
return null;
};
}
[Symbol.iterator]() {
}
}
class Operator extends LiveMap {
class Operator extends ObservableMap {
constructor(source) {
super();
this._source = source;
}
subscribe(handler) {
this.onSubscribe(this._source);
let subscription = super.subscribe(handler);
let sourceSubscription = this._source.subscribe(this);
return () => {
sourceSubscription = sourceSubscription && sourceSubscription();
subscription = subscription && subscription();
// this.onUnsubscribe(); ?
return null;
};
onSubscribeFirst() {
this._sourceSubscription = this._source.subscribe(this);
}
onSubscribe() {
onUnsubscribeLast() {
this._sourceSubscription();
this._sourceSubscription = null;
}
onRemove(key, value) {}
@ -74,7 +75,7 @@ class Operator extends LiveMap {
onReset() {}
}
export default class LiveMapCollection extends LiveMap {
export default class ObservableMapCollection extends ObservableMap {
constructor(initialValues) {
super();
this._values = new Map(initialValues);
@ -124,7 +125,7 @@ export default class LiveMapCollection extends LiveMap {
}
}
class LiveMapOperator extends Operator {
class ObservableMapOperator extends Operator {
constructor(source, mapper, updater) {
super(source);
this._mapper = mapper;
@ -132,13 +133,6 @@ class LiveMapOperator extends Operator {
this._mappedValues = new Map();
}
onSubscribe(source) {
for (let [key, value] of source) {
const mappedValue = this._mapper(value);
this._mappedValues.set(key, mappedValue);
}
}
onAdd(key, value) {
const mappedValue = this._mapper(value);
this._mappedValues.set(key, mappedValue);
@ -162,6 +156,19 @@ class LiveMapOperator extends Operator {
}
}
onSubscribeFirst() {
for (let [key, value] of this._source) {
const mappedValue = this._mapper(value);
this._mappedValues.set(key, mappedValue);
}
super.onSubscribeFirst();
}
onUnsubscribeLast() {
super.onUnsubscribeLast();
this._mappedValues.clear();
}
onReset() {
this._mappedValues.clear();
this.emitReset();
@ -172,12 +179,12 @@ class LiveMapOperator extends Operator {
}
}
class FilterOperator extends LiveMapOperator {
class FilterOperator extends ObservableMapOperator {
}
class SortSet {
constructor(liveMap) {
constructor(ObservableMap) {
}
}

78
src/ui/web/ListView.js Normal file
View file

@ -0,0 +1,78 @@
import * as html from "./html.js";
class UIView {
mount(initialValue) {
}
unmount() {
}
update() {
}
}
export default class ListView {
constructor(collection, childCreator) {
this._collection = collection;
this._root = null;
this._subscription = null;
this._childCreator = childCreator;
this._childInstances = null;
}
getDOMNode() {
return this._root;
}
mount() {
this._subscription = this._collection.subscribe(this);
this._root = html.ul({className: "ListView"});
this._childInstances = new Array(this._collection.length);
for (let item of this._collection) {
const child = this._childCreator(item);
this._childInstances.push(child);
const childDomNode = child.mount();
this._root.appendChild(childDomNode);
}
return this._root;
}
unmount() {
this._subscription = this._subscription();
for (let child of this._childInstances) {
child.unmount();
}
this._childInstances = null;
}
onAdd(i, value) {
const child = this._childCreator(value);
const childDomNode = child.mount();
this._childInstances.splice(i, 0, child);
const isLast = i === this._collection.length - 1;
if (isLast) {
this._root.appendChild(childDomNode);
} else {
const nextDomNode = this._childInstances[i + 1].getDOMNode();
this._root.insertBefore(childDomNode, nextDomNode);
}
}
onRemove(i, _value) {
const [child] = this._childInstances.splice(i, 1);
child.getDOMNode().remove();
child.unmount();
}
onMove(fromIdx, toIdx, value) {
}
onChange(i, value) {
this._childInstances[i].update(value);
}
}

43
src/ui/web/html.js Normal file
View file

@ -0,0 +1,43 @@
export function setAttribute(el, name, value) {
el.setAttribute(name, value);
}
export function el(elementName, attrs, children) {
const e = document.createElement(elementName);
if (typeof attrs === "object") {
for (let [name, value] of Object.entries(attrs)) {
setAttribute(e, name, value);
}
}
if (Array.isArray(children)) {
// TODO: use fragment here?
for (let c of children) {
e.appendChild(c);
}
}
return e;
}
export function text(str) {
return document.createTextNode(str);
}
export function ol(... params) { return el("ol", ... params); }
export function ul(... params) { return el("ul", ... params); }
export function li(... params) { return el("li", ... params); }
export function div(... params) { return el("div", ... params); }
export function h1(... params) { return el("h1", ... params); }
export function h2(... params) { return el("h2", ... params); }
export function h3(... params) { return el("h3", ... params); }
export function h4(... params) { return el("h4", ... params); }
export function h5(... params) { return el("h5", ... params); }
export function h6(... params) { return el("h6", ... params); }
export function p(... params) { return el("p", ... params); }
export function strong(... params) { return el("strong", ... params); }
export function em(... params) { return el("em", ... params); }
export function span(... params) { return el("span", ... params); }
export function img(... params) { return el("img", ... params); }
export function section(... params) { return el("section", ... params); }
export function main(... params) { return el("main", ... params); }
export function article(... params) { return el("article", ... params); }
export function aside(... params) { return el("aside", ... params); }