Compare commits

...

38 Commits

Author SHA1 Message Date
Kegan Dougal b0ca6609f4 Merge branch 'develop' 2015-06-23 15:45:09 +01:00
Kegan Dougal ff9bc0b865 Merge branch 'release-v0.1.1' into develop 2015-06-23 15:44:47 +01:00
Kegan Dougal b3ef0b90ac Update README 2015-06-23 15:44:24 +01:00
Kegan Dougal 805d914940 Bump to 0.1.1. Add 'npm run release' script. 2015-06-23 15:42:08 +01:00
Kegsay de560d1a4a Update CHANGELOG.md 2015-06-23 15:27:18 +01:00
Kegan Dougal 07d3f43d8b Refactor terminal app; add /invite and /roominfo. 2015-06-23 15:22:57 +01:00
Kegan Dougal 57914969b8 Add MatrixClient.resendEvent to manually resend an event that was not sent.
Bundle txnId as MatrixEvent._txnId instead of exposing it to every place that
happens to need it (since it's so tightly coupled with MatrixEvent)
2015-06-23 14:08:02 +01:00
Kegan Dougal 378c7fd6cb Update docs 2015-06-23 12:15:41 +01:00
Kegan Dougal 7534f59af2 Hook up the callback fn for scrollback. Update CHANGELOG. 2015-06-23 12:04:43 +01:00
Kegan Dougal 553325d66f Improve UX on node terminal app.
Add auto-complete, history and a prompt prefix.
2015-06-23 11:50:17 +01:00
Kegan Dougal 6737388766 Implement pagination via the scrollback() function. Update CHANGELOG. 2015-06-23 11:36:30 +01:00
Kegan Dougal b7ed78b432 Update CHANGELOG. Add StubStore.
Default to a no-op store class to prevent having to constantly check for
MatrixClient.store and other defensive checks which clutters the intent
of the code.
2015-06-23 11:03:08 +01:00
Kegan Dougal 9c2a2902eb Fix bug where hibernating laptops fail to restart the event stream.
On some devices, the act of hibernating black holes the request so the
success/failure callbacks never fire. This prevents a re-poll as that is done
in the callbacks. To fix this, we add a local timer which, when it fires,
will forcibly do a re-poll. The local timer has a timeout value several
seconds more than the timeout= value (which should've returned by then).
2015-06-23 10:40:53 +01:00
Kegan Dougal ef77c6f989 Sync room state when another device joins a room. 2015-06-23 10:18:13 +01:00
Kegan Dougal bc0e2ad504 Sync room state when joining via client.joinRoom
Does not currently sync state when another device joins.
Update node example app to refresh room list.
2015-06-22 17:50:49 +01:00
Kegan Dougal ad70b3d434 Don't set NOT_SENT in the scheduler; the MatrixClient is responsible for this. 2015-06-19 17:19:48 +01:00
Kegsay 002ae08f04 Update CHANGELOG.md 2015-06-19 17:16:38 +01:00
Kegsay 402a648a92 Update list of what the SDK does 2015-06-19 17:15:36 +01:00
Kegan Dougal 756fe4ddcb Implement MatrixScheduler. 2015-06-19 16:53:28 +01:00
Kegan Dougal 898d16ae76 Update CHANGELOG 2015-06-19 16:07:36 +01:00
Kegan Dougal 18db0d8c02 Emit 'Room' event when the Room is fully populated rather than newly created. 2015-06-19 16:05:13 +01:00
Kegan Dougal 2f78ceb6fc Design the API for the scheduler and hook MatrixClient up to it.
Scheduler itself still needs internal impl.
2015-06-19 15:50:05 +01:00
Kegan Dougal 8a9f84a4b2 Don't return something which doesn't exist. 2015-06-18 17:49:13 +01:00
Kegan Dougal 1e3162128a Shelve scheduler queue work. Add processFn. 2015-06-18 13:56:46 +01:00
Kegan Dougal 7e12ea4273 Start gluing MatrixClient and MatrixScheduler. 2015-06-18 12:07:18 +01:00
Kegan Dougal 18a3ce415c Add _queues to MatrixScheduler. 2015-06-18 11:49:34 +01:00
Kegan Dougal ca414d1611 Implement rate limiting in the default retry algorithm. 2015-06-18 11:28:17 +01:00
Kegan Dougal ba56f5f621 Implement MatrixScheduler.
Formed of two parts: the retry and queuing functions. Use sensible defaults.
2015-06-18 11:11:07 +01:00
Kegan Dougal 4810d04257 Add stub MatrixScheduler. 2015-06-18 10:07:44 +01:00
Kegsay 8d77edce86 Formatting 2015-06-18 10:01:13 +01:00
Kegan Dougal 9cb37fbe4f Actually allow MatrixClient to not have a store. Update jsdoc. 2015-06-18 09:59:02 +01:00
Kegan Dougal d151ac49f2 Mark events which fail to send. 2015-06-18 09:32:04 +01:00
Kegan Dougal d74a71cc2d linting and update README. 2015-06-17 17:47:57 +01:00
Kegan Dougal 8e6eb35dfe Implement local echo.
Mark events being sent via the status property. Update CHANGELOG.
2015-06-17 17:42:12 +01:00
Kegan Dougal ee4d66024a Update changelog 2015-06-15 10:20:54 +01:00
Kegan Dougal f77e8e3bf7 Add User.events.presence property for storing the m.presence event for a user. 2015-06-15 10:20:04 +01:00
Kegan Dougal 94931cdc07 Remove broken function; add RoomMember.events.member property. 2015-06-15 09:49:54 +01:00
Kegan Dougal c6b11fc382 Make Room.calculateRoomName private to avoid confusion with Room.name. 2015-06-15 09:31:46 +01:00
18 changed files with 7547 additions and 179 deletions
+51
View File
@@ -0,0 +1,51 @@
Changes in 0.1.1
================
Breaking changes:
* `Room.calculateRoomName` is now private. Use `Room.recalculate` instead, and
access the calculated name via `Room.name`.
* `new MatrixClient(...)` no longer creates a `MatrixInMemoryStore` if
`opts.store` is not specified. Instead, the `createClient` global function
creates it and passes it to the constructor. This change will not affect
users who have always used `createClient` to create a `MatrixClient`.
* `"Room"` events will now be emitted when the Room has *finished* being
populated with state rather than at the moment of creation. This will fire
when the SDK encounters a room it doesn't know about (just arrived from the
event stream; e.g. a room invite) and will also fire after syncing room
state (e.g. after calling joinRoom).
* `MatrixClient.joinRoom` now returns a `Room` object when resolved, not an
object with a `room_id` property.
* `MatrixClient.scrollback` now expects a `Room` arg instead of a `room_id`
and `from` token. Construct a `new Room(roomId)` if you want to continue
using this directly, then set the pagination token using
`room.oldState.paginationToken = from`. It now resolves to a `Room` object
instead of the raw HTTP response.
New properties:
* `User.events`
* `RoomMember.events`
New methods:
* `Room.hasMembershipState(userId, membership)`
* `MatrixClient.resendEvent(event, room)`
New features:
* Local echo. When you send an event using the SDK it will immediately be
added to `Room.timeline` with the `event.status` of `EventStatus.SENDING`.
When the event is finally sent, this status will be removed.
* Not sent status. When an event fails to send using the SDK, it will have the
`event.status` of `EventStatus.NOT_SENT`.
* Retries. If events fail to send, they will be automatically retried.
* Manual resending. Events which failed to send can be passed to
`MatrixClient.resendEvent(event, room)` to resend them.
* Queueing. Messages sent in quick succession will be queued to preserve the
order in which they were submitted.
* Room state is automatcally synchronised when joining a room (including if
another device joins a room).
* Scrollback. You can request earlier events in a room using
`MatrixClient.scrollback(room, limit, callback)`.
Bug fixes:
* Fixed a bug which prevented the event stream from polling. Some devices will
black hole requests when they hibernate, meaning that the callbacks will
never fire. We now maintain a local timer to forcibly restart the request.
+10 -6
View File
@@ -10,7 +10,7 @@ Quickstart
In a browser
------------
Copy ``dist/browser-matrix-$VERSION.js`` and add that as a ``<script>`` to
Copy ``dist/$VERSION/browser-matrix-$VERSION.js`` and add that as a ``<script>`` to
your page. There will be a global variable ``matrixcs`` attached to
``window`` through which you can access the SDK.
@@ -44,14 +44,18 @@ events for incoming data and state changes. Aside from wrapping the HTTP API, it
- Exposes high-level objects like `Rooms`, `RoomState`, `RoomMembers` and `Users`
which can be listened to for things like name changes, new messages, membership
changes, presence changes, and more.
Later versions of the SDK will:
- Handle "local echo" of messages sent using the SDK. This means that messages
that have just been sent will appear in the timeline as 'sending', until it
completes. This is beneficial because it prevents there being a gap between
hitting the send button and having the "remote echo" arrive.
- Mark messages which failed to send as not sent.
- Automatically retry requests to send messages due to network errors.
- Automatically retry requests to send messages due to rate limiting errors.
- Mark events' sent status (e.g. 'not sent').
- Handle "local echo" of messages sent.
- Handle queueing of messages.
- Handle pagination.
- Handles pagination.
- Handles room initial sync on accepting invites.
Later versions of the SDK will:
- Expose a `RoomSummary` which would be suitable for a recents page.
- Provide different pluggable storage layers (e.g. local storage, database-backed)
+6490
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+192 -48
View File
@@ -3,6 +3,7 @@
var myUserId = "@example:localhost";
var myAccessToken = "QGV4YW1wbGU6bG9jYWxob3N0.qPEvLuYfNBjxikiCjP";
var sdk = require("matrix-js-sdk");
var clc = require("cli-color");
var matrixClient = sdk.createClient({
baseUrl: "http://localhost:8008",
accessToken: myAccessToken,
@@ -15,46 +16,106 @@ var viewingRoom = null;
var numMessagesToShow = 20;
// Reading from stdin
var CLEAR_CONSOLE = '\x1B[2J';
var readline = require("readline");
var rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: false
completer: completer
});
rl.setPrompt("$ ");
rl.on('line', function(line) {
if (line.indexOf("/enter ") === 0 && !viewingRoom) {
var roomIndex = line.split(" ")[1];
viewingRoom = roomList[roomIndex];
if (viewingRoom.getMember(myUserId).membership === "invite") {
// join the room first
matrixClient.joinRoom(viewingRoom.roomId).done(function() {
if (line.trim().length === 0) {
rl.prompt();
return;
}
if (line === "/help") {
printHelp();
rl.prompt();
return;
}
if (viewingRoom) {
if (line === "/exit") {
viewingRoom = null;
printRoomList();
}
else if (line === "/members") {
printMemberList(viewingRoom);
}
else if (line === "/roominfo") {
printRoomInfo(viewingRoom);
}
else if (line === "/resend") {
// get the oldest not sent event.
var notSentEvent;
for (var i = 0; i < viewingRoom.timeline.length; i++) {
if (viewingRoom.timeline[i].status == sdk.EventStatus.NOT_SENT) {
notSentEvent = viewingRoom.timeline[i];
break;
}
}
if (notSentEvent) {
matrixClient.resendEvent(notSentEvent, viewingRoom).done(function() {
printMessages();
rl.prompt();
}, function(err) {
printMessages();
print("/resend Error: %s", err);
rl.prompt();
});
printMessages();
rl.prompt();
}
}
else if (line.indexOf("/more ") === 0) {
var amount = parseInt(line.split(" ")[1]) || 20;
matrixClient.scrollback(viewingRoom, amount).done(function(room) {
printMessages();
rl.prompt();
}, function(err) {
console.log("Error: %s", err);
print("/more Error: %s", err);
});
}
else if (line.indexOf("/invite ") === 0) {
var userId = line.split(" ")[1].trim();
matrixClient.invite(viewingRoom.roomId, userId).done(function() {
printMessages();
rl.prompt();
}, function(err) {
print("/invite Error: %s", err);
});
}
else {
matrixClient.sendTextMessage(viewingRoom.roomId, line).finally(function() {
printMessages();
rl.prompt();
});
// print local echo immediately
printMessages();
}
}
else if (line === "/exit" && viewingRoom) {
viewingRoom = null;
printRoomList();
}
else if (line === "/members" && viewingRoom) {
printMemberList();
}
else if (line === "/help") {
printHelp();
}
else if (viewingRoom) {
matrixClient.sendTextMessage(viewingRoom.roomId, line).done(function() {
console.log('\x1B[2J'); // clear console
printMessages();
}, function(err) {
console.log("Error: %s", err);
});
else {
if (line.indexOf("/join ") === 0) {
var roomIndex = line.split(" ")[1];
viewingRoom = roomList[roomIndex];
if (viewingRoom.getMember(myUserId).membership === "invite") {
// join the room first
matrixClient.joinRoom(viewingRoom.roomId).done(function(room) {
roomList = matrixClient.getRooms();
viewingRoom = room;
printMessages();
rl.prompt();
}, function(err) {
print("/join Error: %s", err);
});
}
else {
printMessages();
}
}
}
rl.prompt();
});
// ==== END User input
@@ -63,6 +124,15 @@ matrixClient.on("syncComplete", function() {
roomList = matrixClient.getRooms();
printRoomList();
printHelp();
rl.prompt();
});
matrixClient.on("Room", function() {
roomList = matrixClient.getRooms();
if (!viewingRoom) {
printRoomList();
rl.prompt();
}
});
// print incoming messages.
@@ -77,9 +147,9 @@ matrixClient.on("Room.timeline", function(event, room, toStartOfTimeline) {
});
function printRoomList() {
console.log("Room List:");
print("Room List:");
for (var i = 0; i < roomList.length; i++) {
console.log(
print(
"[%s] %s (%s members)",
i, roomList[i].name, roomList[i].getJoinedMembers().length
);
@@ -87,13 +157,27 @@ function printRoomList() {
}
function printHelp() {
console.log("Global commands:");
console.log(" '/help' : Show this help.");
console.log("Room list index commands:");
console.log(" '/enter <index>' Enter a room, e.g. '/enter 5'");
console.log("Room commands:");
console.log(" '/exit' Return to the room list index.");
console.log(" '/members' Show the room member list.");
var hlp = clc.italic;
print("Global commands:", hlp);
print(" '/help' : Show this help.");
print("Room list index commands:", hlp);
print(" '/join <index>' Join a room, e.g. '/join 5'");
print("Room commands:", hlp);
print(" '/exit' Return to the room list index.");
print(" '/members' Show the room member list.");
print(" '/invite @foo:bar' Invite @foo:bar to the room.");
print(" '/more 15' Scrollback 15 events");
print(" '/resend' Resend the oldest event which failed to send.");
print(" '/roominfo' Display room info e.g. name, topic.");
}
function completer(line) {
var completions = [
"/help", "/join ", "/exit", "/members", "/more ", "/resend", "/invite"
];
var hits = completions.filter(function(c) { return c.indexOf(line) == 0 });
// show all completions if none found
return [hits.length ? hits : completions, line]
}
function printMessages() {
@@ -101,19 +185,15 @@ function printMessages() {
printRoomList();
return;
}
console.log('\x1B[2J'); // clear console
var mostRecentMessages = viewingRoom.timeline.slice(numMessagesToShow * -1);
print(CLEAR_CONSOLE);
var mostRecentMessages = viewingRoom.timeline;
for (var i = 0; i < mostRecentMessages.length; i++) {
printLine(mostRecentMessages[i]);
}
}
function printMemberList() {
if (!viewingRoom) {
printRoomList();
return;
}
var members = viewingRoom.currentState.getMembers();
function printMemberList(room) {
var members = room.currentState.getMembers();
// sorted based on name.
members.sort(function(a, b) {
if (a.name > b.name) {
@@ -124,23 +204,54 @@ function printMemberList() {
}
return 0;
});
console.log("Membership list for room \"%s\"", viewingRoom.name);
console.log(new Array(viewingRoom.name.length + 28).join("-"));
viewingRoom.currentState.getMembers().forEach(function(member) {
print("Membership list for room \"%s\"", room.name);
print(new Array(room.name.length + 28).join("-"));
room.currentState.getMembers().forEach(function(member) {
if (!member.membership) {
return;
}
var membershipWithPadding = (
member.membership + new Array(10 - member.membership.length).join(" ")
);
console.log(
print(
"%s :: %s (%s)", membershipWithPadding, member.name,
(member.userId === myUserId ? "Me" : member.userId)
);
});
}
function printRoomInfo(room) {
var eventDict = room.currentState.events;
var eTypeHeader = " Event Type(state_key) ";
var sendHeader = " Sender ";
// pad content to 100
var restCount = (
100 - "Content".length - " | ".length - " | ".length -
eTypeHeader.length - sendHeader.length
);
var padSide = new Array(Math.floor(restCount/2)).join(" ");
var contentHeader = padSide + "Content" + padSide;
print(eTypeHeader+sendHeader+contentHeader);
print(new Array(100).join("-"));
Object.keys(eventDict).forEach(function(eventType) {
if (eventType === "m.room.member") { return; } // use /members instead.
Object.keys(eventDict[eventType]).forEach(function(stateKey) {
var typeAndKey = eventType + (
stateKey.length > 0 ? "("+stateKey+")" : ""
);
var typeStr = fixWidth(typeAndKey, eTypeHeader.length);
var event = eventDict[eventType][stateKey];
var sendStr = fixWidth(event.getSender(), sendHeader.length);
var contentStr = fixWidth(
JSON.stringify(event.getContent()), contentHeader.length
);
print(typeStr+" | "+sendStr+" | "+contentStr);
});
})
}
function printLine(event) {
var fmt;
var name = event.sender ? event.sender.name : event.getSender();
var time = new Date(
event.getTs()
@@ -149,6 +260,14 @@ function printLine(event) {
if (event.getSender() === myUserId) {
name = "Me";
separator = ">>>";
if (event.status === sdk.EventStatus.SENDING) {
separator = "...";
fmt = clc.xterm(8);
}
else if (event.status === sdk.EventStatus.NOT_SENT) {
separator = " x ";
fmt = clc.redBright;
}
}
var body = "";
@@ -177,7 +296,32 @@ function printLine(event) {
);
separator = "---";
}
console.log("[%s] %s %s %s", time, name, separator, body);
if (fmt) {
print(
"[%s] %s %s %s", fmt(time), fmt(name), fmt(separator), fmt(body)
);
}
else {
print("[%s] %s %s %s", time, name, separator, body);
}
}
function print(str, formatter) {
if (arguments.length == 2 && typeof arguments[1] === "function") {
console.log(arguments[1](str));
return;
}
console.log.apply(console.log, arguments);
}
function fixWidth(str, len) {
if (str.length > len) {
return str.substr(0, len-2) + "\u2026";
}
else if (str.length < len) {
return str + new Array(len - str.length).join(" ");
}
return str;
}
matrixClient.startClient(numMessagesToShow); // messages for each room.
+4 -1
View File
@@ -7,5 +7,8 @@
"preinstall": "npm install ../.."
},
"author": "",
"license": "Apache 2.0"
"license": "Apache 2.0",
"dependencies": {
"cli-color": "^1.0.0"
}
}
+303 -79
View File
@@ -4,36 +4,55 @@
* @module client
*/
var EventEmitter = require("events").EventEmitter;
var q = require("q");
var httpApi = require("./http-api");
var MatrixEvent = require("./models/event").MatrixEvent;
var EventStatus = require("./models/event").EventStatus;
var StubStore = require("./store/stub");
var Room = require("./models/room");
var User = require("./models/user");
var MatrixInMemoryStore = require("./store/memory").MatrixInMemoryStore;
var utils = require("./utils");
// TODO:
// Internal: rate limiting
/**
* Construct a Matrix Client.
* Construct a Matrix Client. Only directly construct this if you want to use
* custom modules. Normally, {@link createClient} should be used
* as it specifies 'sensible' defaults for these modules.
* @constructor
* @extends {external:EventEmitter}
* @param {Object} opts The configuration options for this client.
* @param {string} opts.baseUrl Required. The base URL to the client-server HTTP API.
* @param {Function} opts.request Required. The function to invoke for HTTP requests.
* @param {string} opts.baseUrl Required. The base URL to the client-server
* HTTP API.
* @param {Function} opts.request Required. The function to invoke for HTTP
* requests. The value of this property is typically <code>require("request")
* </code> as it returns a function which meets the required interface. See
* {@link requestFunction} for more information.
* @param {string} opts.accessToken The access_token for this user.
* @param {string} opts.userId The user ID for this user.
* @param {Object} opts.store Optional. The data store to use. Defaults to
* {@link module:store/memory.MatrixInMemoryStore}.
* @param {Object} opts.store Optional. The data store to use. If not specified,
* this client will not store any HTTP responses.
* @param {Object} opts.scheduler Optional. The scheduler to use. If not
* specified, this client will not retry requests on failure. This client
* will supply its own processing function to
* {@link module:scheduler~MatrixScheduler#setProcessFunction}.
*/
function MatrixClient(opts) {
utils.checkObjectHasKeys(opts, ["baseUrl", "request"]);
utils.checkObjectHasNoAdditionalKeys(opts,
["baseUrl", "request", "accessToken", "userId", "store"]
["baseUrl", "request", "accessToken", "userId", "store", "scheduler"]
);
this.store = opts.store || new MatrixInMemoryStore();
this.store = opts.store || new StubStore();
this.scheduler = opts.scheduler;
if (this.scheduler) {
var self = this;
this.scheduler.setProcessFunction(function(eventToSend) {
return _sendEventHttpRequest(self, eventToSend);
});
}
// track our position in the overall eventstream
this.fromToken = undefined;
this.clientRunning = false;
@@ -49,6 +68,9 @@ function MatrixClient(opts) {
userId: (opts.userId || null)
};
this._http = new httpApi.MatrixHttpApi(httpOpts);
this._syncingRooms = {
// room_id: Promise
};
}
utils.inherits(MatrixClient, EventEmitter);
@@ -58,9 +80,6 @@ utils.inherits(MatrixClient, EventEmitter);
* @return {Room} The Room or null if it doesn't exist or there is no data store.
*/
MatrixClient.prototype.getRoom = function(roomId) {
if (!this.store) {
return null;
}
return this.store.getRoom(roomId);
};
@@ -69,9 +88,6 @@ MatrixClient.prototype.getRoom = function(roomId) {
* @return {Room[]} A list of rooms, or an empty list if there is no data store.
*/
MatrixClient.prototype.getRooms = function() {
if (!this.store) {
return [];
}
return this.store.getRooms();
};
@@ -103,12 +119,39 @@ MatrixClient.prototype.createRoom = function(options, callback) {
* Join a room.
* @param {string} roomIdOrAlias The room ID or room alias to join.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: <code>{room_id: {string}}</code>
* @return {module:client.Promise} Resolves: Room object.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.joinRoom = function(roomIdOrAlias, callback) {
var path = utils.encodeUri("/join/$roomid", { $roomid: roomIdOrAlias});
return this._http.authedRequest(callback, "POST", path, undefined, {});
var defer = q.defer();
var self = this;
this._http.authedRequest(undefined, "POST", path, undefined, {}).then(
function(res) {
var roomId = res.room_id;
var room = createNewRoom(self, roomId);
return _syncRoom(self, room);
}, function(err) {
_reject(callback, defer, err);
}).done(function(room) {
_resolve(callback, defer, room);
}, function(err) {
_reject(callback, defer, err);
});
return defer.promise;
};
/**
* Resend an event.
* @param {MatrixEvent} event The event to resend.
* @param {Room} room Optional. The room the event is in. Will update the
* timeline entry if provided.
* @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.resendEvent = function(event, room) {
event.status = EventStatus.SENDING;
return _sendEvent(this, room, event);
};
/**
@@ -223,21 +266,109 @@ MatrixClient.prototype.sendStateEvent = function(roomId, eventType, content, sta
MatrixClient.prototype.sendEvent = function(roomId, eventType, content, txnId,
callback) {
if (utils.isFunction(txnId)) { callback = txnId; txnId = undefined; }
if (!txnId) {
txnId = "m" + new Date().getTime();
}
var path = utils.encodeUri("/rooms/$roomId/send/$eventType/$txnId", {
$roomId: roomId,
$eventType: eventType,
$txnId: txnId
// we always construct a MatrixEvent when sending because the store and
// scheduler use them. We'll extract the params back out if it turns out
// the client has no scheduler or store.
var room = this.getRoom(roomId);
var localEvent = new MatrixEvent({
event_id: "~" + roomId + ":" + txnId,
user_id: this.credentials.userId,
room_id: roomId,
type: eventType,
origin_server_ts: new Date().getTime(),
content: content
});
return this._http.authedRequest(
callback, "PUT", path, undefined, content
);
localEvent._txnId = txnId;
// add this event immediately to the local store as 'sending'.
if (room) {
localEvent.status = EventStatus.SENDING;
room.addEventsToTimeline([localEvent]);
}
return _sendEvent(this, room, localEvent, callback);
};
function _sendEvent(client, room, event, callback) {
var defer = q.defer();
var promise;
// this event may be queued
if (client.scheduler) {
// if this returns a promsie then the scheduler has control now and will
// resolve/reject when it is done. Internally, the scheduler will invoke
// processFn which is set to this._sendEventHttpRequest so the same code
// path is executed regardless.
promise = client.scheduler.queueEvent(event);
}
if (!promise) {
promise = _sendEventHttpRequest(client, event);
}
promise.done(function(res) { // the request was sent OK
if (room) {
var eventId = res.event_id;
// try to find an event with this event_id. If we find it, this is
// the echo of this event *from the event stream* so we can remove
// the fake event we made above. If we don't find it, we're still
// waiting on the fake event and so should assign the fake event
// with the real event_id for matching later.
var matchingEvent = utils.findElement(room.timeline, function(ev) {
return ev.getId() === eventId;
}, true);
if (matchingEvent) {
utils.removeElement(room.timeline, function(ev) {
return ev.getId() === event.getId();
}, true);
}
else {
event.event.event_id = res.event_id;
event.status = null;
}
}
_resolve(callback, defer, res);
}, function(err) {
// the request failed to send.
event.status = EventStatus.NOT_SENT;
_reject(callback, defer, err);
});
return defer.promise;
}
function _sendEventHttpRequest(client, event) {
var pathParams = {
$roomId: event.getRoomId(),
$eventType: event.getType(),
$stateKey: event.getStateKey(),
$txnId: event._txnId ? event._txnId : new Date().getTime()
};
var path;
if (event.isState()) {
var pathTemplate = "/rooms/$roomId/state/$eventType";
if (event.getStateKey() && event.getStateKey().length > 0) {
pathTemplate = "/rooms/$roomId/state/$eventType/$stateKey";
}
path = utils.encodeUri(pathTemplate, pathParams);
}
else {
path = utils.encodeUri(
"/rooms/$roomId/send/$eventType/$txnId", pathParams
);
}
return client._http.authedRequest(
undefined, "PUT", path, undefined, event.getContent()
);
}
/**
* @param {string} roomId
* @param {Object} content
@@ -668,25 +799,36 @@ MatrixClient.prototype.roomState = function(roomId, callback) {
};
/**
* @param {string} roomId
* @param {string} from
* @param {Number} limit
* Retrieve older messages from the given room and put them in the timeline.
* @param {Room} room The room to get older messages in.
* @param {Integer} limit Optional. The maximum number of previous events to
* pull in. Default: 30.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {module:client.Promise} Resolves: Room.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.scrollback = function(roomId, from, limit, callback) {
MatrixClient.prototype.scrollback = function(room, limit, callback) {
if (utils.isFunction(limit)) { callback = limit; limit = undefined; }
var path = utils.encodeUri("/rooms/$roomId/messages", {$roomId: roomId});
if (!limit) {
limit = 30;
}
var path = utils.encodeUri(
"/rooms/$roomId/messages", {$roomId: room.roomId}
);
var params = {
from: from,
limit: limit,
from: room.oldState.paginationToken,
limit: (limit || 30),
dir: 'b'
};
return this._http.authedRequest(callback, "GET", path, params);
var defer = q.defer();
this._http.authedRequest(callback, "GET", path, params).done(function(res) {
// res.chunk end start
room.addEventsToTimeline(
utils.map(res.chunk, _PojoToMatrixEventMapper), true
);
room.oldState.paginationToken = res.end;
_resolve(callback, defer, room);
}, function(err) {
_reject(callback, defer, err);
});
return defer.promise;
};
// Registration/Login operations
@@ -803,10 +945,6 @@ MatrixClient.prototype.isLoggedIn = function() {
};
// Higher level APIs
// =================
@@ -846,11 +984,9 @@ MatrixClient.prototype.startClient = function(historyLen) {
).done(function(data) {
var i, j;
// intercept the results and put them into our store
if (self.store) {
var eventMapper = function(event) {
return new MatrixEvent(event);
};
utils.forEach(utils.map(data.presence, eventMapper), function(e) {
if (!(self.store instanceof StubStore)) {
utils.forEach(utils.map(data.presence, _PojoToMatrixEventMapper),
function(e) {
var user = createNewUser(self, e.getContent().user_id);
user.setPresenceEvent(e);
self.store.storeUser(user);
@@ -874,27 +1010,8 @@ MatrixClient.prototype.startClient = function(historyLen) {
});
}
// "old" and "current" state are the same initially; they
// start diverging if the user paginates.
// We must deep copy otherwise membership changes in old state
// will leak through to current state!
var oldStateEvents = utils.map(
utils.deepCopy(data.rooms[i].state), eventMapper
);
var stateEvents = utils.map(data.rooms[i].state, eventMapper);
room.oldState.setStateEvents(oldStateEvents);
room.currentState.setStateEvents(stateEvents);
// add events to the timeline *after* setting the state
// events so messages use the right display names. Initial sync
// returns messages in chronological order, so we need to reverse
// it to get most recent -> oldest. We need it in that order in
// order to diverge old/current state correctly.
room.addEventsToTimeline(
utils.map(
data.rooms[i].messages ? data.rooms[i].messages.chunk : [],
eventMapper
).reverse(), true
_processRoomEvents(
room, data.rooms[i].state, data.rooms[i].messages
);
// cache the name/summary/etc prior to storage since we don't
@@ -902,6 +1019,7 @@ MatrixClient.prototype.startClient = function(historyLen) {
room.recalculate(self.credentials.userId);
self.store.storeRoom(room);
self.emit("Room", room);
}
}
@@ -948,17 +1066,29 @@ function _pollForEvents(client) {
if (!client.clientRunning) {
return;
}
var discardResult = false;
var timeoutObj = setTimeout(function() {
discardResult = true;
_pollForEvents(client);
}, 40000);
client._http.authedRequest(undefined, "GET", "/events", {
from: client.fromToken,
timeout: 30000
}).done(function(data) {
if (discardResult) {
return;
}
else {
clearTimeout(timeoutObj);
}
var events = [];
if (data) {
events = utils.map(data.chunk, function(event) {
return new MatrixEvent(event);
});
}
if (self.store) {
if (!(self.store instanceof StubStore)) {
// bucket events based on room.
var i = 0;
var roomIdToEvents = {};
@@ -974,17 +1104,38 @@ function _pollForEvents(client) {
}
// add events to room
var roomIds = utils.keys(roomIdToEvents);
for (i = 0; i < roomIds.length; i++) {
var room = self.store.getRoom(roomIds[i]);
utils.forEach(roomIds, function(roomId) {
var room = self.store.getRoom(roomId);
var isBrandNewRoom = false;
if (!room) {
// TODO: whine about this. We got an event for a room
// we don't know about (we should really be doing a
// roomInitialSync at this point to pull in state).
room = createNewRoom(self, roomIds[i]);
room = createNewRoom(self, roomId);
isBrandNewRoom = true;
}
room.addEvents(roomIdToEvents[roomIds[i]]);
var wasJoined = room.hasMembershipState(
self.credentials.userId, "join"
);
room.addEvents(roomIdToEvents[roomId], "replace");
room.recalculate(self.credentials.userId);
}
// store the Room for things like invite events so developers
// can update the UI
if (isBrandNewRoom) {
self.store.storeRoom(room);
self.emit("Room", room);
}
var justJoined = room.hasMembershipState(
self.credentials.userId, "join"
);
if (!wasJoined && justJoined) {
// we've just transitioned into a join state for this room,
// so sync state.
_syncRoom(self, room);
}
});
}
if (data) {
self.fromToken = data.end;
@@ -994,6 +1145,12 @@ function _pollForEvents(client) {
}
_pollForEvents(self);
}, function(err) {
if (discardResult) {
return;
}
else {
clearTimeout(timeoutObj);
}
self.emit("syncError", err);
// retry every few seconds
// FIXME: this should be exponential backoff with an option to nudge
@@ -1003,6 +1160,55 @@ function _pollForEvents(client) {
});
}
function _syncRoom(client, room) {
if (client._syncingRooms[room.roomId]) {
return client._syncingRooms[room.roomId];
}
var defer = q.defer();
client._syncingRooms[room.roomId] = defer.promise;
client.roomInitialSync(room.roomId, 8).done(function(res) {
room.timeline = []; // blow away any previous messages.
_processRoomEvents(room, res.state, res.messages);
room.recalculate(client.credentials.userId);
client.store.storeRoom(room);
client.emit("Room", room);
defer.resolve(room);
client._syncingRooms[room.roomId] = undefined;
}, function(err) {
defer.reject(err);
client._syncingRooms[room.roomId] = undefined;
});
return defer.promise;
}
function _processRoomEvents(room, stateEventList, messageChunk) {
// "old" and "current" state are the same initially; they
// start diverging if the user paginates.
// We must deep copy otherwise membership changes in old state
// will leak through to current state!
var oldStateEvents = utils.map(
utils.deepCopy(stateEventList), _PojoToMatrixEventMapper
);
var stateEvents = utils.map(stateEventList, _PojoToMatrixEventMapper);
room.oldState.setStateEvents(oldStateEvents);
room.currentState.setStateEvents(stateEvents);
// add events to the timeline *after* setting the state
// events so messages use the right display names. Initial sync
// returns messages in chronological order, so we need to reverse
// it to get most recent -> oldest. We need it in that order in
// order to diverge old/current state correctly.
room.addEventsToTimeline(
utils.map(
messageChunk ? messageChunk.chunk : [],
_PojoToMatrixEventMapper
).reverse(), true
);
if (messageChunk) {
room.oldState.paginationToken = messageChunk.start;
}
}
/**
* High level helper method to stop the client from polling and allow a
* clean shutdown.
@@ -1040,7 +1246,6 @@ function createNewUser(client, userId) {
function createNewRoom(client, roomId) {
var room = new Room(roomId);
reEmit(client, room, ["Room.name", "Room.timeline"]);
client.emit("Room", room); // emit created room event
// we need to also re-emit room state and room member events, so hook it up
// to the client now. We need to add a listener for RoomState.members in
@@ -1060,6 +1265,24 @@ function createNewRoom(client, roomId) {
return room;
}
function _reject(callback, defer, err) {
if (callback) {
callback(err);
}
defer.reject(err);
}
function _resolve(callback, defer, res) {
if (callback) {
callback(null, res);
}
defer.resolve(res);
}
function _PojoToMatrixEventMapper(plainOldJsObject) {
return new MatrixEvent(plainOldJsObject);
}
/** */
module.exports.MatrixClient = MatrixClient;
@@ -1097,10 +1320,11 @@ module.exports.MatrixClient = MatrixClient;
*/
/**
* Fires whenever a new Room is created. This Room will not be fully populated
* yet (e.g. no members). <strong>This event is experimental and may change.</strong>
* Fires whenever a new Room is added. This will fire when you are invited to a
* room, as well as when you join a room. <strong>This event is experimental and
* may change.</strong>
* @event module:client~MatrixClient#"Room"
* @param {Room} room The newly created room.
* @param {Room} room The newly created, fully populated room.
* @example
* matrixClient.on("Room", function(room){
* var roomId = room.roomId;
+12 -5
View File
@@ -2,6 +2,8 @@
/** The {@link module:models/event.MatrixEvent|MatrixEvent} class. */
module.exports.MatrixEvent = require("./models/event").MatrixEvent;
/** The {@link module:models/event.EventStatus|EventStatus} enum. */
module.exports.EventStatus = require("./models/event").EventStatus;
/** The {@link module:store/memory.MatrixInMemoryStore|MatrixInMemoryStore} class. */
module.exports.MatrixInMemoryStore = require("./store/memory").MatrixInMemoryStore;
/** The {@link module:http-api.MatrixHttpApi|MatrixHttpApi} class. */
@@ -18,6 +20,8 @@ module.exports.RoomMember = require("./models/room-member");
module.exports.RoomState = require("./models/room-state");
/** The {@link module:models/user~User|User} class. */
module.exports.User = require("./models/user");
/** The {@link module:scheduler~MatrixScheduler|MatrixScheduler} class. */
module.exports.MatrixScheduler = require("./scheduler");
// expose the underlying request object so different environments can use
// different request libs (e.g. request or browser-request)
@@ -33,15 +37,16 @@ module.exports.request = function(r) {
};
/**
* Construct a Matrix Client. Identical to {@link module:client.MatrixClient}
* except the 'request' option is already specified.
* Construct a Matrix Client. Similar to {@link module:client~MatrixClient}
* except that the 'request', 'store' and 'scheduler' dependencies are satisfied.
* @param {(Object|string)} opts The configuration options for this client. If
* this is a string, it is assumed to be the base URL.
* this is a string, it is assumed to be the base URL. These configuration
* options will be passed directly to {@link module:client~MatrixClient}.
* @param {string} opts.baseUrl The base URL to the client-server HTTP API.
* @param {string} opts.accessToken Optional. The access_token for this user.
* @param {string} opts.userId Optional. The user ID for this user.
* @param {Object} opts.store Optional. The data store to use. Defaults to
* {@link module:store/memory.MatrixInMemoryStore}.
* @param {Object} opts.store Set to {@link module:store/memory.MatrixInMemoryStore}.
* @param {Object} opts.scheduler Set to {@link module:scheduler~MatrixScheduler}.
* @return {MatrixClient} A new matrix client.
*/
module.exports.createClient = function(opts) {
@@ -51,6 +56,8 @@ module.exports.createClient = function(opts) {
};
}
opts.request = request;
opts.store = new module.exports.MatrixInMemoryStore();
opts.scheduler = new module.exports.MatrixScheduler();
return new module.exports.MatrixClient(opts);
};
+4 -4
View File
@@ -12,11 +12,10 @@
* @enum {string}
*/
module.exports.EventStatus = {
UNKNOWN: "unknown",
SENT: "sent",
/** The event was not sent and will no longer be retried. */
NOT_SENT: "not_sent",
SENDING: "sending",
INCOMING: "incoming"
/** The event is in the process of being sent. */
SENDING: "sending"
};
/**
@@ -44,6 +43,7 @@ module.exports.MatrixEvent = function MatrixEvent(event) {
this.forwardLooking = true;
};
module.exports.MatrixEvent.prototype = {
/**
* Get the event_id for this event.
* @return {string} The event ID, e.g. <code>$143350589368169JsLZx:localhost
+7 -10
View File
@@ -20,7 +20,8 @@ var utils = require("../utils");
* room member.
* @prop {User} user The User object for this room member, if one exists.
* @prop {string} membership The membership state for this room member e.g. 'join'.
* @throws If the event provided is not <code>m.room.member</code>
* @prop {Object} events The events describing this RoomMember.
* @prop {MatrixEvent} events.member The m.room.member event for this RoomMember.
*/
function RoomMember(roomId, userId) {
this.roomId = roomId;
@@ -31,6 +32,9 @@ function RoomMember(roomId, userId) {
this.powerLevelNorm = 0;
this.user = null;
this.membership = null;
this.events = {
member: null
};
}
utils.inherits(RoomMember, EventEmitter);
@@ -47,6 +51,8 @@ RoomMember.prototype.setMembershipEvent = function(event, roomState) {
if (event.getType() !== "m.room.member") {
return;
}
this.events.member = event;
var oldMembership = this.membership;
this.membership = event.getDirectionalContent().membership;
@@ -119,15 +125,6 @@ RoomMember.prototype.setTypingEvent = function(event) {
}
};
/**
* Get the membership state of this room member.
* @return {string} The membership state e.g. 'join'.
*/
RoomMember.prototype.getMembershipState = function() {
return this.event.getContent().membership;
};
function calculateDisplayName(member, event, roomState) {
var displayName = event.getDirectionalContent().displayname;
var selfUserId = member.userId;
+1 -1
View File
@@ -176,7 +176,7 @@ module.exports = RoomState;
* @param {RoomMember} member The room member that was updated.
* @example
* matrixClient.on("RoomState.members", function(event, state, member){
* var newMembershipState = member.getMembershipState();
* var newMembershipState = member.membership;
* });
*/
+55 -9
View File
@@ -55,6 +55,18 @@ utils.inherits(Room, EventEmitter);
});
};
/**
* Check if the given user_id has the given membership state.
* @param {string} userId The user ID to check.
* @param {string} membership The membership e.g. <code>'join'</code>
* @return {boolean} True if this user_id has the given membership state.
*/
Room.prototype.hasMembershipState = function(userId, membership) {
return utils.filter(this.currentState.getMembers(), function(m) {
return m.membership === membership && m.userId === userId;
}).length > 0;
};
/**
* Add some events to this room's timeline. Will fire "Room.timeline" for
* each event added.
@@ -104,13 +116,45 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline) {
* events and typing notifications. These events are treated as "live" so
* they will go to the end of the timeline.
* @param {MatrixEvent[]} events A list of events to add.
* @param {string} duplicateStrategy Optional. Applies to events in the
* timeline only. If this is not specified, no duplicate suppression is
* performed (this improves performance). If this is 'replace' then if a
* duplicate is encountered, the event passed to this function will replace the
* existing event in the timeline. If this is 'ignore', then the event passed to
* this function will be ignored entirely, preserving the existing event in the
* timeline. Events are identical based on their event ID <b>only</b>.
* @throws If <code>duplicateStrategy</code> is not falsey, 'replace' or 'ignore'.
*/
Room.prototype.addEvents = function(events) {
Room.prototype.addEvents = function(events, duplicateStrategy) {
if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) {
throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'");
}
for (var i = 0; i < events.length; i++) {
if (events[i].getType() === "m.typing") {
this.currentState.setTypingEvent(events[i]);
}
else {
if (duplicateStrategy) {
// is there a duplicate?
var shouldIgnore = false;
for (var j = 0; j < this.timeline.length; j++) {
if (this.timeline[j].getId() === events[i].getId()) {
if (duplicateStrategy === "replace") {
this.timeline[j] = events[i];
// skip the insert so we don't add this event twice.
// Don't break in case we replace multiple events.
shouldIgnore = true;
}
else if (duplicateStrategy === "ignore") {
shouldIgnore = true;
break; // stop searching, we're skipping the insert
}
}
}
if (shouldIgnore) {
continue; // skip the insertion of this event.
}
}
// TODO: We should have a filter to say "only add state event
// types X Y Z to the timeline".
this.addEventsToTimeline([events[i]]);
@@ -127,7 +171,7 @@ Room.prototype.addEvents = function(events) {
*/
Room.prototype.recalculate = function(userId) {
var oldName = this.name;
this.name = this.calculateRoomName(userId);
this.name = calculateRoomName(this, userId);
this.summary = new RoomSummary(this.roomId, {
title: this.name
});
@@ -138,21 +182,23 @@ Room.prototype.recalculate = function(userId) {
};
/**
* Calculates the name of the room from the current room state.
* This is an internal method. Calculates the name of the room from the current
* room state.
* @param {Room} room The matrix room.
* @param {string} userId The client's user ID. Used to filter room members
* correctly.
* @return {string} The calculated room name.
*/
Room.prototype.calculateRoomName = function(userId) {
function calculateRoomName(room, userId) {
// check for an alias, if any. for now, assume first alias is the
// official one.
var alias;
var mRoomAliases = this.currentState.getStateEvents("m.room.aliases")[0];
var mRoomAliases = room.currentState.getStateEvents("m.room.aliases")[0];
if (mRoomAliases && utils.isArray(mRoomAliases.getContent().aliases)) {
alias = mRoomAliases.getContent().aliases[0];
}
var mRoomName = this.currentState.getStateEvents('m.room.name', '');
var mRoomName = room.currentState.getStateEvents('m.room.name', '');
if (mRoomName) {
return mRoomName.getContent().name + (alias ? " (" + alias + ")" : "");
}
@@ -161,12 +207,12 @@ Room.prototype.calculateRoomName = function(userId) {
}
else {
// get members that are NOT ourselves.
var members = utils.filter(this.currentState.getMembers(), function(m) {
var members = utils.filter(room.currentState.getMembers(), function(m) {
return m.userId !== userId;
});
// TODO: Localisation
if (members.length === 0) {
var memberList = this.currentState.getMembers();
var memberList = room.currentState.getMembers();
if (memberList.length === 1) {
// we exist, but no one else... self-chat or invite.
if (memberList[0].membership === "invite") {
@@ -195,7 +241,7 @@ Room.prototype.calculateRoomName = function(userId) {
);
}
}
};
}
/**
* The Room class.
+8
View File
@@ -16,6 +16,8 @@
* @prop {string} avatarUrl The 'avatar_url' of the user if known.
* @prop {string} presence The presence enum if known.
* @prop {Number} lastActiveAgo The last time the user performed some action in ms.
* @prop {Object} events The events describing this user.
* @prop {MatrixEvent} events.presence The m.presence event for this user.
*/
function User(userId) {
this.userId = userId;
@@ -23,6 +25,10 @@ function User(userId) {
this.displayName = userId;
this.avatarUrl = null;
this.lastActiveAgo = 0;
this.events = {
presence: null,
profile: null
};
}
utils.inherits(User, EventEmitter);
@@ -39,6 +45,8 @@ User.prototype.setPresenceEvent = function(event) {
if (event.getType() !== "m.presence") {
return;
}
this.events.presence = event;
var eventsToFire = [];
if (event.getContent().presence !== this.presence) {
eventsToFire.push("User.presence");
+248
View File
@@ -0,0 +1,248 @@
"use strict";
/**
* This is an internal module which manages queuing, scheduling and retrying
* of requests.
* @module scheduler
*/
var utils = require("./utils");
var q = require("q");
var DEBUG = false; // set true to enable console logging.
/**
* Construct a scheduler for Matrix. Requires
* {@link module:scheduler~MatrixScheduler#setProcessFunction} to be provided
* with a way of processing events.
* @constructor
* @param {module:scheduler~retryAlgorithm} retryAlgorithm Optional. The retry
* algorithm to apply when determining when to try to send an event again.
* Defaults to {@link module:scheduler~MatrixScheduler.RETRY_BACKOFF_RATELIMIT}.
* @param {module:scheduler~queueAlgorithm} queueAlgorithm Optional. The queuing
* algorithm to apply when determining which events should be sent before the
* given event. Defaults to {@link module:scheduler~MatrixScheduler.QUEUE_MESSAGES}.
*/
function MatrixScheduler(retryAlgorithm, queueAlgorithm) {
this.retryAlgorithm = retryAlgorithm || MatrixScheduler.RETRY_BACKOFF_RATELIMIT;
this.queueAlgorithm = queueAlgorithm || MatrixScheduler.QUEUE_MESSAGES;
this._queues = {
// queueName: [{
// event: MatrixEvent, // event to send
// defer: Deferred, // defer to resolve/reject at the END of the retries
// attempts: Number // number of times we've called processFn
// }, ...]
};
this._activeQueues = [];
this._procFn = null;
}
/**
* Set the process function. Required for events in the queue to be processed.
* If set after events have been added to the queue, this will immediately start
* processing them.
* @param {module:scheduler~processFn} fn The function that can process events
* in the queue.
*/
MatrixScheduler.prototype.setProcessFunction = function(fn) {
this._procFn = fn;
_startProcessingQueues(this);
};
/**
* Queue an event if it is required and start processing queues.
* @param {MatrixEvent} event The event that may be queued.
* @return {?Promise} A promise if the event was queued, which will be
* resolved or rejected in due time, else null.
*/
MatrixScheduler.prototype.queueEvent = function(event) {
var queueName = this.queueAlgorithm(event);
if (!queueName) {
return null;
}
// add the event to the queue and make a deferred for it.
if (!this._queues[queueName]) {
this._queues[queueName] = [];
}
var defer = q.defer();
this._queues[queueName].push({
event: event,
defer: defer,
attempts: 0
});
debuglog(
"Queue algorithm dumped event %s into queue '%s'",
event.getId(), queueName
);
_startProcessingQueues(this);
return defer.promise;
};
/**
* Retries events up to 4 times using exponential backoff. This produces wait
* times of 2, 4, 8, and 16 seconds (30s total) after which we give up. If the
* failure was due to a rate limited request, the time specified in the error is
* waited before being retried.
* @param {MatrixEvent} event
* @param {Number} attempts
* @param {MatrixError} err
* @return {Number}
* @see module:scheduler~retryAlgorithm
*/
MatrixScheduler.RETRY_BACKOFF_RATELIMIT = function(event, attempts, err) {
if (err.name === "M_LIMIT_EXCEEDED") {
var waitTime = err.data.retry_after_ms;
if (waitTime) {
return waitTime;
}
}
if (attempts > 4) {
return -1; // give up
}
return (1000 * Math.pow(2, attempts));
};
/**
* Queues <code>m.room.message</code> events and lets other events continue
* concurrently.
* @param {MatrixEvent} event
* @return {string}
* @see module:scheduler~queueAlgorithm
*/
MatrixScheduler.QUEUE_MESSAGES = function(event) {
if (event.getType() === "m.room.message") {
// put these events in the 'message' queue.
return "message";
}
// allow all other events continue concurrently.
return null;
};
function _startProcessingQueues(scheduler) {
if (!scheduler._procFn) {
return;
}
// for each inactive queue with events in them
utils.forEach(utils.filter(utils.keys(scheduler._queues), function(queueName) {
return scheduler._activeQueues.indexOf(queueName) === -1 &&
scheduler._queues[queueName].length > 0;
}), function(queueName) {
// mark the queue as active
scheduler._activeQueues.push(queueName);
// begin processing the head of the queue
debuglog("Spinning up queue: '%s'", queueName);
_processQueue(scheduler, queueName);
});
}
function _processQueue(scheduler, queueName) {
// get head of queue
var obj = _peekNextEvent(scheduler, queueName);
if (!obj) {
// queue is empty. Mark as inactive and stop recursing.
var index = scheduler._activeQueues.indexOf(queueName);
if (index >= 0) {
scheduler._activeQueues.splice(index, 1);
}
debuglog("Stopping queue '%s' as it is now empty", queueName);
return;
}
debuglog(
"Queue '%s' has %s pending events",
queueName, scheduler._queues[queueName].length
);
// fire the process function and if it resolves, resolve the deferred. Else
// invoke the retry algorithm.
scheduler._procFn(obj.event).done(function(res) {
// remove this from the queue
_removeNextEvent(scheduler, queueName);
debuglog("Queue '%s' sent event %s", queueName, obj.event.getId());
obj.defer.resolve(res);
// keep processing
_processQueue(scheduler, queueName);
}, function(err) {
obj.attempts += 1;
// ask the retry algorithm when/if we should try again
var waitTimeMs = scheduler.retryAlgorithm(obj.event, obj.attempts, err);
debuglog(
"retry(%s) err=%s event_id=%s waitTime=%s",
obj.attempts, err, obj.event.getId(), waitTimeMs
);
if (waitTimeMs === -1) { // give up (you quitter!)
debuglog(
"Queue '%s' giving up on event %s", queueName, obj.event.getId()
);
// remove this from the queue
_removeNextEvent(scheduler, queueName);
obj.defer.reject(err);
// process next event
_processQueue(scheduler, queueName);
}
else {
setTimeout(function() {
_processQueue(scheduler, queueName);
}, waitTimeMs);
}
});
}
function _peekNextEvent(scheduler, queueName) {
var queue = scheduler._queues[queueName];
if (!utils.isArray(queue)) {
return null;
}
return queue[0];
}
function _removeNextEvent(scheduler, queueName) {
var queue = scheduler._queues[queueName];
if (!utils.isArray(queue)) {
return null;
}
return queue.shift();
}
function debuglog() {
if (DEBUG) {
console.log.apply(console, arguments);
}
}
/**
* The retry algorithm to apply when retrying events. To stop retrying, return
* <code>-1</code>. If this event was part of a queue, it will be removed from
* the queue.
* @callback retryAlgorithm
* @param {MatrixEvent} event The event being retried.
* @param {Number} attempts The number of failed attempts. This will always be
* >= 1.
* @param {MatrixError} err The most recent error message received when trying
* to send this event.
* @return {Number} The number of milliseconds to wait before trying again. If
* this is 0, the request will be immediately retried. If this is
* <code>-1</code>, the event will be marked as
* {@link module:models/event.EventStatus.NOT_SENT} and will not be retried.
*/
/**
* The queuing algorithm to apply to events. All queues created are serviced in
* a FIFO manner. To send the event ASAP, return <code>null</code> which will
* not put this event in a queue. Events that fail to send that form part of
* a queue will be removed from the queue and the next event in the queue will
* be sent.
* @callback queueAlgorithm
* @param {MatrixEvent} event The event to be sent.
* @return {string} The name of the queue to put the event into. If a queue with
* this name does not exist, it will be created. If this is <code>null</code>,
* the event is not put into a queue and will be sent concurrently.
*/
/**
* The function to invoke to process (send) events in the queue.
* @callback processFn
* @param {MatrixEvent} event The event to send.
* @return {Promise} Resolved/rejected depending on the outcome of the request.
*/
/**
* The MatrixScheduler class.
*/
module.exports = MatrixScheduler;
+73
View File
@@ -0,0 +1,73 @@
"use strict";
/**
* This is an internal module.
* @module store/stub
*/
/**
* Construct a stub store. This does no-ops on all store methods.
* @constructor
*/
function StubStore() {
}
StubStore.prototype = {
/**
* No-op.
* @param {Room} room
*/
storeRoom: function(room) {
},
/**
* No-op.
* @param {string} roomId
* @return {null}
*/
getRoom: function(roomId) {
return null;
},
/**
* No-op.
* @return {Array} An empty array.
*/
getRooms: function() {
return [];
},
/**
* No-op.
* @return {Array} An empty array.
*/
getRoomSummaries: function() {
return [];
},
/**
* No-op.
* @param {User} user
*/
storeUser: function(user) {
},
/**
* No-op.
* @param {string} userId
* @return {null}
*/
getUser: function(userId) {
return null;
}
// TODO
//setMaxHistoryPerRoom: function(maxHistory) {},
// TODO
//reapOldMessages: function() {},
};
/** Stub Store class. */
module.exports = StubStore;
+57
View File
@@ -111,6 +111,63 @@ module.exports.forEach = function(array, fn) {
}
};
/**
* The findElement() method returns a value in the array, if an element in the array
* satisfies (returns true) the provided testing function. Otherwise undefined
* is returned.
* @param {Array} array The array.
* @param {Function} fn Function to execute on each value in the array, with the
* function signature <code>fn(element, index, array)</code>
* @param {boolean} reverse True to search in reverse order.
* @return {*} The first value in the array which returns <code>true</code> for
* the given function.
*/
module.exports.findElement = function(array, fn, reverse) {
var i;
if (reverse) {
for (i = array.length - 1; i >= 0; i--) {
if (fn(array[i], i, array)) {
return array[i];
}
}
}
else {
for (i = 0; i < array.length; i++) {
if (fn(array[i], i, array)) {
return array[i];
}
}
}
};
/**
* The removeElement() method removes the first element in the array that
* satisfies (returns true) the provided testing function.
* @param {Array} array The array.
* @param {Function} fn Function to execute on each value in the array, with the
* function signature <code>fn(element, index, array)</code>. Return true to
* remove this element and break.
* @param {boolean} reverse True to search in reverse order.
*/
module.exports.removeElement = function(array, fn, reverse) {
var i;
if (reverse) {
for (i = array.length - 1; i >= 0; i--) {
if (fn(array[i], i, array)) {
array.splice(i, 1);
return; }
}
}
else {
for (i = 0; i < array.length; i++) {
if (fn(array[i], i, array)) {
array.splice(i, 1);
return;
}
}
}
};
/**
* Checks if the given thing is a function.
* @param {*} value The thing to check.
+3 -2
View File
@@ -1,13 +1,14 @@
{
"name": "matrix-js-sdk",
"version": "0.1.0",
"version": "0.1.1",
"description": "Matrix Client-Server SDK for Javascript",
"main": "index.js",
"scripts": {
"test": "istanbul cover --report cobertura -i \"lib/**/*.js\" jasmine-node -- spec --verbose --junitreport --forceexit --captureExceptions",
"build": "jshint -c .jshint lib/ && browserify browser-index.js -o dist/browser-matrix-dev.js",
"watch": "watchify browser-index.js -o dist/browser-matrix-dev.js -v",
"lint": "jshint -c .jshint lib spec && gjslint --unix_mode --disable 0131,0211,0200 --max_line_length 90 -r spec/ -r lib/"
"lint": "jshint -c .jshint lib spec && gjslint --unix_mode --disable 0131,0211,0200 --max_line_length 90 -r spec/ -r lib/",
"release": "npm run build && mkdir dist/$npm_package_version && uglifyjs -c -m -o dist/$npm_package_version/browser-matrix-$npm_package_version.min.js dist/browser-matrix-dev.js && cp dist/browser-matrix-dev.js dist/$npm_package_version/browser-matrix-$npm_package_version.js"
},
"repository": {
"url": "https://github.com/matrix-org/matrix-js-sdk"
+27 -14
View File
@@ -58,7 +58,7 @@ describe("Room", function() {
});
});
describe("calculateRoomName", function() {
describe("calculate (Room Name)", function() {
var stateLookup = {
// event.type + "$" event.state_key : MatrixEvent
};
@@ -136,7 +136,8 @@ describe("Room", function() {
addMember(userB);
addMember(userC);
addMember(userD);
var name = room.calculateRoomName(userA);
room.recalculate(userA);
var name = room.name;
// we expect at least 1 member to be mentioned
var others = [userB, userC, userD];
var found = false;
@@ -156,7 +157,8 @@ describe("Room", function() {
addMember(userA);
addMember(userB);
addMember(userC);
var name = room.calculateRoomName(userA);
room.recalculate(userA);
var name = room.name;
expect(name.indexOf(userB)).not.toEqual(-1, name);
expect(name.indexOf(userC)).not.toEqual(-1, name);
});
@@ -168,7 +170,8 @@ describe("Room", function() {
addMember(userA);
addMember(userB);
addMember(userC);
var name = room.calculateRoomName(userA);
room.recalculate(userA);
var name = room.name;
expect(name.indexOf(userB)).not.toEqual(-1, name);
expect(name.indexOf(userC)).not.toEqual(-1, name);
});
@@ -179,7 +182,8 @@ describe("Room", function() {
setJoinRule("public");
addMember(userA);
addMember(userB);
var name = room.calculateRoomName(userA);
room.recalculate(userA);
var name = room.name;
expect(name.indexOf(userB)).not.toEqual(-1, name);
});
@@ -189,7 +193,8 @@ describe("Room", function() {
setJoinRule("invite");
addMember(userA);
addMember(userB);
var name = room.calculateRoomName(userA);
room.recalculate(userA);
var name = room.name;
expect(name.indexOf(userB)).not.toEqual(-1, name);
});
@@ -198,7 +203,8 @@ describe("Room", function() {
setJoinRule("invite");
addMember(userA, "invite");
addMember(userB);
var name = room.calculateRoomName(userA);
room.recalculate(userA);
var name = room.name;
expect(name.indexOf(userB)).not.toEqual(-1, name);
});
@@ -207,7 +213,8 @@ describe("Room", function() {
var alias = "#room_alias:here";
setJoinRule("invite");
setAliases([alias, "#another:one"]);
var name = room.calculateRoomName(userA);
room.recalculate(userA);
var name = room.name;
expect(name).toEqual(alias);
});
@@ -216,7 +223,8 @@ describe("Room", function() {
var alias = "#room_alias:here";
setJoinRule("public");
setAliases([alias, "#another:one"]);
var name = room.calculateRoomName(userA);
room.recalculate(userA);
var name = room.name;
expect(name).toEqual(alias);
});
@@ -225,7 +233,8 @@ describe("Room", function() {
var roomName = "A mighty name indeed";
setJoinRule("invite");
setRoomName(roomName);
var name = room.calculateRoomName(userA);
room.recalculate(userA);
var name = room.name;
expect(name).toEqual(roomName);
});
@@ -234,7 +243,8 @@ describe("Room", function() {
var roomName = "A mighty name indeed";
setJoinRule("public");
setRoomName(roomName);
var name = room.calculateRoomName(userA);
room.recalculate(userA);
var name = room.name;
expect(name).toEqual(roomName);
});
@@ -242,7 +252,8 @@ describe("Room", function() {
" a room name and alias don't exist and it is a self-chat.", function() {
setJoinRule("invite");
addMember(userA);
var name = room.calculateRoomName(userA);
room.recalculate(userA);
var name = room.name;
expect(name).toEqual(userA);
});
@@ -250,13 +261,15 @@ describe("Room", function() {
" room name and alias don't exist and it is a self-chat.", function() {
setJoinRule("public");
addMember(userA);
var name = room.calculateRoomName(userA);
room.recalculate(userA);
var name = room.name;
expect(name).toEqual(userA);
});
it("should return '?' if there is no name, alias or members in the room.",
function() {
var name = room.calculateRoomName(userA);
room.recalculate(userA);
var name = room.name;
expect(name).toEqual("?");
});
});