Skip to content

Commit

Permalink
Add SpaceUpdate class
Browse files Browse the repository at this point in the history
This class is responsible for creating objects that will be passed to presence. It takes care of creating an update id and encapsulates the structure of the update.
  • Loading branch information
Dominik Piatek authored and dpiatek committed Sep 22, 2023
1 parent 9740465 commit 0cbad07
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 106 deletions.
24 changes: 4 additions & 20 deletions src/Locations.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { nanoid } from 'nanoid';

import EventEmitter, {
InvalidArgumentError,
inspect,
Expand All @@ -11,6 +9,7 @@ import type { SpaceMember } from './types.js';
import type { PresenceMember } from './utilities/types.js';
import type Space from './Space.js';
import { ERR_NOT_ENTERED_SPACE } from './Errors.js';
import SpaceUpdate from './SpaceUpdate.js';

type LocationsEventMap = {
update: { member: SpaceMember; currentLocation: unknown; previousLocation: unknown };
Expand All @@ -19,10 +18,7 @@ type LocationsEventMap = {
export default class Locations extends EventEmitter<LocationsEventMap> {
private lastLocationUpdate: Record<string, PresenceMember['data']['locationUpdate']['id']> = {};

constructor(
private space: Space,
private presenceUpdate: (update: PresenceMember['data'], extras?: PresenceMember['extras']) => Promise<void>,
) {
constructor(private space: Space, private presenceUpdate: Space['presenceUpdate']) {
super();
}

Expand Down Expand Up @@ -61,20 +57,8 @@ export default class Locations extends EventEmitter<LocationsEventMap> {
throw ERR_NOT_ENTERED_SPACE();
}

const update: PresenceMember['data'] = {
profileUpdate: {
id: null,
current: self.profileData,
},
locationUpdate: {
id: nanoid(),
previous: self.location,
current: location,
},
};

const extras = this.space.locks.getLockExtras(self.connectionId);
await this.presenceUpdate(update, extras);
const update = new SpaceUpdate({ self, extras: this.space.locks.getLockExtras(self.connectionId) });
await this.presenceUpdate(update.updateLocation(location));
}

subscribe<K extends EventKey<LocationsEventMap>>(
Expand Down
23 changes: 6 additions & 17 deletions src/Locks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import EventEmitter, {
type EventListener,
} from './utilities/EventEmitter.js';

import SpaceUpdate from './SpaceUpdate.js';

export class LockAttributes extends Map<string, string> {
toJSON() {
return Object.fromEntries(this);
Expand All @@ -34,10 +36,7 @@ export default class Locks extends EventEmitter<LockEventMap> {
// have requested.
private locks: Map<string, Map<string, Lock>>;

constructor(
private space: Space,
private presenceUpdate: (update: PresenceMember['data'], extras?: any) => Promise<void>,
) {
constructor(private space: Space, private presenceUpdate: Space['presenceUpdate']) {
super();
this.locks = new Map();
}
Expand Down Expand Up @@ -267,19 +266,9 @@ export default class Locks extends EventEmitter<LockEventMap> {
pendingLock.reason = ERR_LOCK_IS_LOCKED();
}

updatePresence(member: SpaceMember) {
const update: PresenceMember['data'] = {
profileUpdate: {
id: null,
current: member.profileData,
},
locationUpdate: {
id: null,
current: member?.location ?? null,
previous: null,
},
};
return this.presenceUpdate(update, this.getLockExtras(member.connectionId));
updatePresence(self: SpaceMember) {
const update = new SpaceUpdate({ self, extras: this.getLockExtras(self.connectionId) });
return this.presenceUpdate(update.noop());
}

getLock(id: string, connectionId: string): Lock | undefined {
Expand Down
89 changes: 20 additions & 69 deletions src/Space.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Ably, { Types } from 'ably';
import { nanoid } from 'nanoid';

import EventEmitter, {
InvalidArgumentError,
Expand All @@ -11,9 +10,9 @@ import Locations from './Locations.js';
import Cursors from './Cursors.js';
import Members from './Members.js';
import Locks from './Locks.js';
import SpaceUpdate, { type SpacePresenceData } from './SpaceUpdate.js';

import { ERR_NOT_ENTERED_SPACE } from './Errors.js';

import { isFunction, isObject } from './utilities/is.js';

import type { SpaceOptions, SpaceMember, ProfileData } from './types.js';
Expand Down Expand Up @@ -63,21 +62,21 @@ class Space extends EventEmitter<SpaceEventsMap> {
this.locks = new Locks(this, this.presenceUpdate);
}

private presenceUpdate = (data: PresenceMember['data'], extras?: PresenceMember['extras']) => {
private presenceUpdate = ({ data, extras }: SpacePresenceData) => {
if (!extras) {
return this.channel.presence.update(data);
}
return this.channel.presence.update(Ably.Realtime.PresenceMessage.fromValues({ data, extras }));
};

private presenceEnter = (data: PresenceMember['data'], extras?: PresenceMember['extras']) => {
private presenceEnter = ({ data, extras }: SpacePresenceData) => {
if (!extras) {
return this.channel.presence.enter(data);
}
return this.channel.presence.enter(Ably.Realtime.PresenceMessage.fromValues({ data, extras }));
};

private presenceLeave = (data: PresenceMember['data'], extras?: PresenceMember['extras']) => {
private presenceLeave = ({ data, extras }: SpacePresenceData) => {
if (!extras) {
return this.channel.presence.leave(data);
}
Expand Down Expand Up @@ -106,33 +105,6 @@ class Space extends EventEmitter<SpaceEventsMap> {
this.emit('update', { members: await this.members.getAll() });
}

private createProfileUpdate(self, update: ProfileData) {
const profileUpdate = {
id: nanoid(),
current: update,
};

if (!self) {
return {
profileUpdate,
locationUpdate: {
id: null,
current: null,
previous: null,
},
};
} else {
return {
profileUpdate,
locationUpdate: {
id: null,
current: self.location ?? null,
previous: null,
},
};
}
}

async enter(profileData: ProfileData = null): Promise<SpaceMember[]> {
return new Promise((resolve) => {
const presence = this.channel.presence;
Expand All @@ -150,17 +122,8 @@ class Space extends EventEmitter<SpaceEventsMap> {
resolve(members);
});

this.presenceEnter({
profileUpdate: {
id: nanoid(),
current: profileData,
},
locationUpdate: {
id: null,
current: null,
previous: null,
},
});
const update = new SpaceUpdate({ self: null, extras: null });
this.presenceEnter(update.updateProfileData(profileData));
});
}

Expand All @@ -173,22 +136,20 @@ class Space extends EventEmitter<SpaceEventsMap> {
);
}

let update = new SpaceUpdate({ self, extras: self ? this.locks.getLockExtras(self.connectionId) : null });

if (!self) {
const update = await this.createProfileUpdate(
self,
const data = update.updateProfileData(
isFunction(profileDataOrUpdateFn) ? profileDataOrUpdateFn(null) : profileDataOrUpdateFn,
);
await this.presenceEnter(update);
await this.presenceEnter(data);
return;
} else {
const data = update.updateProfileData(
isFunction(profileDataOrUpdateFn) ? profileDataOrUpdateFn(self.profileData) : profileDataOrUpdateFn,
);
return this.presenceUpdate(data);
}

const update = await this.createProfileUpdate(
self,
isFunction(profileDataOrUpdateFn) ? profileDataOrUpdateFn(self.profileData) : profileDataOrUpdateFn,
);
const extras = this.locks.getLockExtras(self.connectionId);

return this.presenceUpdate(update, extras);
}

async leave(profileData: ProfileData = null) {
Expand All @@ -198,27 +159,17 @@ class Space extends EventEmitter<SpaceEventsMap> {
throw ERR_NOT_ENTERED_SPACE();
}

let update;
const update = new SpaceUpdate({ self, extras: this.locks.getLockExtras(self.connectionId) });
let data;

// Use arguments so it's possible to deliberately nullify profileData on leave
if (arguments.length > 0) {
update = this.createProfileUpdate(self, profileData);
data = update.updateProfileData(profileData);
} else {
update = {
profileUpdate: {
id: null,
current: self.profileData,
},
locationUpdate: {
id: null,
current: self.location,
previous: null,
},
};
data = update.noop();
}

const extras = this.locks.getLockExtras(self.connectionId);
await this.presenceLeave(update, extras);
await this.presenceLeave(data);
}

async getState(): Promise<{ members: SpaceMember[] }> {
Expand Down
71 changes: 71 additions & 0 deletions src/SpaceUpdate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { describe, it, vi, expect } from 'vitest';

import SpaceUpdate from './SpaceUpdate.js';
import { createSpaceMember } from './utilities/test/fakes.js';

vi.mock('nanoid');

describe('SpaceUpdate', () => {
it('creates a profileUpdate', () => {
const self = createSpaceMember({ profileData: { name: 'Berry' } });
const update = new SpaceUpdate({ self });
expect(update.updateProfileData({ name: 'Barry' })).toEqual({
data: {
locationUpdate: {
current: null,
id: null,
previous: null,
},
profileUpdate: {
current: {
name: 'Barry',
},
id: 'NanoidID',
},
},
extras: undefined,
});
});

it('creates a locationUpdate', () => {
const self = createSpaceMember({ location: { slide: 3 }, profileData: { name: 'Berry' } });
const update = new SpaceUpdate({ self });
expect(update.updateLocation({ slide: 1 }, null)).toEqual({
data: {
locationUpdate: {
current: { slide: 1 },
id: 'NanoidID',
previous: { slide: 3 },
},
profileUpdate: {
current: {
name: 'Berry',
},
id: null,
},
},
extras: undefined,
});
});

it('creates an object with no updates to current data', () => {
const self = createSpaceMember({ location: { slide: 3 }, profileData: { name: 'Berry' } });
const update = new SpaceUpdate({ self });
expect(update.noop()).toEqual({
data: {
locationUpdate: {
current: { slide: 3 },
id: null,
previous: null,
},
profileUpdate: {
current: {
name: 'Berry',
},
id: null,
},
},
extras: undefined,
});
});
});
73 changes: 73 additions & 0 deletions src/SpaceUpdate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { nanoid } from 'nanoid';
import { Types } from 'ably';

import type { SpaceMember, ProfileData } from './types.js';
import type { PresenceMember } from './utilities/types.js';

export interface SpacePresenceData {
data: PresenceMember['data'];
extras: PresenceMember['extras'];
}

class SpaceUpdate {
private self: SpaceMember | null;
private extras: Types.PresenceMessage['extras'];

constructor({ self, extras }: { self: SpaceMember | null; extras?: Types.PresenceMessage['extras'] }) {
this.self = self;
this.extras = extras;
}

private profileUpdate(id: string | null, current: ProfileData) {
return { id, current };
}

private profileNoChange() {
return this.profileUpdate(null, this.self ? this.self.profileData : null);
}

private locationUpdate(id: string | null, current: SpaceMember['location'], previous: SpaceMember['location']) {
return { id, current, previous };
}

private locationNoChange() {
const location = this.self ? this.self.location : null;
return this.locationUpdate(null, location, null);
}

updateProfileData(current: ProfileData): SpacePresenceData {
return {
data: {
profileUpdate: this.profileUpdate(nanoid(), current),
locationUpdate: this.locationNoChange(),
},
extras: this.extras,
};
}

updateLocation(location: SpaceMember['location'], previousLocation?: SpaceMember['location']): SpacePresenceData {
return {
data: {
profileUpdate: this.profileNoChange(),
locationUpdate: this.locationUpdate(
nanoid(),
location,
previousLocation ? previousLocation : this.self?.location,
),
},
extras: this.extras,
};
}

noop(): SpacePresenceData {
return {
data: {
profileUpdate: this.profileNoChange(),
locationUpdate: this.locationNoChange(),
},
extras: this.extras,
};
}
}

export default SpaceUpdate;

0 comments on commit 0cbad07

Please sign in to comment.