Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MMB-171] Update profile data #80

Merged
merged 3 commits into from
Jul 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/class-definitions.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,24 @@ Leave the space. Can optionally take `profileData`. This triggers the `leave` ev
type leave = (profileData?: Record<string, unknown>) => Promise<void>;
```

### updateProfileData

Update `profileData`. This data can be an arbitrary JSON-serializable object which is attached to the [member object](#spacemember). If the connection
has not entered the space, calling `updateProfileData` will call `enter` instead.

```ts
type updateProfileData = (profileDataOrUpdateFn?: unknown| (unknown) => unknown) => Promise<void>;
```

A function can also be passed in. This function will receive the existing `profileData` and lets you update based on the existing value of `profileData`:

```ts
await space.updateProfileData((oldProfileData) => {
const newProfileData = getNewProfileData();
return { ...oldProfileData, ...newProfileData };
})
```

### on

Listen to events for the space. See [EventEmitter](/docs/usage.md#event-emitters) for overloading usage.
Expand Down
13 changes: 13 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,19 @@ space.leave({
});
```

### Update profileData

To update `profileData` provided when entering the space, use the `updateProfileData` method. Pass new `profileData` or a function to base the new `profileData` of the existing value:

```ts
await space.updateProfileData((oldProfileData) => {
return {
...oldProfileData,
username: 'Clara Lemons'
}
});
```

## Location

Each member can set a location for themselves:
Expand Down
60 changes: 60 additions & 0 deletions src/Space.mockClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,66 @@ describe('Space (mockClient)', () => {
});
});

describe('updateProfileData', () => {
describe('did not enter', () => {
it<SpaceTestContext>('enter & update profileData successfully', async ({ presence, space }) => {
const enterSpy = vi.spyOn(presence, 'enter');
const updateSpy = vi.spyOn(presence, 'update');
await space.updateProfileData({ a: 1 });
expect(enterSpy).toHaveBeenNthCalledWith(1, { profileData: { a: 1 } });
expect(updateSpy).not.toHaveBeenCalled();
});

it<SpaceTestContext>('enter & update profileData with function successfully', async ({ presence, space }) => {
const enterSpy = vi.spyOn(presence, 'enter');
const updateSpy = vi.spyOn(presence, 'update');
await space.updateProfileData((profileData) => ({ ...profileData, a: 1 }));
expect(enterSpy).toHaveBeenNthCalledWith(1, { profileData: { a: 1 } });
expect(updateSpy).not.toHaveBeenCalled();
});
});

describe('did enter', () => {
it<SpaceTestContext>('update profileData successfully', async ({ presence, space }) => {
vi.spyOn(space, 'getSelf').mockResolvedValueOnce({
clientId: '1',
connectionId: 'testConnectionId',
isConnected: true,
location: null,
lastEvent: {
name: 'enter',
timestamp: 1,
},
profileData: {
a: 1,
},
});
const updateSpy = vi.spyOn(presence, 'update');
await space.updateProfileData({ a: 2 });
expect(updateSpy).toHaveBeenNthCalledWith(1, { profileData: { a: 2 } });
});

it<SpaceTestContext>('enter & update profileData with function successfully', async ({ presence, space }) => {
vi.spyOn(space, 'getSelf').mockResolvedValueOnce({
clientId: '1',
connectionId: 'testConnectionId',
isConnected: true,
location: null,
lastEvent: {
name: 'enter',
timestamp: 1,
},
profileData: {
a: 1,
},
});
const updateSpy = vi.spyOn(presence, 'update');
await space.updateProfileData((profileData) => ({ ...profileData, a: 2 }));
expect(updateSpy).toHaveBeenNthCalledWith(1, { profileData: { a: 2 } });
});
});
});

