1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2026-01-03 23:22:30 +03:00

Merge pull request #4 from matrix-org/mxstore

Experimental data store layer to aid tracking client state
This commit is contained in:
Kegsay
2015-06-03 09:57:02 +01:00

View File

@@ -20,9 +20,13 @@ var init = function(exports){
/*
* Construct a Matrix Client.
* @param {Object} credentials The credentials for this client
* @param {Object} config The config for this client.
* @param {Object} config The config (if any) for this client.
* Valid config params include:
* noUserAgent: true // to avoid warnings whilst setting UA headers
* debug: true // to use console.err() style debugging from the lib
* @param {Object} store The data store (if any) for this client.
*/
function MatrixClient(credentials, config) {
function MatrixClient(credentials, config, store) {
if (typeof credentials === "string") {
credentials = {
"baseUrl": credentials
@@ -41,10 +45,15 @@ var init = function(exports){
}
this.config = config;
this.credentials = credentials;
this.store = store;
// track our position in the overall eventstream
this.fromToken = undefined;
this.clientRunning = false;
};
exports.MatrixClient = MatrixClient; // expose the class
exports.createClient = function(credentials, config) {
return new MatrixClient(credentials, config);
exports.createClient = function(credentials, config, store) {
return new MatrixClient(credentials, config, store);
};
var CLIENT_PREFIX = "/_matrix/client/api/v1";
@@ -52,11 +61,371 @@ var init = function(exports){
var HEADERS = {
"User-Agent": "matrix-js"
};
// Basic DAOs to abstract slightly from the line protocol and let the
// application customise events with domain-specific info
// (e.g. chat-specific semantics) if it so desires.
/*
* Construct a Matrix Event object
* @param {Object} event The raw event to be wrapped in this DAO
*/
function MatrixEvent(event) {
this.event = event || {};
};
exports.MatrixEvent = MatrixEvent;
MatrixEvent.prototype = {
getId: function() {
return this.event.event_id;
},
getSender: function() {
return this.event.user_id;
},
getType: function() {
return this.event.type;
},
getRoomId: function() {
return this.event.room_id;
},
getTs: function() {
return this.event.ts;
},
getContent: function() {
return this.event.content;
},
isState: function() {
return this.event.state_key !== undefined;
},
};
function MatrixInMemoryStore() {
this.rooms = {
// state: { },
// timeline: [ ],
};
this.presence = {
// presence objects keyed by userId
};
};
exports.MatrixInMemoryStore = MatrixInMemoryStore;
// XXX: this is currently quite procedural - we could possibly pass back
// models of Rooms, Users, Events, etc instead.
MatrixInMemoryStore.prototype = {
/*
* Add an array of one or more state MatrixEvents into the store, overwriting
* any existing state with the same {room, type, stateKey} tuple.
*/
setStateEvents: function(stateEvents) {
// we store stateEvents indexed by room, event type and state key.
for (var i = 0; i < stateEvents.length; i++) {
var event = stateEvents[i].event;
var roomId = event.room_id;
if (this.rooms[roomId] === undefined) {
this.rooms[roomId] = {};
}
if (this.rooms[roomId].state === undefined) {
this.rooms[roomId].state = {};
}
if (this.rooms[roomId].state[event.type] === undefined) {
this.rooms[roomId].state[event.type] = {};
}
this.rooms[roomId].state[event.type][event.state_key] = stateEvents[i];
}
},
/*
* Add a single state MatrixEvents into the store, overwriting
* any existing state with the same {room, type, stateKey} tuple.
*/
setStateEvent: function(stateEvent) {
this.setStateEvents([stateEvent]);
},
/*
* Return a list of MatrixEvents from the store
* @param {String} roomId the Room ID whose state is to be returned
* @param {String} type the type of the state events to be returned (optional)
* @param {String} stateKey the stateKey of the state events to be returned
* (optional, requires type to be specified)
* @return {MatrixEvent[]} an array of MatrixEvents from the store, filtered by roomid, type and state key.
*/
getStateEvents: function(roomId, type, stateKey) {
var stateEvents = [];
if (stateKey === undefined && type === undefined) {
for (type in this.rooms[roomId].state) {
if (this.rooms[roomId].state.hasOwnProperty(type)) {
for (stateKey in this.rooms[roomId].state[type]) {
if (this.rooms[roomId].state[type].hasOwnProperty(stateKey)) {
stateEvents.push(this.rooms[roomId].state[type][stateKey]);
}
}
}
}
return stateEvents;
}
else if (stateKey === undefined) {
for (stateKey in this.rooms[roomId].state[type]) {
if (this.rooms[roomId].state[type].hasOwnProperty(stateKey)) {
stateEvents.push(this.rooms[roomId].state[type][stateKey]);
}
}
return stateEvents;
}
else {
return [this.rooms[roomId].state[type][stateKey]];
}
},
/*
* Return a single state MatrixEvent from the store for the given roomId
* and type.
* @param {String} roomId the Room ID whose state is to be returned
* @param {String} type the type of the state events to be returned
* @param {String} stateKey the stateKey of the state events to be returned
* @return {MatrixEvent} a single MatrixEvent from the store, filtered by roomid, type and state key.
*/
getStateEvent: function(roomId, type, stateKey) {
return this.rooms[roomId].state[type][stateKey];
},
/*
* Adds a list of arbitrary MatrixEvents into the store.
* If the event is a state event, it is also updates state.
*/
setEvents: function(events) {
for (var i = 0; i < events.length; i++) {
var event = events[i].event;
if (event.type === "m.presence") {
this.setPresenceEvents([events[i]]);
continue;
}
var roomId = event.room_id;
if (this.rooms[roomId] === undefined) {
this.rooms[roomId] = {};
}
if (this.rooms[roomId].timeline === undefined) {
this.rooms[roomId].timeline = [];
}
if (event.state_key !== undefined) {
this.setStateEvents([events[i]]);
}
this.rooms[roomId].timeline.push(events[i]);
}
},
/*
* Get the timeline of events for a given room
* TODO: ordering!
*/
getEvents: function(roomId) {
return this.room[roomId].timeline;
},
setPresenceEvents: function(presenceEvents) {
for (var i = 0; i < presenceEvents.length; i++) {
var matrixEvent = presenceEvents[i];
this.presence[matrixEvent.event.user_id] = matrixEvent;
}
},
getPresenceEvents: function(userId) {
return this.presence[userId];
},
getRoomList: function() {
var roomIds = [];
for (var roomId in this.rooms) {
if (this.rooms.hasOwnProperty(roomId)) {
roomIds.push(roomId);
}
}
return roomIds;
},
// TODO
//setMaxHistoryPerRoom: function(maxHistory) {},
// TODO
//reapOldMessages: function() {},
};
MatrixClient.prototype = {
isLoggedIn: function() {
return this.credentials.accessToken != undefined &&
this.credentials.userId != undefined;
this.credentials.userId != undefined;
},
// Higher level APIs
// =================
// TODO: stuff to handle:
// local echo
// event dup suppression? - apparently we should still be doing this
// tracking current display name / avatar per-message
// pagination
// re-sending (including persisting pending messages to be sent)
// - Need a nice way to callback the app for arbitrary events like displayname changes
// due to ambiguity (or should this be on a chat-specific layer)?
// reconnect after connectivity outages
/*
* Helper method for retrieving the name of a room suitable for display in the UI
* TODO: in future, this should be being generated serverside.
* @param {String} roomId ID of room whose name is to be resolved
* @return {String} human-readable label for room.
*/
getFriendlyRoomName: function(roomId) {
// we need a store to track the inputs for calculating room names
if (!this.store) return roomId;
// check for an alias, if any. for now, assume first alias is the official one.
var alias;
var mRoomAliases = this.store.getStateEvents(roomId, 'm.room.aliases')[0];
if (mRoomAliases) {
alias = mRoomAliases.event.content.aliases[0];
}
var mRoomName = this.store.getStateEvent(roomId, 'm.room.name', '');
if (mRoomName) {
return mRoomName.event.content.name + (alias ? " (" + alias + ")": "");
}
else if (alias) {
return alias;
}
else {
var userId = this.credentials.userId;
var members = this.store.getStateEvents(roomId, 'm.room.member')
.filter(function(event) {
return event.event.user_id !== userId;
});
if (members.length == 0) {
return "Unknown";
}
else if (members.length == 1) {
return members[0].event.content.displayname || members[0].event.user_id;
}
else if (members.length == 2) {
return (members[0].event.content.displayname || members[0].event.user_id) + " and " +
(members[1].event.content.displayname || members[1].event.user_id);
}
else {
return (members[0].event.content.displayname || members[0].event.user_id) + " and " +
(members.length - 1) + " others";
}
}
},
/*
* Helper method for retrieving the name of a user suitable for display in the UI
* in the context of a room - i.e. disambiguating from any other users in the room.
* XXX: This could perhaps also be generated serverside, perhaps by just passing
* a 'disambiguate' flag down on membership entries which have ambiguous displaynames?
* @param {String} userId ID of the user whose name is to be resolved
* @param {String} roomId ID of room to be used as the context for resolving the name
* @return {String} human-readable name of the user.
*/
getFriendlyDisplayName: function(userId, roomId) {
// we need a store to track the inputs for calculating display names
if (!this.store) return userId;
var displayName;
var memberEvent = this.store.getStateEvent(roomId, 'm.room.member', userId);
if (memberEvent && memberEvent.event.content.displayname) {
displayName = memberEvent.event.content.displayname;
}
else {
return userId;
}
var members = this.store.getStateEvents(roomId, 'm.room.member')
.filter(function(event) {
return event.event.content.displayname === displayName;
});
if (members.length > 1) {
return displayName + " (" + userId + ")";
}
else {
return displayName;
}
},
/*
* High level helper method to call initialSync, emit the resulting events,
* and then start polling the eventStream for new events.
* @param {function} callback Callback invoked whenever new event are available
* @param {Number} historyLen amount of historical timeline events to emit during from the initial sync
*/
startClient: function(callback, historyLen) {
historyLen = historyLen || 12;
var self = this;
if (!this.fromToken) {
this.initialSync(historyLen, function(err, data) {
if (err) {
if (this.config && this.config.debug) {
console.error("startClient error on initialSync: %s", JSON.stringify(err));
}
callback(err);
} else {
var events = []
for (var i = 0; i < data.presence.length; i++) {
events.push(new MatrixEvent(data.presence[i]));
}
for (var i = 0; i < data.rooms.length; i++) {
for (var j = 0; j < data.rooms[i].state.length; j++) {
events.push(new MatrixEvent(data.rooms[i].state[j]));
}
for (var j = 0; j < data.rooms[i].messages.chunk.length; j++) {
events.push(new MatrixEvent(data.rooms[i].messages.chunk[j]));
}
}
callback(undefined, events, false);
self.clientRunning = true;
self._pollForEvents(callback);
}
});
}
else {
this._pollForEvents(callback);
}
},
_pollForEvents: function(callback) {
var self = this;
if (!this.clientRunning) return;
this.eventStream(this.fromToken, 30000, function(err, data) {
if (err) {
if (this.config && this.config.debug) {
console.error("error polling for events via eventStream: %s", JSON.stringify(err));
}
callback(err);
// retry every few seconds
// FIXME: this should be exponential backoff with an option to nudge
setTimeout(function() {
self._pollForEvents(callback);
}, 2000);
} else {
var events = [];
for (var j = 0; j < data.chunk.length; j++) {
events.push(new MatrixEvent(data.chunk[j]));
}
callback(undefined, events, true);
self._pollForEvents(callback);
}
});
},
/*
* High level helper method to stop the client from polling and allow a clean shutdown
*/
stopClient: function() {
this.clientRunning = false;
},
// Room operations
@@ -373,8 +742,32 @@ var init = function(exports){
var params = {
limit: limit
};
var self = this;
return this._doAuthedRequest(
callback, "GET", "/initialSync", params
function(err, data) {
if (self.store) {
// intercept the results and put them into our store
self.store.setPresenceEvents(map(data.presence,
function(event) {
return new MatrixEvent(event);
}
));
for (var i = 0 ; i < data.rooms.length; i++) {
self.store.setStateEvents(map(data.rooms[i].state,
function(event) {
return new MatrixEvent(event);
}
));
self.store.setEvents(map(data.rooms[i].messages.chunk,
function(event) {
return new MatrixEvent(event);
}
));
}
}
if (data) self.fromToken = data.end;
callback(err, data); // continue with original callback
}, "GET", "/initialSync", params
);
},
@@ -420,7 +813,19 @@ var init = function(exports){
from: from,
timeout: timeout
};
return this._doAuthedRequest(callback, "GET", "/events", params);
var self = this;
return this._doAuthedRequest(
function(err, data) {
if (self.store) {
self.store.setEvents(map(data.chunk,
function(event) {
return new MatrixEvent(event);
}
));
}
if (data) self.fromToken = data.end;
callback(err, data); // continue with original callback
}, "GET", "/events", params);
},
// Registration/Login operations
@@ -431,6 +836,7 @@ var init = function(exports){
return this._doAuthedRequest(
callback, "POST", "/login", undefined, data
);
// XXX: surely we should store the results of this into our credentials
},
register: function(loginType, data, callback) {
@@ -645,6 +1051,14 @@ var init = function(exports){
var isFunction = function(value) {
return Object.prototype.toString.call(value) == "[object Function]";
};
var map = function(array, fn) {
var results = Array(array.length);
for (var i = 0; i < array.length; i++) {
results[i] = fn(array[i]);
}
return results;
};
};
if (typeof exports === 'undefined') {