You've already forked matrix-js-sdk
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:
428
lib/matrix.js
428
lib/matrix.js
@@ -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') {
|
||||
|
||||
Reference in New Issue
Block a user