describe('leave', () => {
it<SpaceTestContext>('leaves a space successfully', async ({ presence, space }) => {
const spy = vi.spyOn(presence, 'leave');
Expand Down
24 changes: 23 additions & 1 deletion src/Space.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Cursors from './Cursors.js';

// Unique prefix to avoid conflicts with channels
import { LOCATION_UPDATE, MEMBERS_UPDATE, SPACE_CHANNEL_PREFIX } from './utilities/Constants.js';
import { isFunction } from './utilities/TypeOf.js';

export type SpaceMember = {
clientId: string;
Expand All @@ -30,7 +31,7 @@ const SPACE_OPTIONS_DEFAULTS = {
offlineTimeout: 120_000,
};

type SpaceEventsMap = { membersUpdate: SpaceMember[]; leave: SpaceMember; enter: SpaceMember };
type SpaceEventsMap = { membersUpdate: SpaceMember[]; leave: SpaceMember; enter: SpaceMember; update: SpaceMember };

class Space extends EventEmitter<SpaceEventsMap> {
private channelName: string;
Expand Down Expand Up @@ -153,6 +154,7 @@ class Space extends EventEmitter<SpaceEventsMap> {
const spaceMember = this.updateOrCreateMember(message);

if (index >= 0) {
this.emit('update', spaceMember);
this.members[index] = spaceMember;
} else {
this.emit('enter', spaceMember);
Expand Down Expand Up @@ -191,6 +193,26 @@ class Space extends EventEmitter<SpaceEventsMap> {
});
}

async updateProfileData(profileDataOrUpdateFn: unknown | ((unknown) => unknown)): Promise<void> {
const self = this.getSelf();

if (isFunction(profileDataOrUpdateFn) && !self) {
const update = profileDataOrUpdateFn();
await this.enter(update);
return;
} else if (!self) {
await this.enter(profileDataOrUpdateFn);
return;
} else if (isFunction(profileDataOrUpdateFn) && self) {
const update = profileDataOrUpdateFn(self.profileData);
await this.channel.presence.update({ profileData: update });
return;
}

await this.channel.presence.update({ profileData: profileDataOrUpdateFn });
return;
}

leave(profileData?: unknown) {
return this.channel.presence.leave({ profileData });
}
Expand Down
23 changes: 2 additions & 21 deletions src/utilities/EventEmitter.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isArray, isFunction, isObject, isString } from './TypeOf.js';

function callListener(eventThis: { event: string }, listener: Function, args: unknown[]) {
try {
listener.apply(eventThis, args);
Expand Down Expand Up @@ -55,27 +57,6 @@ function inspect(args: any): string {
return JSON.stringify(args);
}

function typeOf(arg: unknown): string {
return Object.prototype.toString.call(arg).slice(8, -1);
}

// Equivalent of Util.isObject from ably-js
function isObject(arg: unknown): arg is Record<string, unknown> {
return typeOf(arg) === 'Object';
}

function isFunction(arg: unknown): arg is Function {
return typeOf(arg) === 'Function';
}

function isString(arg: unknown): arg is String {
return typeOf(arg) === 'String';
}

function isArray<T>(arg: unknown): arg is Array<T> {
return Array.isArray(arg);
}

type EventMap = Record<string, any>;
// extract all the keys of an event map and use them as a type
type EventKey<T extends EventMap> = string & keyof T;
Expand Down
22 changes: 22 additions & 0 deletions src/utilities/TypeOf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
function typeOf(arg: unknown): string {
return Object.prototype.toString.call(arg).slice(8, -1);
}

// Equivalent of Util.isObject from ably-js
function isObject(arg: unknown): arg is Record<string, unknown> {
return typeOf(arg) === 'Object';
}

function isFunction(arg: unknown): arg is Function {
return typeOf(arg) === 'Function';
}

function isString(arg: unknown): arg is String {
return typeOf(arg) === 'String';
}

function isArray<T>(arg: unknown): arg is Array<T> {
return Array.isArray(arg);
}

export { isArray, isFunction, isObject, isString };
Loading