diff --git a/docs/class-definitions.md b/docs/class-definitions.md index 6726f773..7c400d8a 100644 --- a/docs/class-definitions.md +++ b/docs/class-definitions.md @@ -381,6 +381,34 @@ Example: const lastPositions = space.cursors.getAll(); ``` +### getSelf + +Get the last CursorUpdate for self. + +```ts +type getSelf = () => ; +``` + +Example: + +```ts +const selfPosition = space.cursors.getSelf(); +``` + +### getOthers + +Get the last CursorUpdate for each connection. + +```ts +type getOthers = () => Record; +``` + +Example: + +```ts +const otherPositions = space.cursors.getOthers(); +``` + ### subscribe Listen to `CursorUpdate` events. See [EventEmitter](/docs/usage.md#event-emitters) for overloading usage. diff --git a/src/CursorHistory.ts b/src/CursorHistory.ts index 6d785dd1..04474e8b 100644 --- a/src/CursorHistory.ts +++ b/src/CursorHistory.ts @@ -3,11 +3,8 @@ import { Types } from 'ably'; import type { CursorUpdate } from './Cursors.js'; import type { StrictCursorsOptions } from './options/CursorsOptions.js'; -type LastPosition = null | CursorUpdate; -type CursorName = string; -type CursorsLastPostion = Record; type ConnectionId = string; -type ConnectionsLastPosition = Record; +type ConnectionsLastPosition = Record; export default class CursorHistory { constructor() {} diff --git a/src/Cursors.mockClient.test.ts b/src/Cursors.mockClient.test.ts index 611acf4f..4496f797 100644 --- a/src/Cursors.mockClient.test.ts +++ b/src/Cursors.mockClient.test.ts @@ -1,13 +1,14 @@ import { it, describe, expect, vi, beforeEach, vitest, afterEach } from 'vitest'; import { Realtime, Types } from 'ably/promises'; -import Space from './Space.js'; +import Space, { SpaceMember } from './Space.js'; import Cursors from './Cursors.js'; import { createPresenceMessage } from './utilities/test/fakes.js'; import CursorBatching from './CursorBatching.js'; import { CURSOR_UPDATE } from './utilities/Constants.js'; import CursorDispensing from './CursorDispensing.js'; import CursorHistory from './CursorHistory.js'; +import type { CursorUpdate } from './Cursors.js'; interface CursorsTestContext { client: Types.RealtimePromise; @@ -18,6 +19,8 @@ interface CursorsTestContext { dispensing: CursorDispensing; history: CursorHistory; fakeMessageStub: Types.Message; + selfStub: SpaceMember; + lastCursorPositionsStub: Record; } vi.mock('ably/promises'); @@ -335,6 +338,49 @@ describe('Cursors (mockClient)', () => { }); describe('CursorHistory', () => { + beforeEach((context) => { + context.selfStub = { + connectionId: 'connectionId1', + clientId: 'clientId1', + isConnected: true, + profileData: {}, + location: {}, + lastEvent: { name: 'enter', timestamp: 0 }, + }; + + context.lastCursorPositionsStub = { + connectionId1: { + connectionId: 'connectionId1', + clientId: 'clientId1', + data: { + color: 'blue', + }, + position: { + x: 2, + y: 3, + }, + }, + connectionId2: { + connectionId: 'connectionId2', + clientId: 'clientId2', + data: undefined, + position: { + x: 25, + y: 44, + }, + }, + connectionId3: { + connectionId: 'connectionId3', + clientId: 'clientId3', + data: undefined, + position: { + x: 225, + y: 244, + }, + }, + }; + }); + it('returns an empty object if there is no members in the space', async ({ space, channel, @@ -410,5 +456,85 @@ describe('Cursors (mockClient)', () => { expect(currentSpy).toHaveBeenCalledOnce(); expect(nextSpy).toHaveBeenCalledTimes(cursors.options.paginationLimit - 1); }); + + it('returns undefined if self is not present in cursors', async ({ space }) => { + vi.spyOn(space.cursors, 'getAll').mockImplementation(async () => ({})); + + const self = await space.cursors.getSelf(); + expect(self).toBeUndefined(); + }); + + it('returns the cursor update for self', async ({ + space, + lastCursorPositionsStub, + selfStub, + }) => { + vi.spyOn(space.cursors, 'getAll').mockImplementation(async () => lastCursorPositionsStub); + vi.spyOn(space, 'getSelf').mockReturnValue(selfStub); + + const selfCursor = await space.cursors.getSelf(); + expect(selfCursor).toEqual(lastCursorPositionsStub['connectionId1']); + }); + + it('returns an empty object if self is not present in cursors', async ({ space }) => { + vi.spyOn(space.cursors, 'getAll').mockResolvedValue({}); + vi.spyOn(space, 'getSelf').mockReturnValue(undefined); + + const others = await space.cursors.getOthers(); + expect(others).toEqual({}); + }); + + it('returns an empty object if there are no other cursors', async ({ space, selfStub }) => { + const onlyMyCursor = { + connectionId1: { + connectionId: 'connectionId1', + clientId: 'clientId1', + data: { + color: 'blue', + }, + position: { + x: 2, + y: 3, + }, + }, + }; + + vi.spyOn(space.cursors, 'getAll').mockResolvedValue(onlyMyCursor); + vi.spyOn(space, 'getSelf').mockReturnValue(selfStub); + + const others = await space.cursors.getOthers(); + expect(others).toEqual({}); + }); + + it('returns an object of other cursors', async ({ + space, + selfStub, + lastCursorPositionsStub, + }) => { + vi.spyOn(space.cursors, 'getAll').mockResolvedValue(lastCursorPositionsStub); + vi.spyOn(space, 'getSelf').mockReturnValue(selfStub); + + const others = await space.cursors.getOthers(); + expect(others).toEqual({ + connectionId2: { + connectionId: 'connectionId2', + clientId: 'clientId2', + position: { + x: 25, + y: 44, + }, + data: undefined, + }, + connectionId3: { + connectionId: 'connectionId3', + clientId: 'clientId3', + position: { + x: 225, + y: 244, + }, + data: undefined, + }, + }); + }); }); }); diff --git a/src/Cursors.ts b/src/Cursors.ts index 229449ff..ad1a427b 100644 --- a/src/Cursors.ts +++ b/src/Cursors.ts @@ -14,18 +14,15 @@ import CursorHistory from './CursorHistory.js'; import { CURSOR_UPDATE } from './utilities/Constants.js'; import type { CursorsOptions, StrictCursorsOptions } from './options/CursorsOptions.js'; - +type ConnectionId = string; type CursorPosition = { x: number; y: number }; - type CursorData = Record; - type CursorUpdate = { clientId: string; connectionId: string; position: CursorPosition; data?: CursorData; }; - type CursorsEventMap = { cursorsUpdate: Record; }; @@ -161,6 +158,24 @@ export default class Cursors extends EventEmitter { const channel = this.getChannel(); return await this.cursorHistory.getLastCursorUpdate(channel, this.options.paginationLimit); } + + async getSelf(): Promise { + const self = this.space.getSelf(); + if (!self) return; + + const allCursors = await this.getAll(); + return allCursors[self.connectionId] as CursorUpdate; + } + + async getOthers(): Promise> { + const self = this.space.getSelf(); + if (!self) return {}; + + const allCursors = await this.getAll(); + const allCursorsFiltered = allCursors; + delete allCursorsFiltered[self.connectionId]; + return allCursorsFiltered; + } } export { type CursorPosition, type CursorData, type CursorUpdate };