Skip to content

Commit

Permalink
Reset online clients when stream is disconnected (#796)
Browse files Browse the repository at this point in the history
When the stream is disconnected, online-clients information should be
cleared and no longer accessible.
  • Loading branch information
chacha912 authored Apr 30, 2024
1 parent fa85683 commit 589dd8f
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 1 deletion.
1 change: 1 addition & 0 deletions public/quill-two-clients.html
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@
}

function updateAllCursors() {
cursors.clearCursors();
for (const user of doc.getPresences()) {
updateCursor(user);
}
Expand Down
9 changes: 9 additions & 0 deletions src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
StreamConnectionStatus,
DocumentSyncStatus,
} from '@yorkie-js-sdk/src/document/document';
import { OpSource } from '@yorkie-js-sdk/src/document/operation/operation';
import { createAuthInterceptor } from '@yorkie-js-sdk/src/client/auth_interceptor';
import { createMetricInterceptor } from '@yorkie-js-sdk/src/client/metric_interceptor';

Expand Down Expand Up @@ -604,6 +605,14 @@ export class Client {
}
}
} catch (err) {
attachment.doc.resetOnlineClients();
attachment.doc.publish([
{
type: DocEventType.Initialized,
source: OpSource.Local,
value: attachment.doc.getPresences(),
},
]);
attachment.doc.publish([
{
type: DocEventType.ConnectionChanged,
Expand Down
21 changes: 21 additions & 0 deletions src/document/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1383,6 +1383,9 @@ export class Document<T, P extends Indexable = Indexable> {
const clientIDs = resp.body.value.clientIds;
const onlineClients: Set<ActorID> = new Set();
for (const clientID of clientIDs) {
if (clientID === this.changeID.getActorID()) {
continue;
}
onlineClients.add(clientID);
}
this.setOnlineClients(onlineClients);
Expand Down Expand Up @@ -1552,6 +1555,15 @@ export class Document<T, P extends Indexable = Indexable> {
this.onlineClients = onlineClients;
}

/**
* `resetOnlineClients` resets the online client set.
*
* @internal
*/
public resetOnlineClients() {
this.onlineClients = new Set();
}

/**
* `addOnlineClient` adds the given clientID into the online client set.
*
Expand Down Expand Up @@ -1597,6 +1609,10 @@ export class Document<T, P extends Indexable = Indexable> {
* `getPresence` returns the presence of the given clientID.
*/
public getPresence(clientID: ActorID): P | undefined {
if (clientID === this.changeID.getActorID()) {
return this.getMyPresence();
}

if (!this.onlineClients.has(clientID)) return;
const p = this.presences.get(clientID);
return p ? deepcopy(p) : undefined;
Expand All @@ -1618,6 +1634,11 @@ export class Document<T, P extends Indexable = Indexable> {
*/
public getPresences(): Array<{ clientID: ActorID; presence: P }> {
const presences: Array<{ clientID: ActorID; presence: P }> = [];
presences.push({
clientID: this.changeID.getActorID(),
presence: deepcopy(this.getMyPresence()),
});

for (const clientID of this.onlineClients) {
if (this.presences.has(clientID)) {
presences.push({
Expand Down
50 changes: 49 additions & 1 deletion test/integration/presence_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import yorkie, {
DocEventType,
Counter,
SyncMode,
StreamConnectionStatus,
} from '@yorkie-js-sdk/src/yorkie';
import { InitialActorID } from '@yorkie-js-sdk/src/document/time/actor_id';
import {
testRPCAddr,
toDocKey,
Expand Down Expand Up @@ -312,6 +314,52 @@ describe('Presence', function () {
await c2.sync();
assert.deepEqual(doc2.getPresenceForTest(c1.getID()!), { counter: 1 });
});

it(`Should not be accessible to other clients' presence when the stream is disconnected`, async function ({
task,
}) {
const c1 = new yorkie.Client(testRPCAddr);
const c2 = new yorkie.Client(testRPCAddr);
await c1.activate();
await c2.activate();
const c2ID = c2.getID()!;

const docKey = toDocKey(`${task.name}-${new Date().getTime()}`);
type EventForTest = Pick<DocEvent, 'type' | 'value'>;
const eventCollector = new EventCollector<EventForTest>();
const eventCollector2 = new EventCollector<EventForTest>();
type PresenceType = { name: string };
const doc1 = new yorkie.Document<{}, PresenceType>(docKey);

await c1.attach(doc1, { initialPresence: { name: 'a' } });
const unsub = doc1.subscribe('presence', ({ type, value }) => {
eventCollector.add({ type, value });
});
const unsub2 = doc1.subscribe('connection', ({ type, value }) => {
eventCollector2.add({ type, value });
});

const doc2 = new yorkie.Document<{}, PresenceType>(docKey);
await c2.attach(doc2, { initialPresence: { name: 'b' } });
await eventCollector.waitAndVerifyNthEvent(1, {
type: DocEventType.Watched,
value: { clientID: c2ID, presence: doc2.getMyPresence() },
});
assert.deepEqual(doc1.getPresence(c2ID), { name: 'b' });

await c1.changeSyncMode(doc1, SyncMode.Manual);
await eventCollector2.waitAndVerifyNthEvent(1, {
type: DocEventType.ConnectionChanged,
value: StreamConnectionStatus.Disconnected,
});
assert.equal(doc1.getPresence(c2ID), undefined);

await c1.deactivate();
await c2.deactivate();

unsub();
unsub2();
});
});

describe(`Document.Subscribe('presence')`, function () {
Expand Down Expand Up @@ -410,8 +458,8 @@ describe(`Document.Subscribe('presence')`, function () {
deepSort([{ clientID: c1ID, presence: { name: 'a' } }]),
);
assert.deepEqual(
deepSort(doc1.getPresences()),
deepSort(doc2.getPresences()),
deepSort([{ clientID: InitialActorID, presence: {} }]),
);

await c1.deactivate();
Expand Down

0 comments on commit 589dd8f

Please sign in to comment.