* DEV: Improve PresenceChannel state storage
Replaces some objects with Maps, and removes the redundant _presentChannels Set.
* DEV: Automatically leave PresenceChannels when in the background
If a tab has been in the background for 10s, or there has been no user activity for 60s, then the user will be removed from all PresenceChannels until activity resumes. Developers can opt-out of this by passing `{onlyWhileActive: false}` to the `enter` method.
382 lines
11 KiB
JavaScript
382 lines
11 KiB
JavaScript
import {
|
|
acceptance,
|
|
publishToMessageBus,
|
|
} from "discourse/tests/helpers/qunit-helpers";
|
|
import { test } from "qunit";
|
|
import { PresenceChannelNotFound } from "discourse/services/presence";
|
|
import { setTestPresence } from "discourse/lib/user-presence";
|
|
|
|
function usersFixture() {
|
|
return [
|
|
{
|
|
id: 1,
|
|
username: "bruce0",
|
|
name: "Bruce Wayne",
|
|
avatar_template: "/letter_avatar_proxy/v4/letter/b/90ced4/{size}.png",
|
|
},
|
|
{
|
|
id: 2,
|
|
username: "bruce1",
|
|
name: "Bruce Wayne",
|
|
avatar_template: "/letter_avatar_proxy/v4/letter/b/9de053/{size}.png",
|
|
},
|
|
{
|
|
id: 3,
|
|
username: "bruce2",
|
|
name: "Bruce Wayne",
|
|
avatar_template: "/letter_avatar_proxy/v4/letter/b/35a633/{size}.png",
|
|
},
|
|
];
|
|
}
|
|
|
|
acceptance("Presence - Subscribing", function (needs) {
|
|
needs.pretender((server, helper) => {
|
|
server.get("/presence/get", (request) => {
|
|
const channels = request.queryParams.channels;
|
|
const response = {};
|
|
|
|
channels.forEach((c) => {
|
|
if (c.startsWith("/test/")) {
|
|
response[c] = {
|
|
count: 3,
|
|
last_message_id: 1,
|
|
users: usersFixture(),
|
|
};
|
|
} else if (c.startsWith("/countonly/")) {
|
|
response[c] = {
|
|
count: 3,
|
|
last_message_id: 1,
|
|
};
|
|
} else {
|
|
response[c] = null;
|
|
}
|
|
});
|
|
|
|
return helper.response(200, response);
|
|
});
|
|
});
|
|
|
|
test("subscribing and receiving updates", async function (assert) {
|
|
let presenceService = this.container.lookup("service:presence");
|
|
let channel = presenceService.getChannel("/test/ch1");
|
|
assert.strictEqual(channel.name, "/test/ch1");
|
|
|
|
await channel.subscribe({
|
|
users: usersFixture(),
|
|
last_message_id: 1,
|
|
});
|
|
|
|
assert.strictEqual(channel.users.length, 3, "it starts with three users");
|
|
|
|
publishToMessageBus(
|
|
"/presence/test/ch1",
|
|
{
|
|
leaving_user_ids: [1],
|
|
},
|
|
0,
|
|
2
|
|
);
|
|
|
|
assert.strictEqual(channel.users.length, 2, "one user is removed");
|
|
|
|
publishToMessageBus(
|
|
"/presence/test/ch1",
|
|
{
|
|
entering_users: [usersFixture()[0]],
|
|
},
|
|
0,
|
|
3
|
|
);
|
|
|
|
assert.strictEqual(channel.users.length, 3, "one user is added");
|
|
});
|
|
|
|
test("fetches data when no initial state", async function (assert) {
|
|
let presenceService = this.container.lookup("service:presence");
|
|
let channel = presenceService.getChannel("/test/ch1");
|
|
|
|
await channel.subscribe();
|
|
|
|
assert.strictEqual(channel.users.length, 3, "loads initial state");
|
|
|
|
publishToMessageBus(
|
|
"/presence/test/ch1",
|
|
{
|
|
leaving_user_ids: [1],
|
|
},
|
|
0,
|
|
2
|
|
);
|
|
|
|
assert.strictEqual(
|
|
channel.users.length,
|
|
2,
|
|
"updates following messagebus message"
|
|
);
|
|
|
|
publishToMessageBus(
|
|
"/presence/test/ch1",
|
|
{
|
|
leaving_user_ids: [2],
|
|
},
|
|
0,
|
|
99
|
|
);
|
|
|
|
await channel._presenceState._resubscribePromise;
|
|
|
|
assert.strictEqual(
|
|
channel.users.length,
|
|
3,
|
|
"detects missed messagebus message, fetches data from server"
|
|
);
|
|
});
|
|
|
|
test("raises error when subscribing to nonexistent channel", async function (assert) {
|
|
let presenceService = this.container.lookup("service:presence");
|
|
let channel = presenceService.getChannel("/nonexistent/ch1");
|
|
|
|
assert.rejects(
|
|
channel.subscribe(),
|
|
PresenceChannelNotFound,
|
|
"raises not found"
|
|
);
|
|
});
|
|
|
|
test("can subscribe to count_only channel", async function (assert) {
|
|
let presenceService = this.container.lookup("service:presence");
|
|
let channel = presenceService.getChannel("/countonly/ch1");
|
|
|
|
await channel.subscribe();
|
|
|
|
assert.strictEqual(channel.count, 3, "has the correct count");
|
|
assert.strictEqual(channel.countOnly, true, "identifies as countOnly");
|
|
assert.strictEqual(channel.users, null, "has null users list");
|
|
|
|
publishToMessageBus(
|
|
"/presence/countonly/ch1",
|
|
{
|
|
count_delta: 1,
|
|
},
|
|
0,
|
|
2
|
|
);
|
|
|
|
assert.strictEqual(channel.count, 4, "updates the count via messagebus");
|
|
|
|
publishToMessageBus(
|
|
"/presence/countonly/ch1",
|
|
{
|
|
leaving_user_ids: [2],
|
|
},
|
|
0,
|
|
3
|
|
);
|
|
|
|
await channel._presenceState._resubscribePromise;
|
|
|
|
assert.strictEqual(
|
|
channel.count,
|
|
3,
|
|
"resubscribes when receiving a non-count-only message"
|
|
);
|
|
});
|
|
|
|
test("can share data between multiple PresenceChannel objects", async function (assert) {
|
|
let presenceService = this.container.lookup("service:presence");
|
|
let channel = presenceService.getChannel("/test/ch1");
|
|
let channelDup = presenceService.getChannel("/test/ch1");
|
|
|
|
await channel.subscribe();
|
|
assert.strictEqual(channel.subscribed, true, "channel is subscribed");
|
|
assert.strictEqual(channel.count, 3, "channel has the correct count");
|
|
assert.strictEqual(channel.users.length, 3, "channel has users");
|
|
|
|
assert.strictEqual(
|
|
channelDup.subscribed,
|
|
false,
|
|
"channelDup is not subscribed"
|
|
);
|
|
assert.strictEqual(channelDup.count, undefined, "channelDup has no count");
|
|
assert.strictEqual(channelDup.users, undefined, "channelDup has users");
|
|
|
|
await channelDup.subscribe();
|
|
assert.strictEqual(channelDup.subscribed, true, "channelDup can subscribe");
|
|
assert.ok(
|
|
channelDup._presenceState,
|
|
"channelDup has a valid internal state"
|
|
);
|
|
assert.strictEqual(
|
|
channelDup._presenceState,
|
|
channel._presenceState,
|
|
"internal state is shared"
|
|
);
|
|
|
|
await channel.unsubscribe();
|
|
assert.strictEqual(channel.subscribed, false, "channel can unsubscribe");
|
|
assert.strictEqual(
|
|
channelDup._presenceState,
|
|
channel._presenceState,
|
|
"state is maintained"
|
|
);
|
|
|
|
await channelDup.unsubscribe();
|
|
assert.strictEqual(channel.subscribed, false, "channelDup can unsubscribe");
|
|
assert.strictEqual(
|
|
channelDup._presenceState,
|
|
undefined,
|
|
"state is cleared"
|
|
);
|
|
});
|
|
});
|
|
|
|
acceptance("Presence - Entering and Leaving", function (needs) {
|
|
needs.user();
|
|
|
|
const requests = [];
|
|
needs.hooks.afterEach(() => requests.clear());
|
|
needs.pretender((server, helper) => {
|
|
server.post("/presence/update", (request) => {
|
|
const body = new URLSearchParams(request.requestBody);
|
|
requests.push(body);
|
|
|
|
const response = {};
|
|
const channelsRequested = body.getAll("present_channels[]");
|
|
channelsRequested.forEach((c) => {
|
|
if (c.startsWith("/test/")) {
|
|
response[c] = true;
|
|
} else {
|
|
response[c] = false;
|
|
}
|
|
});
|
|
|
|
return helper.response(response);
|
|
});
|
|
});
|
|
|
|
test("can join and leave channels", async function (assert) {
|
|
const presenceService = this.container.lookup("service:presence");
|
|
const channel = presenceService.getChannel("/test/ch1");
|
|
|
|
await channel.enter();
|
|
assert.strictEqual(requests.length, 1, "updated the server for enter");
|
|
let presentChannels = requests.pop().getAll("present_channels[]");
|
|
assert.deepEqual(
|
|
presentChannels,
|
|
["/test/ch1"],
|
|
"included the correct present channel"
|
|
);
|
|
|
|
await channel.leave();
|
|
assert.strictEqual(requests.length, 1, "updated the server for leave");
|
|
const request = requests.pop();
|
|
presentChannels = request.getAll("present_channels[]");
|
|
const leaveChannels = request.getAll("leave_channels[]");
|
|
assert.deepEqual(presentChannels, [], "included no present channels");
|
|
assert.deepEqual(
|
|
leaveChannels,
|
|
["/test/ch1"],
|
|
"included the correct leave channel"
|
|
);
|
|
});
|
|
|
|
test("raises an error when entering a non-existant channel", async function (assert) {
|
|
const presenceService = this.container.lookup("service:presence");
|
|
const channel = presenceService.getChannel("/blah/doesnotexist");
|
|
await assert.rejects(
|
|
channel.enter(),
|
|
PresenceChannelNotFound,
|
|
"raises a not found error"
|
|
);
|
|
});
|
|
|
|
test("deduplicates calls from multiple PresenceChannel instances", async function (assert) {
|
|
const presenceService = this.container.lookup("service:presence");
|
|
const channel = presenceService.getChannel("/test/ch1");
|
|
const channelDup = presenceService.getChannel("/test/ch1");
|
|
|
|
await channel.enter();
|
|
assert.strictEqual(channel.present, true, "channel is present");
|
|
assert.strictEqual(channelDup.present, false, "channelDup is absent");
|
|
assert.ok(
|
|
presenceService._presentChannels.has("/test/ch1"),
|
|
"service shows present"
|
|
);
|
|
|
|
await channelDup.enter();
|
|
assert.strictEqual(channel.present, true, "channel is present");
|
|
assert.strictEqual(channelDup.present, true, "channelDup is present");
|
|
assert.ok(
|
|
presenceService._presentChannels.has("/test/ch1"),
|
|
"service shows present"
|
|
);
|
|
|
|
await channel.leave();
|
|
assert.strictEqual(channel.present, false, "channel is absent");
|
|
assert.strictEqual(channelDup.present, true, "channelDup is present");
|
|
assert.ok(
|
|
presenceService._presentChannels.has("/test/ch1"),
|
|
"service shows present"
|
|
);
|
|
|
|
await channelDup.leave();
|
|
assert.strictEqual(channel.present, false, "channel is absent");
|
|
assert.strictEqual(channel.present, false, "channelDup is absent");
|
|
assert.notOk(
|
|
presenceService._presentChannels.has("/test/ch1"),
|
|
"service shows absent"
|
|
);
|
|
});
|
|
|
|
test("handles the onlyWhileActive flag", async function (assert) {
|
|
const presenceService = this.container.lookup("service:presence");
|
|
const channel = presenceService.getChannel("/test/ch1");
|
|
await channel.enter();
|
|
requests.pop(); // Throw away this request
|
|
|
|
const channel2 = presenceService.getChannel("/test/ch2");
|
|
await channel2.enter({ onlyWhileActive: false });
|
|
|
|
assert.strictEqual(requests.length, 1, "updated the server");
|
|
let presentChannels = requests.pop().getAll("present_channels[]");
|
|
assert.deepEqual(
|
|
presentChannels,
|
|
["/test/ch1", "/test/ch2"],
|
|
"included both channels when active"
|
|
);
|
|
|
|
setTestPresence(false);
|
|
await presenceService._updateServer();
|
|
assert.strictEqual(
|
|
requests.length,
|
|
1,
|
|
"updated the server after going idle"
|
|
);
|
|
let request = requests.pop();
|
|
assert.deepEqual(
|
|
request.getAll("present_channels[]"),
|
|
["/test/ch2"],
|
|
"ch2 remained present"
|
|
);
|
|
assert.ok(
|
|
request.getAll("leave_channels[]").includes("/test/ch1"),
|
|
"left ch1"
|
|
);
|
|
|
|
await channel2.leave();
|
|
assert.strictEqual(requests.length, 1, "updated the server");
|
|
request = requests.pop();
|
|
assert.ok(
|
|
request.getAll("leave_channels[]").includes("/test/ch2"),
|
|
"left ch2"
|
|
);
|
|
|
|
await presenceService._updateServer();
|
|
assert.strictEqual(
|
|
requests.length,
|
|
0,
|
|
"skips sending empty updates to the server"
|
|
);
|
|
});
|
|
});
|