From e4a464116e53d2f0297d425116e0f823da00b4d2 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Thu, 28 Sep 2023 15:50:38 -0300 Subject: [PATCH 01/30] Remove ability to pass array of event names to EventEmitter.prototype.once MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This copies across the change from commit cd4f7b3 in ably-js: > This functionality is implemented by wrapping the listener argument in > another listener. This means that the event emitter does not hold a > reference to the listener argument (other than that held indirectly > through the wrapper) and so it is not possible to remove this listener > using `off(..., listener)`. > > The client library specification does not specify a version of `once` > which accepts an array of event names, and we do not advertise it as > part of the public API. So, I think the simplest thing is to remove this > functionality. The removed code in the current commit also had a bug — the second argument in the removed code `self.off(eventName, this)` was incorrectly populated and this made the removed code equivalent to `self.off(eventName)`; that is, it removed _all_ listeners for the given event name. I believe that the removal of this code accounts for the increased expected number of calls to context.spy in one of the tests here. I’m not sure what reasoning led to the previous expected count of 3 (perhaps the expectation was written just based on the number of calls observed when running the code), but here’s my reasoning for the expectation of 4 calls: Before `context.eventEmitter['off']('myEvent', context.spy)`, the following calls are relevant to context.spy: - eventEmitter['on'](context.spy); - eventEmitter['on']('myEvent', context.spy); - eventEmitter['on'](['myEvent', 'myOtherEvent', 'myThirdEvent'], context.spy); - eventEmitter['once'](context.spy); - eventEmitter['once']('myEvent', context.spy); After `context.eventEmitter['off']('myEvent', context.spy)`, the following calls are relevant to context.spy: - eventEmitter['on'](context.spy); - eventEmitter['on'](['myEvent' /* no longer applies */, 'myOtherEvent', 'myThirdEvent'], context.spy); - eventEmitter['once'](context.spy); After `context.eventEmitter['emit']('myEvent', '')`, the following calls are relevant to context.spy: - eventEmitter['on'](context.spy); - eventEmitter['on'](['myEvent' /* no longer applies *\/, 'myOtherEvent', 'myThirdEvent'], context.spy); And hence calling `context.eventEmitter['emit']('myOtherEvent', '')` calls context.spy a further two times. --- src/utilities/EventEmitter.test.ts | 15 +------------- src/utilities/EventEmitter.ts | 33 +++++++----------------------- 2 files changed, 8 insertions(+), 40 deletions(-) diff --git a/src/utilities/EventEmitter.test.ts b/src/utilities/EventEmitter.test.ts index aa4c87f6..330102ea 100644 --- a/src/utilities/EventEmitter.test.ts +++ b/src/utilities/EventEmitter.test.ts @@ -138,7 +138,6 @@ describe('EventEmitter', () => { eventEmitter['once'](altListener); eventEmitter['once']('myEvent', context.spy); eventEmitter['once']('myEvent', altListener); - eventEmitter['once'](['myEvent', 'myOtherEvent', 'myThirdEvent'], altListener); context.eventEmitter = eventEmitter; }); @@ -171,7 +170,7 @@ describe('EventEmitter', () => { context.eventEmitter['emit']('myEvent', ''); expect(context.spy).toHaveBeenCalledTimes(2); context.eventEmitter['emit']('myOtherEvent', ''); - expect(context.spy).toHaveBeenCalledTimes(3); + expect(context.spy).toHaveBeenCalledTimes(4); }); it('removes a specific listener from multiple events', () => { @@ -254,18 +253,6 @@ describe('EventEmitter', () => { context.eventEmitter['emit']('myEvent', ''); expect(context.spy).toHaveBeenCalledOnce(); }); - - it('adds a listener to multiple eventOnce fields on calling `once` with a listener and event name; and after emitting any of the events, all are removed', (context) => { - context.eventEmitter['once'](['myEvent', 'myOtherEvent', 'myThirdEvent'], context.spy); - expect(context.eventEmitter['eventsOnce']['myEvent']).toHaveLength(1); - expect(context.eventEmitter['eventsOnce']['myOtherEvent']).toHaveLength(1); - expect(context.eventEmitter['eventsOnce']['myThirdEvent']).toHaveLength(1); - expect(context.eventEmitter['emit']('myEvent', '')); - expect(context.eventEmitter['eventsOnce']['myEvent']).toBe(undefined); - expect(context.eventEmitter['eventsOnce']['myOtherEvent']).toBe(undefined); - expect(context.eventEmitter['eventsOnce']['myThirdEvent']).toBe(undefined); - expect(context.spy).toHaveBeenCalledOnce(); - }); }); describe('calling the emit method', () => { diff --git a/src/utilities/EventEmitter.ts b/src/utilities/EventEmitter.ts index 247645a2..6c2d1101 100644 --- a/src/utilities/EventEmitter.ts +++ b/src/utilities/EventEmitter.ts @@ -223,46 +223,27 @@ export default class EventEmitter { /** * Listen for a single occurrence of an event - * @param listenerOrEvents (optional) the name of the event to listen to + * @param listenerOrEvent (optional) the name of the event to listen to * @param listener (optional) the listener to be called */ once>( - listenerOrEvents: K | K[] | EventListener, + listenerOrEvent: K | EventListener, listener?: EventListener, ): void | Promise { // .once("eventName", () => {}) - if (isString(listenerOrEvents) && isFunction(listener)) { - const listeners = this.eventsOnce[listenerOrEvents] || (this.eventsOnce[listenerOrEvents] = []); + if (isString(listenerOrEvent) && isFunction(listener)) { + const listeners = this.eventsOnce[listenerOrEvent] || (this.eventsOnce[listenerOrEvent] = []); listeners.push(listener); return; } - // .once(["eventName"], () => {}) - if (isArray(listenerOrEvents) && isFunction(listener)) { - const self = this; - - listenerOrEvents.forEach(function (eventName) { - const listenerWrapper: EventListener = function (this: EventListener, listenerThis) { - const innerArgs = Array.prototype.slice.call(arguments) as [params: T[K]]; - listenerOrEvents.forEach((eventName) => { - self.off(eventName, this); - }); - - listener.apply(listenerThis, innerArgs); - }; - self.once(eventName, listenerWrapper); - }); - - return; - } - // .once(() => {}) - if (isFunction(listenerOrEvents)) { - this.anyOnce.push(listenerOrEvents); + if (isFunction(listenerOrEvent)) { + this.anyOnce.push(listenerOrEvent); return; } - throw new InvalidArgumentError('EventEmitter.once(): invalid arguments:' + inspect([listenerOrEvents, listener])); + throw new InvalidArgumentError('EventEmitter.once(): invalid arguments:' + inspect([listenerOrEvent, listener])); } /** From a3cd2bf5d59bf5dc1096e572b1600ce9a1f9eff3 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Wed, 27 Sep 2023 15:20:54 -0300 Subject: [PATCH 02/30] Remove unused type `Provider` --- src/utilities/types.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/utilities/types.ts b/src/utilities/types.ts index cd3e2f5a..e1896cbf 100644 --- a/src/utilities/types.ts +++ b/src/utilities/types.ts @@ -1,6 +1,5 @@ import type { Types } from 'ably'; -import type { EventKey, EventListener, EventMap } from './EventEmitter.js'; import type { ProfileData, Lock } from '../types.js'; export type PresenceMember = { @@ -24,18 +23,6 @@ export type Subset = { [attr in keyof K]?: K[attr] extends object ? Subset : K[attr]; }; -export interface Provider { - subscribe>( - listenerOrEvents?: K | K[] | EventListener, - listener?: EventListener, - ): void; - - unsubscribe>( - listenerOrEvents?: K | K[] | EventListener, - listener?: EventListener, - ): void; -} - export type RealtimeMessage = Omit & { connectionId: string; }; From ad4e91602800b575836c9d65b04c3ae78b36750e Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Wed, 20 Sep 2023 16:13:27 -0300 Subject: [PATCH 03/30] Make naming of event map types consistent Make them all of the form "EventMap" prefixed by the name of the class that emits them. Note that the use of "EventMap" as opposed to "EventsMap" naming is also consistent with the naming used by TypeScript for the DOM event maps (e.g. [1]). [1] https://github.com/microsoft/TypeScript/blob/f3dad2a07d007c40d260f13d6b087a2d5685d12d/src/lib/dom.generated.d.ts#L2418-L2422 --- src/Locks.ts | 16 ++++++++-------- src/Members.ts | 16 ++++++++-------- src/Space.ts | 16 ++++++++-------- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/Locks.ts b/src/Locks.ts index 8e6eba2f..b81ea078 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -23,11 +23,11 @@ interface LockOptions { attributes: LockAttributes; } -type LockEventMap = { +type LocksEventMap = { update: Lock; }; -export default class Locks extends EventEmitter { +export default class Locks extends EventEmitter { // locks tracks the local state of locks, which is used to determine whether // a lock's status has changed when processing presence updates. // @@ -135,9 +135,9 @@ export default class Locks extends EventEmitter { this.deleteLock(id, self.connectionId); } - subscribe>( - listenerOrEvents?: K | K[] | EventListener, - listener?: EventListener, + subscribe>( + listenerOrEvents?: K | K[] | EventListener, + listener?: EventListener, ) { try { super.on(listenerOrEvents, listener); @@ -152,9 +152,9 @@ export default class Locks extends EventEmitter { } } - unsubscribe>( - listenerOrEvents?: K | K[] | EventListener, - listener?: EventListener, + unsubscribe>( + listenerOrEvents?: K | K[] | EventListener, + listener?: EventListener, ) { try { super.off(listenerOrEvents, listener); diff --git a/src/Members.ts b/src/Members.ts index 2edae1f3..6ca88b73 100644 --- a/src/Members.ts +++ b/src/Members.ts @@ -10,7 +10,7 @@ import type { SpaceMember } from './types.js'; import type { PresenceMember } from './utilities/types.js'; import type Space from './Space.js'; -type MemberEventsMap = { +type MembersEventMap = { leave: SpaceMember; enter: SpaceMember; update: SpaceMember; @@ -18,7 +18,7 @@ type MemberEventsMap = { remove: SpaceMember; }; -class Members extends EventEmitter { +class Members extends EventEmitter { private lastMemberUpdate: Record = {}; private leavers: Leavers; @@ -69,9 +69,9 @@ class Members extends EventEmitter { return members.filter((m) => m.connectionId !== this.space.connectionId); } - subscribe>( - listenerOrEvents?: K | K[] | EventListener, - listener?: EventListener, + subscribe>( + listenerOrEvents?: K | K[] | EventListener, + listener?: EventListener, ) { try { super.on(listenerOrEvents, listener); @@ -86,9 +86,9 @@ class Members extends EventEmitter { } } - unsubscribe>( - listenerOrEvents?: K | K[] | EventListener, - listener?: EventListener, + unsubscribe>( + listenerOrEvents?: K | K[] | EventListener, + listener?: EventListener, ) { try { super.off(listenerOrEvents, listener); diff --git a/src/Space.ts b/src/Space.ts index 50dec7d2..a93b9959 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -29,11 +29,11 @@ const SPACE_OPTIONS_DEFAULTS = { }, }; -type SpaceEventsMap = { +type SpaceEventMap = { update: { members: SpaceMember[] }; }; -class Space extends EventEmitter { +class Space extends EventEmitter { readonly channelName: string; readonly connectionId: string | undefined; readonly options: SpaceOptions; @@ -177,9 +177,9 @@ class Space extends EventEmitter { return { members }; } - subscribe>( - listenerOrEvents?: K | K[] | EventListener, - listener?: EventListener, + subscribe>( + listenerOrEvents?: K | K[] | EventListener, + listener?: EventListener, ) { try { super.on(listenerOrEvents, listener); @@ -194,9 +194,9 @@ class Space extends EventEmitter { } } - unsubscribe>( - listenerOrEvents?: K | K[] | EventListener, - listener?: EventListener, + unsubscribe>( + listenerOrEvents?: K | K[] | EventListener, + listener?: EventListener, ) { try { super.off(listenerOrEvents, listener); From c51b23e26d42eddf8847eb7dbe122b4a1963ab04 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Wed, 20 Sep 2023 15:03:37 -0300 Subject: [PATCH 04/30] =?UTF-8?q?Mark=20non-public=20API=20where=20we=20ha?= =?UTF-8?q?ven=E2=80=99t=20already?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I am shortly going to be adding documentation comments to the codebase, but only for public API. I’ll then use TypeDoc to generate documentation based on these comments. So, to avoid TypeDoc errors of the form "X does not have any documentation" for non-public API, I’m marking the non-public API as `private` or `@internal`, and will configure TypeDoc to ignore any such-marked items. My guessing of what constitutes public API is done based on what’s mentioned in the documentation on ably.com and what’s used in the demo code bundled in this repo. --- src/Cursors.ts | 1 + src/Locations.ts | 2 ++ src/Locks.ts | 12 ++++++++---- src/Members.ts | 7 +++++-- src/Space.ts | 3 ++- src/utilities/EventEmitter.ts | 11 ++++++++++- 6 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/Cursors.ts b/src/Cursors.ts index e3318bc2..003e8f49 100644 --- a/src/Cursors.ts +++ b/src/Cursors.ts @@ -31,6 +31,7 @@ export default class Cursors extends EventEmitter { public channel?: Types.RealtimeChannelPromise; + /** @internal */ constructor(private space: Space) { super(); diff --git a/src/Locations.ts b/src/Locations.ts index f2b57106..58936210 100644 --- a/src/Locations.ts +++ b/src/Locations.ts @@ -18,10 +18,12 @@ type LocationsEventMap = { export default class Locations extends EventEmitter { private lastLocationUpdate: Record = {}; + /** @internal */ constructor(private space: Space, private presenceUpdate: Space['presenceUpdate']) { super(); } + /** @internal */ async processPresenceMessage(message: PresenceMember) { // Only an update action is currently a valid location update. if (message.action !== 'update') return; diff --git a/src/Locks.ts b/src/Locks.ts index b81ea078..315acd20 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -36,6 +36,7 @@ export default class Locks extends EventEmitter { // have requested. private locks: Map>; + /** @internal */ constructor(private space: Space, private presenceUpdate: Space['presenceUpdate']) { super(); this.locks = new Map(); @@ -169,6 +170,7 @@ export default class Locks extends EventEmitter { } } + /** @internal */ async processPresenceMessage(message: Types.PresenceMessage) { const member = await this.space.members.getByConnectionId(message.connectionId); if (!member) return; @@ -222,7 +224,7 @@ export default class Locks extends EventEmitter { // // TODO: remove this once the Ably system processes PENDING requests // internally using this same logic. - processPending(member: SpaceMember, pendingLock: Lock) { + private processPending(member: SpaceMember, pendingLock: Lock) { // if the requested lock ID is not currently locked, then mark the PENDING // lock request as LOCKED const lock = this.get(pendingLock.id); @@ -266,18 +268,19 @@ export default class Locks extends EventEmitter { pendingLock.reason = ERR_LOCK_IS_LOCKED(); } - updatePresence(self: SpaceMember) { + private updatePresence(self: SpaceMember) { const update = new SpaceUpdate({ self, extras: this.getLockExtras(self.connectionId) }); return this.presenceUpdate(update.noop()); } + /** @internal */ getLock(id: string, connectionId: string): Lock | undefined { const locks = this.locks.get(id); if (!locks) return; return locks.get(connectionId); } - setLock(lock: Lock) { + private setLock(lock: Lock) { let locks = this.locks.get(lock.id); if (!locks) { locks = new Map(); @@ -286,7 +289,7 @@ export default class Locks extends EventEmitter { locks.set(lock.member.connectionId, lock); } - deleteLock(id: string, connectionId: string) { + private deleteLock(id: string, connectionId: string) { const locks = this.locks.get(id); if (!locks) return; return locks.delete(connectionId); @@ -306,6 +309,7 @@ export default class Locks extends EventEmitter { return requests; } + /** @internal */ getLockExtras(connectionId: string): PresenceMember['extras'] { const locks = this.getLocksForConnectionId(connectionId); if (locks.length === 0) return; diff --git a/src/Members.ts b/src/Members.ts index 6ca88b73..6c3b9611 100644 --- a/src/Members.ts +++ b/src/Members.ts @@ -22,11 +22,13 @@ class Members extends EventEmitter { private lastMemberUpdate: Record = {}; private leavers: Leavers; + /** @internal */ constructor(private space: Space) { super(); this.leavers = new Leavers(this.space.options.offlineTimeout); } + /** @internal */ async processPresenceMessage(message: PresenceMember) { const { action, connectionId } = message; const isLeaver = !!this.leavers.getByConnectionId(connectionId); @@ -103,12 +105,13 @@ class Members extends EventEmitter { } } + /** @internal */ async getByConnectionId(connectionId: string): Promise { const members = await this.getAll(); return members.find((m) => m.connectionId === connectionId) ?? null; } - createMember(message: PresenceMember): SpaceMember { + private createMember(message: PresenceMember): SpaceMember { return { clientId: message.clientId, connectionId: message.connectionId, @@ -122,7 +125,7 @@ class Members extends EventEmitter { }; } - async onMemberOffline(member: SpaceMember) { + private async onMemberOffline(member: SpaceMember) { this.leavers.removeLeaver(member.connectionId); this.emit('remove', member); diff --git a/src/Space.ts b/src/Space.ts index a93b9959..a82c6327 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -34,7 +34,7 @@ type SpaceEventMap = { }; class Space extends EventEmitter { - readonly channelName: string; + private readonly channelName: string; readonly connectionId: string | undefined; readonly options: SpaceOptions; readonly locations: Locations; @@ -44,6 +44,7 @@ class Space extends EventEmitter { readonly locks: Locks; readonly name: string; + /** @internal */ constructor(name: string, readonly client: Types.RealtimePromise, options?: Subset) { super(); diff --git a/src/utilities/EventEmitter.ts b/src/utilities/EventEmitter.ts index 6c2d1101..f23289a8 100644 --- a/src/utilities/EventEmitter.ts +++ b/src/utilities/EventEmitter.ts @@ -70,11 +70,18 @@ export type EventKey = string & keyof T; export type EventListener = (params: T) => void; export default class EventEmitter { + /** @internal */ any: Array; + /** @internal */ events: Record; + /** @internal */ anyOnce: Array; + /** @internal */ eventsOnce: Record; + /** + * @internal + */ constructor() { this.any = []; this.events = Object.create(null); @@ -188,6 +195,8 @@ export default class EventEmitter { } /** + * @internal + * * Emit an event * @param event the event name * @param arg the arguments to pass to the listener @@ -247,7 +256,7 @@ export default class EventEmitter { } /** - * Private API + * @internal * * Listen for a single occurrence of a state event and fire immediately if currentState matches targetState * @param targetState the name of the state event to listen to From aaf47a21725b17842b2f8e6ceba42e9801a716aa Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Wed, 20 Sep 2023 14:43:31 -0300 Subject: [PATCH 05/30] Export all types that are referenced by exported types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As mentioned in ad4e916, I’ll be adding TypeDoc to the project. The aim of the change in this commit is to avoid TypeDoc warnings of the form "X (...) is referenced by Y but not included in the documentation." --- src/Cursors.ts | 2 +- src/Locations.ts | 2 +- src/Locks.ts | 4 ++-- src/Members.ts | 2 +- src/Space.ts | 2 +- src/index.ts | 11 ++++++++++- 6 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/Cursors.ts b/src/Cursors.ts index 003e8f49..3a823169 100644 --- a/src/Cursors.ts +++ b/src/Cursors.ts @@ -16,7 +16,7 @@ import type { CursorsOptions, CursorUpdate } from './types.js'; import type { RealtimeMessage } from './utilities/types.js'; import { ERR_NOT_ENTERED_SPACE } from './Errors.js'; -type CursorsEventMap = { +export type CursorsEventMap = { update: CursorUpdate; }; diff --git a/src/Locations.ts b/src/Locations.ts index 58936210..8ae53523 100644 --- a/src/Locations.ts +++ b/src/Locations.ts @@ -11,7 +11,7 @@ import type Space from './Space.js'; import { ERR_NOT_ENTERED_SPACE } from './Errors.js'; import SpaceUpdate from './SpaceUpdate.js'; -type LocationsEventMap = { +export type LocationsEventMap = { update: { member: SpaceMember; currentLocation: unknown; previousLocation: unknown }; }; diff --git a/src/Locks.ts b/src/Locks.ts index 315acd20..2f513f70 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -19,11 +19,11 @@ export class LockAttributes extends Map { } } -interface LockOptions { +export interface LockOptions { attributes: LockAttributes; } -type LocksEventMap = { +export type LocksEventMap = { update: Lock; }; diff --git a/src/Members.ts b/src/Members.ts index 6c3b9611..67330e38 100644 --- a/src/Members.ts +++ b/src/Members.ts @@ -10,7 +10,7 @@ import type { SpaceMember } from './types.js'; import type { PresenceMember } from './utilities/types.js'; import type Space from './Space.js'; -type MembersEventMap = { +export type MembersEventMap = { leave: SpaceMember; enter: SpaceMember; update: SpaceMember; diff --git a/src/Space.ts b/src/Space.ts index a82c6327..511ed439 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -29,7 +29,7 @@ const SPACE_OPTIONS_DEFAULTS = { }, }; -type SpaceEventMap = { +export type SpaceEventMap = { update: { members: SpaceMember[] }; }; diff --git a/src/index.ts b/src/index.ts index f51aacdb..a6a8fec1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,11 @@ import Spaces from './Spaces.js'; -export type Space = Awaited>; +export type { default as Space, SpaceEventMap } from './Space.js'; + +export type { default as Cursors, CursorsEventMap } from './Cursors.js'; +export type { default as Locations, LocationsEventMap } from './Locations.js'; +export type { default as Locks, LocksEventMap, LockOptions } from './Locks.js'; +export type { default as Members, MembersEventMap } from './Members.js'; // Can be changed to * when we update to TS5 @@ -19,3 +24,7 @@ export type { } from './types.js'; export { LockAttributes } from './Locks.js'; + +export type { default as EventEmitter, EventListener, EventKey, EventMap } from './utilities/EventEmitter.js'; + +export type { Subset } from './utilities/types.js'; From f4a949882d329e05d53d7c76971e0d843f98482c Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Thu, 21 Sep 2023 13:56:25 -0300 Subject: [PATCH 06/30] Extract a couple of events to their own types So that I can write documentation comments for them. --- src/Locations.ts | 10 +++++++++- src/Space.ts | 8 +++++++- src/index.ts | 4 ++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/Locations.ts b/src/Locations.ts index 8ae53523..b2ac3d27 100644 --- a/src/Locations.ts +++ b/src/Locations.ts @@ -11,8 +11,16 @@ import type Space from './Space.js'; import { ERR_NOT_ENTERED_SPACE } from './Errors.js'; import SpaceUpdate from './SpaceUpdate.js'; +export namespace LocationsEvents { + export interface UpdateEvent { + member: SpaceMember; + currentLocation: unknown; + previousLocation: unknown; + } +} + export type LocationsEventMap = { - update: { member: SpaceMember; currentLocation: unknown; previousLocation: unknown }; + update: LocationsEvents.UpdateEvent; }; export default class Locations extends EventEmitter { diff --git a/src/Space.ts b/src/Space.ts index 511ed439..7a4bebc8 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -29,8 +29,14 @@ const SPACE_OPTIONS_DEFAULTS = { }, }; +export namespace SpaceEvents { + export interface UpdateEvent { + members: SpaceMember[]; + } +} + export type SpaceEventMap = { - update: { members: SpaceMember[] }; + update: SpaceEvents.UpdateEvent; }; class Space extends EventEmitter { diff --git a/src/index.ts b/src/index.ts index a6a8fec1..1a7da0ca 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,9 @@ import Spaces from './Spaces.js'; -export type { default as Space, SpaceEventMap } from './Space.js'; +export type { default as Space, SpaceEventMap, SpaceEvents } from './Space.js'; export type { default as Cursors, CursorsEventMap } from './Cursors.js'; -export type { default as Locations, LocationsEventMap } from './Locations.js'; +export type { default as Locations, LocationsEventMap, LocationsEvents } from './Locations.js'; export type { default as Locks, LocksEventMap, LockOptions } from './Locks.js'; export type { default as Members, MembersEventMap } from './Members.js'; From 9e27b5e7b6518a1e964760e154de5927a3ceae03 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Wed, 27 Sep 2023 15:27:14 -0300 Subject: [PATCH 07/30] Get rid of EventMap and EventKey types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I want to convert the *EventMap types from types to interfaces, but the EventMap type complicates this. So, let’s get rid of it and the accompanying EventKey type. I can’t see a compelling reason for their existence. Removing them also means fewer types for users to understand, and fewer types for us to document. --- src/Cursors.ts | 13 ++++------ src/Locations.ts | 11 +++------ src/Locks.ts | 11 +++------ src/Members.ts | 11 +++------ src/Space.ts | 11 +++------ src/index.ts | 2 +- src/utilities/EventEmitter.test.ts | 2 +- src/utilities/EventEmitter.ts | 38 ++++++++++++++++-------------- src/utilities/is.ts | 2 +- 9 files changed, 39 insertions(+), 62 deletions(-) diff --git a/src/Cursors.ts b/src/Cursors.ts index 3a823169..da9cb764 100644 --- a/src/Cursors.ts +++ b/src/Cursors.ts @@ -3,12 +3,7 @@ import { Types } from 'ably'; import Space from './Space.js'; import CursorBatching from './CursorBatching.js'; import CursorDispensing from './CursorDispensing.js'; -import EventEmitter, { - InvalidArgumentError, - inspect, - type EventKey, - type EventListener, -} from './utilities/EventEmitter.js'; +import EventEmitter, { InvalidArgumentError, inspect, type EventListener } from './utilities/EventEmitter.js'; import CursorHistory from './CursorHistory.js'; import { CURSOR_UPDATE } from './CursorConstants.js'; @@ -91,7 +86,7 @@ export default class Cursors extends EventEmitter { return !this.emitterHasListeners(subscriptions); } - private emitterHasListeners = (emitter: EventEmitter<{}>) => { + private emitterHasListeners = (emitter: EventEmitter) => { const flattenEvents = (obj: Record) => Object.entries(obj) .map((_, v) => v) @@ -105,7 +100,7 @@ export default class Cursors extends EventEmitter { ); }; - subscribe>( + subscribe( listenerOrEvents?: K | K[] | EventListener, listener?: EventListener, ) { @@ -130,7 +125,7 @@ export default class Cursors extends EventEmitter { } } - unsubscribe>( + unsubscribe( listenerOrEvents?: K | K[] | EventListener, listener?: EventListener, ) { diff --git a/src/Locations.ts b/src/Locations.ts index b2ac3d27..a5118786 100644 --- a/src/Locations.ts +++ b/src/Locations.ts @@ -1,9 +1,4 @@ -import EventEmitter, { - InvalidArgumentError, - inspect, - type EventKey, - type EventListener, -} from './utilities/EventEmitter.js'; +import EventEmitter, { InvalidArgumentError, inspect, type EventListener } from './utilities/EventEmitter.js'; import type { SpaceMember } from './types.js'; import type { PresenceMember } from './utilities/types.js'; @@ -71,7 +66,7 @@ export default class Locations extends EventEmitter { await this.presenceUpdate(update.updateLocation(location)); } - subscribe>( + subscribe( listenerOrEvents?: K | K[] | EventListener, listener?: EventListener, ) { @@ -88,7 +83,7 @@ export default class Locations extends EventEmitter { } } - unsubscribe>( + unsubscribe( listenerOrEvents?: K | K[] | EventListener, listener?: EventListener, ) { diff --git a/src/Locks.ts b/src/Locks.ts index 2f513f70..5b646a39 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -4,12 +4,7 @@ import Space from './Space.js'; import type { Lock, SpaceMember } from './types.js'; import type { PresenceMember } from './utilities/types.js'; import { ERR_LOCK_IS_LOCKED, ERR_LOCK_INVALIDATED, ERR_LOCK_REQUEST_EXISTS, ERR_NOT_ENTERED_SPACE } from './Errors.js'; -import EventEmitter, { - InvalidArgumentError, - inspect, - type EventKey, - type EventListener, -} from './utilities/EventEmitter.js'; +import EventEmitter, { InvalidArgumentError, inspect, type EventListener } from './utilities/EventEmitter.js'; import SpaceUpdate from './SpaceUpdate.js'; @@ -136,7 +131,7 @@ export default class Locks extends EventEmitter { this.deleteLock(id, self.connectionId); } - subscribe>( + subscribe( listenerOrEvents?: K | K[] | EventListener, listener?: EventListener, ) { @@ -153,7 +148,7 @@ export default class Locks extends EventEmitter { } } - unsubscribe>( + unsubscribe( listenerOrEvents?: K | K[] | EventListener, listener?: EventListener, ) { diff --git a/src/Members.ts b/src/Members.ts index 67330e38..f0461f4e 100644 --- a/src/Members.ts +++ b/src/Members.ts @@ -1,9 +1,4 @@ -import EventEmitter, { - InvalidArgumentError, - inspect, - type EventKey, - type EventListener, -} from './utilities/EventEmitter.js'; +import EventEmitter, { InvalidArgumentError, inspect, type EventListener } from './utilities/EventEmitter.js'; import Leavers from './Leavers.js'; import type { SpaceMember } from './types.js'; @@ -71,7 +66,7 @@ class Members extends EventEmitter { return members.filter((m) => m.connectionId !== this.space.connectionId); } - subscribe>( + subscribe( listenerOrEvents?: K | K[] | EventListener, listener?: EventListener, ) { @@ -88,7 +83,7 @@ class Members extends EventEmitter { } } - unsubscribe>( + unsubscribe( listenerOrEvents?: K | K[] | EventListener, listener?: EventListener, ) { diff --git a/src/Space.ts b/src/Space.ts index 7a4bebc8..8e8c26d4 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -1,11 +1,6 @@ import Ably, { Types } from 'ably'; -import EventEmitter, { - InvalidArgumentError, - inspect, - type EventKey, - type EventListener, -} from './utilities/EventEmitter.js'; +import EventEmitter, { InvalidArgumentError, inspect, type EventListener } from './utilities/EventEmitter.js'; import Locations from './Locations.js'; import Cursors from './Cursors.js'; import Members from './Members.js'; @@ -184,7 +179,7 @@ class Space extends EventEmitter { return { members }; } - subscribe>( + subscribe( listenerOrEvents?: K | K[] | EventListener, listener?: EventListener, ) { @@ -201,7 +196,7 @@ class Space extends EventEmitter { } } - unsubscribe>( + unsubscribe( listenerOrEvents?: K | K[] | EventListener, listener?: EventListener, ) { diff --git a/src/index.ts b/src/index.ts index 1a7da0ca..f4c1c17c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,6 +25,6 @@ export type { export { LockAttributes } from './Locks.js'; -export type { default as EventEmitter, EventListener, EventKey, EventMap } from './utilities/EventEmitter.js'; +export type { default as EventEmitter, EventListener } from './utilities/EventEmitter.js'; export type { Subset } from './utilities/types.js'; diff --git a/src/utilities/EventEmitter.test.ts b/src/utilities/EventEmitter.test.ts index 330102ea..8e2a62e6 100644 --- a/src/utilities/EventEmitter.test.ts +++ b/src/utilities/EventEmitter.test.ts @@ -174,7 +174,7 @@ describe('EventEmitter', () => { }); it('removes a specific listener from multiple events', () => { - const eventEmitter = new EventEmitter(); + const eventEmitter = new EventEmitter<{ myEvent: unknown; myOtherEvent: unknown; myThirdEvent: unknown }>(); const specificListener = vi.fn(); eventEmitter['on'](['myEvent', 'myOtherEvent', 'myThirdEvent'], specificListener); eventEmitter['off'](['myEvent', 'myOtherEvent'], specificListener); diff --git a/src/utilities/EventEmitter.ts b/src/utilities/EventEmitter.ts index f23289a8..4ffa4929 100644 --- a/src/utilities/EventEmitter.ts +++ b/src/utilities/EventEmitter.ts @@ -1,6 +1,6 @@ import { isArray, isFunction, isObject, isString } from './is.js'; -function callListener(eventThis: { event: string }, listener: Function, args: unknown[]) { +function callListener(eventThis: { event: K }, listener: Function, args: unknown[]) { try { listener.apply(eventThis, args); } catch (e) { @@ -17,14 +17,14 @@ function callListener(eventThis: { event: string }, listener: Function, args: un * @param listener the listener callback to remove * @param eventFilter (optional) event name instructing the function to only remove listeners for the specified event */ -export function removeListener( - targetListeners: (Function[] | Record)[], +export function removeListener( + targetListeners: (Function[] | Record)[], listener: Function, - eventFilter?: string, + eventFilter?: keyof T, ) { - let listeners: Function[] | Record; + let listeners: Function[] | Record; let index: number; - let eventName: string; + let eventName: keyof T; for (let targetListenersIndex = 0; targetListenersIndex < targetListeners.length; targetListenersIndex++) { listeners = targetListeners[targetListenersIndex]; @@ -64,20 +64,17 @@ export class InvalidArgumentError extends Error { } } -export type EventMap = Record; -// extract all the keys of an event map and use them as a type -export type EventKey = string & keyof T; export type EventListener = (params: T) => void; -export default class EventEmitter { +export default class EventEmitter { /** @internal */ any: Array; /** @internal */ - events: Record; + events: Record; /** @internal */ anyOnce: Array; /** @internal */ - eventsOnce: Record; + eventsOnce: Record; /** * @internal @@ -94,7 +91,7 @@ export default class EventEmitter { * @param listenerOrEvents (optional) the name of the event to listen to or the listener to be called. * @param listener (optional) the listener to be called. */ - on>(listenerOrEvents?: K | K[] | EventListener, listener?: EventListener): void { + on(listenerOrEvents?: K | K[] | EventListener, listener?: EventListener): void { // .on(() => {}) if (isFunction(listenerOrEvents)) { this.any.push(listenerOrEvents); @@ -125,7 +122,7 @@ export default class EventEmitter { * the listener is treated as an 'any' listener. * @param listener (optional) the listener to remove. If not supplied, all listeners are removed. */ - off>(listenerOrEvents?: K | K[] | EventListener, listener?: EventListener): void { + off(listenerOrEvents?: K | K[] | EventListener, listener?: EventListener): void { // .off() // don't use arguments.length === 0 here as don't won't handle // cases like .off(undefined) which is a valid call @@ -180,7 +177,7 @@ export default class EventEmitter { * @param event (optional) the name of the event, or none for 'any' * @return array of events, or null if none */ - listeners>(event: K): Function[] | null { + listeners(event: K): Function[] | null { if (event) { const listeners = [...(this.events[event] ?? [])]; @@ -201,7 +198,7 @@ export default class EventEmitter { * @param event the event name * @param arg the arguments to pass to the listener */ - emit>(event: K, arg: T[K]) { + emit(event: K, arg: T[K]) { const eventThis = { event }; const listeners: Function[] = []; @@ -235,7 +232,7 @@ export default class EventEmitter { * @param listenerOrEvent (optional) the name of the event to listen to * @param listener (optional) the listener to be called */ - once>( + once( listenerOrEvent: K | EventListener, listener?: EventListener, ): void | Promise { @@ -264,7 +261,12 @@ export default class EventEmitter { * @param listener the listener to be called * @param listenerArgs */ - whenState(targetState: string, currentState: string, listener: EventListener, ...listenerArgs: unknown[]) { + whenState( + targetState: keyof T, + currentState: keyof T, + listener: EventListener, + ...listenerArgs: unknown[] + ) { const eventThis = { event: targetState }; if (typeof targetState !== 'string' || typeof currentState !== 'string') { diff --git a/src/utilities/is.ts b/src/utilities/is.ts index 8a6fd0d3..5e3e4435 100644 --- a/src/utilities/is.ts +++ b/src/utilities/is.ts @@ -11,7 +11,7 @@ function isFunction(arg: unknown): arg is Function { return ['Function', 'AsyncFunction', 'GeneratorFunction', 'Proxy'].includes(typeOf(arg)); } -function isString(arg: unknown): arg is String { +function isString(arg: unknown): arg is string { return typeOf(arg) === 'String'; } From b70d185ba6de12294d7198a7fb9c98f756630ce3 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Wed, 27 Sep 2023 20:52:07 -0300 Subject: [PATCH 08/30] Improve typing of callListener function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change `listener`’s type from Function to EventListener, and make it clearer that an EventListener takes a single argument. --- src/utilities/EventEmitter.ts | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/src/utilities/EventEmitter.ts b/src/utilities/EventEmitter.ts index 4ffa4929..c34fae2a 100644 --- a/src/utilities/EventEmitter.ts +++ b/src/utilities/EventEmitter.ts @@ -1,8 +1,8 @@ import { isArray, isFunction, isObject, isString } from './is.js'; -function callListener(eventThis: { event: K }, listener: Function, args: unknown[]) { +function callListener(eventThis: { event: K }, listener: EventListener, arg: T[K]) { try { - listener.apply(eventThis, args); + listener.apply(eventThis, [arg]); } catch (e) { console.error( 'EventEmitter.emit()', @@ -64,7 +64,7 @@ export class InvalidArgumentError extends Error { } } -export type EventListener = (params: T) => void; +export type EventListener = (param: T) => void; export default class EventEmitter { /** @internal */ @@ -200,7 +200,7 @@ export default class EventEmitter { */ emit(event: K, arg: T[K]) { const eventThis = { event }; - const listeners: Function[] = []; + const listeners: EventListener[] = []; if (this.anyOnce.length > 0) { Array.prototype.push.apply(listeners, this.anyOnce); @@ -223,7 +223,7 @@ export default class EventEmitter { } listeners.forEach(function (listener) { - callListener(eventThis, listener, [arg]); + callListener(eventThis, listener, arg); }); } @@ -259,13 +259,13 @@ export default class EventEmitter { * @param targetState the name of the state event to listen to * @param currentState the name of the current state of this object * @param listener the listener to be called - * @param listenerArgs + * @param listenerArg the argument to pass to the listener */ - whenState( - targetState: keyof T, + whenState( + targetState: K, currentState: keyof T, - listener: EventListener, - ...listenerArgs: unknown[] + listener: EventListener, + listenerArg: T[K], ) { const eventThis = { event: targetState }; @@ -274,14 +274,11 @@ export default class EventEmitter { } if (typeof listener !== 'function' && Promise) { return new Promise((resolve) => { - EventEmitter.prototype.whenState.apply( - this, - [targetState, currentState, resolve].concat(listenerArgs as any[]) as any, - ); + EventEmitter.prototype.whenState.apply(this, [targetState, currentState, resolve, listenerArg]); }); } if (targetState === currentState) { - callListener(eventThis, listener, listenerArgs); + callListener(eventThis, listener, listenerArg); } else { this.once(targetState, listener); } From 18f1d2bb90b45ec100d03163796a809271430716 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Thu, 21 Sep 2023 15:26:58 -0300 Subject: [PATCH 09/30] Convert *EventMap types to interfaces So that I can use {@link} in documentation comments to link to the documentation of each of their properties. --- src/Cursors.ts | 4 ++-- src/Locations.ts | 4 ++-- src/Locks.ts | 4 ++-- src/Members.ts | 4 ++-- src/Space.ts | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Cursors.ts b/src/Cursors.ts index da9cb764..7786f445 100644 --- a/src/Cursors.ts +++ b/src/Cursors.ts @@ -11,9 +11,9 @@ import type { CursorsOptions, CursorUpdate } from './types.js'; import type { RealtimeMessage } from './utilities/types.js'; import { ERR_NOT_ENTERED_SPACE } from './Errors.js'; -export type CursorsEventMap = { +export interface CursorsEventMap { update: CursorUpdate; -}; +} const CURSORS_CHANNEL_TAG = '::$cursors'; diff --git a/src/Locations.ts b/src/Locations.ts index a5118786..9aa10b07 100644 --- a/src/Locations.ts +++ b/src/Locations.ts @@ -14,9 +14,9 @@ export namespace LocationsEvents { } } -export type LocationsEventMap = { +export interface LocationsEventMap { update: LocationsEvents.UpdateEvent; -}; +} export default class Locations extends EventEmitter { private lastLocationUpdate: Record = {}; diff --git a/src/Locks.ts b/src/Locks.ts index 5b646a39..4ba0ac57 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -18,9 +18,9 @@ export interface LockOptions { attributes: LockAttributes; } -export type LocksEventMap = { +export interface LocksEventMap { update: Lock; -}; +} export default class Locks extends EventEmitter { // locks tracks the local state of locks, which is used to determine whether diff --git a/src/Members.ts b/src/Members.ts index f0461f4e..9243debb 100644 --- a/src/Members.ts +++ b/src/Members.ts @@ -5,13 +5,13 @@ import type { SpaceMember } from './types.js'; import type { PresenceMember } from './utilities/types.js'; import type Space from './Space.js'; -export type MembersEventMap = { +export interface MembersEventMap { leave: SpaceMember; enter: SpaceMember; update: SpaceMember; updateProfile: SpaceMember; remove: SpaceMember; -}; +} class Members extends EventEmitter { private lastMemberUpdate: Record = {}; diff --git a/src/Space.ts b/src/Space.ts index 8e8c26d4..4989abcc 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -30,9 +30,9 @@ export namespace SpaceEvents { } } -export type SpaceEventMap = { +export interface SpaceEventMap { update: SpaceEvents.UpdateEvent; -}; +} class Space extends EventEmitter { private readonly channelName: string; From 87e1eba4d394ac0b1a8b0cb2c0ecba08de75cdfc Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Tue, 26 Sep 2023 15:21:58 -0300 Subject: [PATCH 10/30] =?UTF-8?q?Split=20LockStatus=E2=80=99s=20allowed=20?= =?UTF-8?q?values=20into=20separate=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit So that they can be individually documented. --- src/index.ts | 1 + src/types.ts | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index f4c1c17c..ffb2b1b4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,6 +21,7 @@ export type { SpaceMember, Lock, LockStatus, + LockStatuses, } from './types.js'; export { LockAttributes } from './Locks.js'; diff --git a/src/types.ts b/src/types.ts index 9641b53c..96a16ba7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -39,7 +39,13 @@ export interface SpaceMember { }; } -export type LockStatus = 'pending' | 'locked' | 'unlocked'; +export namespace LockStatuses { + export type Pending = 'pending'; + export type Locked = 'locked'; + export type Unlocked = 'unlocked'; +} + +export type LockStatus = LockStatuses.Pending | LockStatuses.Locked | LockStatuses.Unlocked; export type Lock = { id: string; From f5a8b6ea082b2a6d3fe01d9f517402025e2ed22f Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Wed, 27 Sep 2023 14:50:02 -0300 Subject: [PATCH 11/30] Create overload signatures for event-related methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The subscribe, unsubscribe, … etc methods are easier to understand (and easier to document) when thought of as two variants: one which accepts one or more event names, and one which doesn’t. So create these overload signatures. --- src/Cursors.ts | 10 ++++++++ src/Locations.ts | 10 ++++++++ src/Locks.ts | 4 +++ src/Members.ts | 10 ++++++++ src/Space.ts | 4 +++ src/utilities/EventEmitter.ts | 46 ++++++++++++++++++++++++++++++++--- 6 files changed, 80 insertions(+), 4 deletions(-) diff --git a/src/Cursors.ts b/src/Cursors.ts index 7786f445..0b1f5b59 100644 --- a/src/Cursors.ts +++ b/src/Cursors.ts @@ -100,6 +100,11 @@ export default class Cursors extends EventEmitter { ); }; + subscribe( + eventOrEvents: K | K[], + listener?: EventListener, + ): void; + subscribe(listener?: EventListener): void; subscribe( listenerOrEvents?: K | K[] | EventListener, listener?: EventListener, @@ -125,6 +130,11 @@ export default class Cursors extends EventEmitter { } } + unsubscribe( + eventOrEvents: K | K[], + listener?: EventListener, + ): void; + unsubscribe(listener?: EventListener): void; unsubscribe( listenerOrEvents?: K | K[] | EventListener, listener?: EventListener, diff --git a/src/Locations.ts b/src/Locations.ts index 9aa10b07..1a8cc804 100644 --- a/src/Locations.ts +++ b/src/Locations.ts @@ -66,6 +66,11 @@ export default class Locations extends EventEmitter { await this.presenceUpdate(update.updateLocation(location)); } + subscribe( + eventOrEvents: K | K[], + listener?: EventListener, + ): void; + subscribe(listener?: EventListener): void; subscribe( listenerOrEvents?: K | K[] | EventListener, listener?: EventListener, @@ -83,6 +88,11 @@ export default class Locations extends EventEmitter { } } + unsubscribe( + eventOrEvents: K | K[], + listener?: EventListener, + ): void; + unsubscribe(listener?: EventListener): void; unsubscribe( listenerOrEvents?: K | K[] | EventListener, listener?: EventListener, diff --git a/src/Locks.ts b/src/Locks.ts index 4ba0ac57..1ccd831e 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -131,6 +131,8 @@ export default class Locks extends EventEmitter { this.deleteLock(id, self.connectionId); } + subscribe(eventOrEvents: K | K[], listener?: EventListener): void; + subscribe(listener?: EventListener): void; subscribe( listenerOrEvents?: K | K[] | EventListener, listener?: EventListener, @@ -148,6 +150,8 @@ export default class Locks extends EventEmitter { } } + unsubscribe(eventOrEvents: K | K[], listener?: EventListener): void; + unsubscribe(listener?: EventListener): void; unsubscribe( listenerOrEvents?: K | K[] | EventListener, listener?: EventListener, diff --git a/src/Members.ts b/src/Members.ts index 9243debb..16543aa7 100644 --- a/src/Members.ts +++ b/src/Members.ts @@ -66,6 +66,11 @@ class Members extends EventEmitter { return members.filter((m) => m.connectionId !== this.space.connectionId); } + subscribe( + eventOrEvents: K | K[], + listener?: EventListener, + ): void; + subscribe(listener?: EventListener): void; subscribe( listenerOrEvents?: K | K[] | EventListener, listener?: EventListener, @@ -83,6 +88,11 @@ class Members extends EventEmitter { } } + unsubscribe( + eventOrEvents: K | K[], + listener?: EventListener, + ): void; + unsubscribe(listener?: EventListener): void; unsubscribe( listenerOrEvents?: K | K[] | EventListener, listener?: EventListener, diff --git a/src/Space.ts b/src/Space.ts index 4989abcc..da22853d 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -179,6 +179,8 @@ class Space extends EventEmitter { return { members }; } + subscribe(eventOrEvents: K | K[], listener?: EventListener): void; + subscribe(listener?: EventListener): void; subscribe( listenerOrEvents?: K | K[] | EventListener, listener?: EventListener, @@ -196,6 +198,8 @@ class Space extends EventEmitter { } } + unsubscribe(eventOrEvents: K | K[], listener?: EventListener): void; + unsubscribe(listener?: EventListener): void; unsubscribe( listenerOrEvents?: K | K[] | EventListener, listener?: EventListener, diff --git a/src/utilities/EventEmitter.ts b/src/utilities/EventEmitter.ts index c34fae2a..b7145ab7 100644 --- a/src/utilities/EventEmitter.ts +++ b/src/utilities/EventEmitter.ts @@ -87,10 +87,22 @@ export default class EventEmitter { } /** + * {@label WITH_EVENTS} * Add an event listener - * @param listenerOrEvents (optional) the name of the event to listen to or the listener to be called. + * @param eventOrEvents the name of the event to listen to or the listener to be called. * @param listener (optional) the listener to be called. */ + on(eventOrEvents?: K | K[], listener?: EventListener): void; + /** + * Behaves the same as { @link on:WITH_EVENTS | the overload which accepts one or more event names }, but listens to _all_ events. + * @param listener (optional) the listener to be called. + */ + on(listener?: EventListener): void; + /** + * @internal + * We add the implementation signature as an overload signature (but mark it as internal so that it does not appear in documentation) so that it can be called by subclasses. + */ + on(listenerOrEvents?: K | K[] | EventListener, listener?: EventListener): void; on(listenerOrEvents?: K | K[] | EventListener, listener?: EventListener): void { // .on(() => {}) if (isFunction(listenerOrEvents)) { @@ -117,11 +129,22 @@ export default class EventEmitter { } /** + * {@label WITH_EVENTS} * Remove one or more event listeners - * @param listenerOrEvents (optional) the name of the event whose listener is to be removed. If not supplied, - * the listener is treated as an 'any' listener. + * @param eventOrEvents the name of the event whose listener is to be removed. * @param listener (optional) the listener to remove. If not supplied, all listeners are removed. */ + off(eventOrEvents?: K | K[], listener?: EventListener): void; + /** + * Behaves the same as { @link off:WITH_EVENTS | the overload which accepts one or more event names }, but removes the listener from _all_ events. + * @param listener (optional) the listener to remove. If not supplied, all listeners are removed. + */ + off(listener?: EventListener): void; + /** + * @internal + * We add the implementation signature as an overload signature (but mark it as internal so that it does not appear in documentation) so that it can be called by subclasses. + */ + off(listenerOrEvents?: K | K[] | EventListener, listener?: EventListener): void; off(listenerOrEvents?: K | K[] | EventListener, listener?: EventListener): void { // .off() // don't use arguments.length === 0 here as don't won't handle @@ -228,10 +251,25 @@ export default class EventEmitter { } /** + * {@label WITH_EVENTS} * Listen for a single occurrence of an event - * @param listenerOrEvent (optional) the name of the event to listen to + * @param event the name of the event to listen to * @param listener (optional) the listener to be called */ + once(event: K, listener?: EventListener): void | Promise; + /** + * Behaves the same as { @link once:WITH_EVENTS | the overload which accepts one or more event names }, but listens for _all_ events. + * @param listener (optional) the listener to be called + */ + once(listener?: EventListener): void | Promise; + /** + * @internal + * We add the implementation signature as an overload signature (but mark it as internal so that it does not appear in documentation) so that it can be called by subclasses. + */ + once( + listenerOrEvent: K | EventListener, + listener?: EventListener, + ): void | Promise; once( listenerOrEvent: K | EventListener, listener?: EventListener, From 02f8f04619804d8579b2262e9842574416248aa8 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Wed, 27 Sep 2023 22:45:10 -0300 Subject: [PATCH 12/30] Add `this` type parameter to EventListener Make it clear, via the type system, that a listener can access the name of the emitted event via the `this` variable. --- src/Cursors.ts | 16 ++++++++-------- src/Locations.ts | 16 ++++++++-------- src/Locks.ts | 16 ++++++++-------- src/Members.ts | 16 ++++++++-------- src/Space.ts | 16 ++++++++-------- src/utilities/EventEmitter.ts | 36 +++++++++++++++++------------------ 6 files changed, 58 insertions(+), 58 deletions(-) diff --git a/src/Cursors.ts b/src/Cursors.ts index 0b1f5b59..936ee370 100644 --- a/src/Cursors.ts +++ b/src/Cursors.ts @@ -102,12 +102,12 @@ export default class Cursors extends EventEmitter { subscribe( eventOrEvents: K | K[], - listener?: EventListener, + listener?: EventListener, ): void; - subscribe(listener?: EventListener): void; + subscribe(listener?: EventListener): void; subscribe( - listenerOrEvents?: K | K[] | EventListener, - listener?: EventListener, + listenerOrEvents?: K | K[] | EventListener, + listener?: EventListener, ) { try { super.on(listenerOrEvents, listener); @@ -132,12 +132,12 @@ export default class Cursors extends EventEmitter { unsubscribe( eventOrEvents: K | K[], - listener?: EventListener, + listener?: EventListener, ): void; - unsubscribe(listener?: EventListener): void; + unsubscribe(listener?: EventListener): void; unsubscribe( - listenerOrEvents?: K | K[] | EventListener, - listener?: EventListener, + listenerOrEvents?: K | K[] | EventListener, + listener?: EventListener, ) { try { super.off(listenerOrEvents, listener); diff --git a/src/Locations.ts b/src/Locations.ts index 1a8cc804..ddf78885 100644 --- a/src/Locations.ts +++ b/src/Locations.ts @@ -68,12 +68,12 @@ export default class Locations extends EventEmitter { subscribe( eventOrEvents: K | K[], - listener?: EventListener, + listener?: EventListener, ): void; - subscribe(listener?: EventListener): void; + subscribe(listener?: EventListener): void; subscribe( - listenerOrEvents?: K | K[] | EventListener, - listener?: EventListener, + listenerOrEvents?: K | K[] | EventListener, + listener?: EventListener, ) { try { super.on(listenerOrEvents, listener); @@ -90,12 +90,12 @@ export default class Locations extends EventEmitter { unsubscribe( eventOrEvents: K | K[], - listener?: EventListener, + listener?: EventListener, ): void; - unsubscribe(listener?: EventListener): void; + unsubscribe(listener?: EventListener): void; unsubscribe( - listenerOrEvents?: K | K[] | EventListener, - listener?: EventListener, + listenerOrEvents?: K | K[] | EventListener, + listener?: EventListener, ) { try { super.off(listenerOrEvents, listener); diff --git a/src/Locks.ts b/src/Locks.ts index 1ccd831e..309daffb 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -131,11 +131,11 @@ export default class Locks extends EventEmitter { this.deleteLock(id, self.connectionId); } - subscribe(eventOrEvents: K | K[], listener?: EventListener): void; - subscribe(listener?: EventListener): void; + subscribe(eventOrEvents: K | K[], listener?: EventListener): void; + subscribe(listener?: EventListener): void; subscribe( - listenerOrEvents?: K | K[] | EventListener, - listener?: EventListener, + listenerOrEvents?: K | K[] | EventListener, + listener?: EventListener, ) { try { super.on(listenerOrEvents, listener); @@ -150,11 +150,11 @@ export default class Locks extends EventEmitter { } } - unsubscribe(eventOrEvents: K | K[], listener?: EventListener): void; - unsubscribe(listener?: EventListener): void; + unsubscribe(eventOrEvents: K | K[], listener?: EventListener): void; + unsubscribe(listener?: EventListener): void; unsubscribe( - listenerOrEvents?: K | K[] | EventListener, - listener?: EventListener, + listenerOrEvents?: K | K[] | EventListener, + listener?: EventListener, ) { try { super.off(listenerOrEvents, listener); diff --git a/src/Members.ts b/src/Members.ts index 16543aa7..b058fc4b 100644 --- a/src/Members.ts +++ b/src/Members.ts @@ -68,12 +68,12 @@ class Members extends EventEmitter { subscribe( eventOrEvents: K | K[], - listener?: EventListener, + listener?: EventListener, ): void; - subscribe(listener?: EventListener): void; + subscribe(listener?: EventListener): void; subscribe( - listenerOrEvents?: K | K[] | EventListener, - listener?: EventListener, + listenerOrEvents?: K | K[] | EventListener, + listener?: EventListener, ) { try { super.on(listenerOrEvents, listener); @@ -90,12 +90,12 @@ class Members extends EventEmitter { unsubscribe( eventOrEvents: K | K[], - listener?: EventListener, + listener?: EventListener, ): void; - unsubscribe(listener?: EventListener): void; + unsubscribe(listener?: EventListener): void; unsubscribe( - listenerOrEvents?: K | K[] | EventListener, - listener?: EventListener, + listenerOrEvents?: K | K[] | EventListener, + listener?: EventListener, ) { try { super.off(listenerOrEvents, listener); diff --git a/src/Space.ts b/src/Space.ts index da22853d..dfba8a16 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -179,11 +179,11 @@ class Space extends EventEmitter { return { members }; } - subscribe(eventOrEvents: K | K[], listener?: EventListener): void; - subscribe(listener?: EventListener): void; + subscribe(eventOrEvents: K | K[], listener?: EventListener): void; + subscribe(listener?: EventListener): void; subscribe( - listenerOrEvents?: K | K[] | EventListener, - listener?: EventListener, + listenerOrEvents?: K | K[] | EventListener, + listener?: EventListener, ) { try { super.on(listenerOrEvents, listener); @@ -198,11 +198,11 @@ class Space extends EventEmitter { } } - unsubscribe(eventOrEvents: K | K[], listener?: EventListener): void; - unsubscribe(listener?: EventListener): void; + unsubscribe(eventOrEvents: K | K[], listener?: EventListener): void; + unsubscribe(listener?: EventListener): void; unsubscribe( - listenerOrEvents?: K | K[] | EventListener, - listener?: EventListener, + listenerOrEvents?: K | K[] | EventListener, + listener?: EventListener, ) { try { super.off(listenerOrEvents, listener); diff --git a/src/utilities/EventEmitter.ts b/src/utilities/EventEmitter.ts index b7145ab7..ce1139c1 100644 --- a/src/utilities/EventEmitter.ts +++ b/src/utilities/EventEmitter.ts @@ -1,6 +1,6 @@ import { isArray, isFunction, isObject, isString } from './is.js'; -function callListener(eventThis: { event: K }, listener: EventListener, arg: T[K]) { +function callListener(eventThis: { event: K }, listener: EventListener, arg: T[K]) { try { listener.apply(eventThis, [arg]); } catch (e) { @@ -64,7 +64,7 @@ export class InvalidArgumentError extends Error { } } -export type EventListener = (param: T) => void; +export type EventListener = (this: { event: K }, param: T[K]) => void; export default class EventEmitter { /** @internal */ @@ -92,18 +92,18 @@ export default class EventEmitter { * @param eventOrEvents the name of the event to listen to or the listener to be called. * @param listener (optional) the listener to be called. */ - on(eventOrEvents?: K | K[], listener?: EventListener): void; + on(eventOrEvents?: K | K[], listener?: EventListener): void; /** * Behaves the same as { @link on:WITH_EVENTS | the overload which accepts one or more event names }, but listens to _all_ events. * @param listener (optional) the listener to be called. */ - on(listener?: EventListener): void; + on(listener?: EventListener): void; /** * @internal * We add the implementation signature as an overload signature (but mark it as internal so that it does not appear in documentation) so that it can be called by subclasses. */ - on(listenerOrEvents?: K | K[] | EventListener, listener?: EventListener): void; - on(listenerOrEvents?: K | K[] | EventListener, listener?: EventListener): void { + on(listenerOrEvents?: K | K[] | EventListener, listener?: EventListener): void; + on(listenerOrEvents?: K | K[] | EventListener, listener?: EventListener): void { // .on(() => {}) if (isFunction(listenerOrEvents)) { this.any.push(listenerOrEvents); @@ -134,18 +134,18 @@ export default class EventEmitter { * @param eventOrEvents the name of the event whose listener is to be removed. * @param listener (optional) the listener to remove. If not supplied, all listeners are removed. */ - off(eventOrEvents?: K | K[], listener?: EventListener): void; + off(eventOrEvents?: K | K[], listener?: EventListener): void; /** * Behaves the same as { @link off:WITH_EVENTS | the overload which accepts one or more event names }, but removes the listener from _all_ events. * @param listener (optional) the listener to remove. If not supplied, all listeners are removed. */ - off(listener?: EventListener): void; + off(listener?: EventListener): void; /** * @internal * We add the implementation signature as an overload signature (but mark it as internal so that it does not appear in documentation) so that it can be called by subclasses. */ - off(listenerOrEvents?: K | K[] | EventListener, listener?: EventListener): void; - off(listenerOrEvents?: K | K[] | EventListener, listener?: EventListener): void { + off(listenerOrEvents?: K | K[] | EventListener, listener?: EventListener): void; + off(listenerOrEvents?: K | K[] | EventListener, listener?: EventListener): void { // .off() // don't use arguments.length === 0 here as don't won't handle // cases like .off(undefined) which is a valid call @@ -223,7 +223,7 @@ export default class EventEmitter { */ emit(event: K, arg: T[K]) { const eventThis = { event }; - const listeners: EventListener[] = []; + const listeners: EventListener[] = []; if (this.anyOnce.length > 0) { Array.prototype.push.apply(listeners, this.anyOnce); @@ -256,23 +256,23 @@ export default class EventEmitter { * @param event the name of the event to listen to * @param listener (optional) the listener to be called */ - once(event: K, listener?: EventListener): void | Promise; + once(event: K, listener?: EventListener): void | Promise; /** * Behaves the same as { @link once:WITH_EVENTS | the overload which accepts one or more event names }, but listens for _all_ events. * @param listener (optional) the listener to be called */ - once(listener?: EventListener): void | Promise; + once(listener?: EventListener): void | Promise; /** * @internal * We add the implementation signature as an overload signature (but mark it as internal so that it does not appear in documentation) so that it can be called by subclasses. */ once( - listenerOrEvent: K | EventListener, - listener?: EventListener, + listenerOrEvent: K | EventListener, + listener?: EventListener, ): void | Promise; once( - listenerOrEvent: K | EventListener, - listener?: EventListener, + listenerOrEvent: K | EventListener, + listener?: EventListener, ): void | Promise { // .once("eventName", () => {}) if (isString(listenerOrEvent) && isFunction(listener)) { @@ -302,7 +302,7 @@ export default class EventEmitter { whenState( targetState: K, currentState: keyof T, - listener: EventListener, + listener: EventListener, listenerArg: T[K], ) { const eventThis = { event: targetState }; From beb605ca6d136763337b9cb9ea619af32a6b352c Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Thu, 28 Sep 2023 13:03:47 -0300 Subject: [PATCH 13/30] Remove unnecessary use of isFunction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The type system tells us that if `listener` is truthy then it is a function. Assuming that we trust the type system (e.g. assuming that we aren’t trying to catch incorrect usage by non-TypeScript users, and in general I see little evidence that we are trying to) then these isFunction checks are unnecessary. --- src/utilities/EventEmitter.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utilities/EventEmitter.ts b/src/utilities/EventEmitter.ts index ce1139c1..b6a3d813 100644 --- a/src/utilities/EventEmitter.ts +++ b/src/utilities/EventEmitter.ts @@ -164,7 +164,7 @@ export default class EventEmitter { } // .off("eventName", () => {}) - if (isString(listenerOrEvents) && isFunction(listener)) { + if (isString(listenerOrEvents) && listener) { removeListener([this.events, this.eventsOnce], listener, listenerOrEvents); return; } @@ -177,7 +177,7 @@ export default class EventEmitter { } // .off(["eventName"], () => {}) - if (isArray(listenerOrEvents) && isFunction(listener)) { + if (isArray(listenerOrEvents) && listener) { listenerOrEvents.forEach((eventName) => { this.off(eventName, listener); }); @@ -275,7 +275,7 @@ export default class EventEmitter { listener?: EventListener, ): void | Promise { // .once("eventName", () => {}) - if (isString(listenerOrEvent) && isFunction(listener)) { + if (isString(listenerOrEvent) && listener) { const listeners = this.eventsOnce[listenerOrEvent] || (this.eventsOnce[listenerOrEvent] = []); listeners.push(listener); return; From 7d9e2170351696bfbf6e90fc698407655fdcf0ec Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Wed, 20 Sep 2023 14:26:38 -0300 Subject: [PATCH 14/30] (WIP) Add notes on class-definitions stuff that's missing in codebase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I don’t know what I’ll do with this information yet. --- docs/class-definitions.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/class-definitions.md b/docs/class-definitions.md index dc9069ff..713379e3 100644 --- a/docs/class-definitions.md +++ b/docs/class-definitions.md @@ -327,6 +327,8 @@ The most recent event emitted by [presence](https://ably.com/docs/presence-occup #### PresenceEvent +TODO This type doesn’t exist in the codebase; it's defined inline in SpaceMember + ```ts type PresenceEvent = { name: 'enter' | 'leave' | 'update' | 'present'; @@ -416,6 +418,8 @@ const otherLocations = await space.locations.getOthers() #### Location +TODO this type doesn’t exist in the codebase; `unknown` is used + Represents a location in an application. ```ts @@ -424,6 +428,8 @@ type Location = string | Record | null; #### LocationUpdate +TODO this type doesn’t exist in the codebase; it’s defined inline in LocationEventMap + Represents a change between locations for a given [`SpaceMember`](#spacemember). ```ts From eb3f1b3c704745caf3b7744239f41a55663f6ce5 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Mon, 18 Sep 2023 15:51:25 -0300 Subject: [PATCH 15/30] Add website documentation as TSDoc comments Copied all relevant-seeming documentation from website (at commit 5f9e999) to roughly appropriate places in code. Done: - avatar.textile - cursors.textile - locations.textile - locking.textile - space.textile Skipped: - setup.textile - index.textile --- src/Cursors.ts | 232 +++++++++++++++++++++++++++++++++++++++ src/Locations.ts | 206 ++++++++++++++++++++++++++++++++++ src/Locks.ts | 278 ++++++++++++++++++++++++++++++++++++++++++++++ src/Members.ts | 279 +++++++++++++++++++++++++++++++++++++++++++++++ src/Space.ts | 191 ++++++++++++++++++++++++++++++++ src/Spaces.ts | 92 ++++++++++++++++ 6 files changed, 1278 insertions(+) diff --git a/src/Cursors.ts b/src/Cursors.ts index 936ee370..ab300c90 100644 --- a/src/Cursors.ts +++ b/src/Cursors.ts @@ -17,6 +17,37 @@ export interface CursorsEventMap { const CURSORS_CHANNEL_TAG = '::$cursors'; +/** + * > **Documentation source** + * > + * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). + * + * The live cursors feature enables you to track the cursors of members within a space in realtime. + * + * Cursor events are emitted whenever a member moves their mouse within a space. In order to optimize the efficiency and frequency of updates, cursor position events are automatically batched. The batching interval may be customized in order to further optimize for increased performance versus the number of events published. + * + * Live cursor updates are not available as part of the "space state":/spaces/space#subscribe and must be subscribed to using "@space.cursors.subscribe()@":#subscribe. + * + * + * + * + * > **Documentation source** + * > + * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). + * + * h2(#foundations). Live cursor foundations + * + * The Spaces SDK is built upon existing Ably functionality available in Ably's Core SDKs. Understanding which core features are used to provide the abstractions in the Spaces SDK enables you to manage space state and build additional functionality into your application. + * + * Live cursors build upon the functionality of the Pub/Sub Channels "presence":/presence-occupancy/presence feature. + * + * Due to the high frequency at which updates are streamed for cursor movements, live cursors utilizes its own "channel":/channels. The other features of the Spaces SDK, such as avatar stacks, member locations and component locking all share a single channel. For this same reason, cursor position updates are not included in the "space state":/spaces/space and may only be subscribed to on the @cursors@ namespace. + * + * The channel is only created when a member calls @space.cursors.set()@. The live cursors channel object can be accessed through @space.cursors.channel@. To monitor the "underlying state of the cursors channel":/channels#states, the channel object can be accessed through @space.cursors.channel@. + * + */ export default class Cursors extends EventEmitter { private readonly cursorBatching: CursorBatching; private readonly cursorDispensing: CursorDispensing; @@ -45,6 +76,66 @@ export default class Cursors extends EventEmitter { * * @param {CursorUpdate} cursor * @return {void} + * + * > **Documentation source** + * > + * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). + * + * Set the position of a member's cursor using the @set()@ method. A position must contain an X-axis value and a Y-axis value to set the cursor position on a 2D plane. Calling @set()@ will emit a cursor event so that other members are informed of the cursor movement in realtime. + * + * A member must have been "entered":/spaces/space#enter into the space to set their cursor position. + * + * The @set()@ method takes the following parameters: + * + * |_. Parameter |_. Description |_. Type | + * | position.x | The position of the member's cursor on the X-axis. | Number | + * | position.y | The position of the member's cursor on the Y-axis. | Number | + * | data | An optional arbitrary JSON-serializable object containing additional information about the cursor, such as a color. | Object | + * + * + * + * The following is an example of a member setting their cursor position by adding an event listener to obtain their cursor coordinates and then publishing their position using the @set()@ method: + * + * ```[javascript] + * window.addEventListener('mousemove', ({ clientX, clientY }) => { + * space.cursors.set({ position: { x: clientX, y: clientY }, data: { color: 'red' } }); + * }); + * ``` + * + * The following is an example payload of a cursor event. Cursor events are uniquely identifiable by the @connectionId@ of a cursor. + * + * ```[json] + * { + * "hd9743gjDc": { + * "connectionId": "hd9743gjDc", + * "clientId": "clemons#142", + * "position": { + * "x": 864, + * "y": 32 + * }, + * "data": { + * "color": "red" + * } + * } + * } + * ``` + * + * The following are the properties of a cursor event payload: + * + * |_. Property |_. Description |_. Type | + * | connectionId | The unique identifier of the member's "connection":/connect. | String | + * | clientId | The "client identifier":/auth/identified-clients for the member. | String | + * | position | An object containing the position of a member's cursor. | Object | + * | position.x | The position of the member's cursor on the X-axis. | Number | + * | position.y | The position of the member's cursor on the Y-axis. | Number | + * | data | An optional arbitrary JSON-serializable object containing additional information about the cursor. | Object | + * */ async set(cursor: Pick) { const self = await this.space.members.getSelf(); @@ -100,6 +191,26 @@ export default class Cursors extends EventEmitter { ); }; + /** + * > **Documentation source** + * > + * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). + * + * Subscribe to cursor events by registering a listener. Cursor events are emitted whenever a member moves their cursor by calling @set()@. Use the @subscribe()@ method on the @cursors@ object of a space to receive updates. + * + * + * + * The following is an example of subscribing to cursor events: + * + * ```[javascript] + * space.cursors.subscribe('update', (cursorUpdate) => { + * console.log(cursorUpdate); + * }); + * ``` + * + */ subscribe( eventOrEvents: K | K[], listener?: EventListener, @@ -130,6 +241,26 @@ export default class Cursors extends EventEmitter { } } + /** + * > **Documentation source** + * > + * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). + * + * Unsubscribe from cursor events to remove previously registered listeners. + * + * The following is an example of removing a listener for cursor update events: + * + * ```[javascript] + * space.cursors.unsubscribe(`update`, listener); + * ``` + * + * Or remove all listeners: + * + * ```[javascript] + * space.cursors.unsubscribe(); + * ``` + * + */ unsubscribe( eventOrEvents: K | K[], listener?: EventListener, @@ -159,6 +290,10 @@ export default class Cursors extends EventEmitter { } } + /** + * + * See the documentation for {@link getAll}. + */ async getSelf(): Promise { const self = await this.space.members.getSelf(); if (!self) return null; @@ -167,6 +302,10 @@ export default class Cursors extends EventEmitter { return allCursors[self.connectionId]; } + /** + * + * See the documentation for {@link getAll}. + */ async getOthers(): Promise> { const self = await this.space.members.getSelf(); if (!self) return {}; @@ -177,6 +316,99 @@ export default class Cursors extends EventEmitter { return allCursorsFiltered; } + /** + * > **Documentation source** + * > + * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). + * + * Cursor positions can be retrieved in one-off calls. These are local calls that retrieve the latest position of cursors retained in memory by the SDK. + * + * The following is an example of retrieving a member's own cursor position: + * + * ```[javascript] + * const myCursor = await space.cursors.getSelf(); + * ``` + * + * The following is an example payload returned by @space.cursors.getSelf()@: + * + * ```[json] + * { + * “clientId”: “DzOBJqgGXzyUBb816Oa6i”, + * “connectionId”: “__UJBKZchX”, + * "position": { + * "x": 864, + * "y": 32 + * } + * } + * ``` + * + * The following is an example of retrieving the cursor positions for all members other than the member themselves: + * + * ```[javascript] + * const othersCursors = await space.cursors.getOthers(); + * ``` + * + * The following is an example payload returned by @space.cursors.getOthers()@: + * + * ```[json] + * { + * "3ej3q7yZZz": { + * "clientId": "yyXidHatpP3hJpMpXZi8W", + * "connectionId": "3ej3q7yZZz", + * "position": { + * "x": 12, + * "y": 3 + * } + * }, + * "Z7CA3-1vlR": { + * "clientId": "b18mj5B5hm-govdFEYRyb", + * "connectionId": "Z7CA3-1vlR", + * "position": { + * "x": 502, + * "y": 43 + * } + * } + * } + * ``` + * + * The following is an example of retrieving the cursor positions for all members, including the member themselves. @getAll()@ is useful for retrieving the initial position of members' cursors. + * + * ```[javascript] + * const allCursors = await space.cursors.getAll(); + * ``` + * + * The following is an example payload returned by @space.cursors.getAll()@: + * + * ```[json] + * { + * "3ej3q7yZZz": { + * "clientId": "yyXidHatpP3hJpMpXZi8W", + * "connectionId": "3ej3q7yZZz", + * "position": { + * "x": 12, + * "y": 3 + * } + * }, + * "Z7CA3-1vlR": { + * "clientId": "b18mj5B5hm-govdFEYRyb", + * "connectionId": "Z7CA3-1vlR", + * "position": { + * "x": 502, + * "y": 43 + * } + * }, + * "__UJBKZchX": { + * “clientId”: “DzOBJqgGXzyUBb816Oa6i”, + * “connectionId”: “__UJBKZchX”, + * "position": { + * "x": 864, + * "y": 32 + * } + * } + * } + * ``` + * + */ async getAll(): Promise> { const channel = this.getChannel(); return await this.cursorHistory.getLastCursorUpdate(channel, this.options.paginationLimit); diff --git a/src/Locations.ts b/src/Locations.ts index ddf78885..7764f991 100644 --- a/src/Locations.ts +++ b/src/Locations.ts @@ -18,6 +18,27 @@ export interface LocationsEventMap { update: LocationsEvents.UpdateEvent; } +/** + * > **Documentation source** + * > + * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). + * + * The member location feature enables you to track where members are within a space, to see which part of your application they're interacting with. A location could be the form field they have selected, the cell they're currently editing in a spreadsheet, or the slide they're viewing within a slide deck. Multiple members can be present in the same location. + * + * Member locations are used to visually display which component other members currently have selected, or are currently active on. Events are emitted whenever a member sets their location, such as when they click on a new cell, or slide. Events are received by members subscribed to location events and the UI component can be highlighted with the active member's profile data to visually display their location. + * + * + * > **Documentation source** + * > + * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). + * + * h2(#foundations). Member location foundations + * + * The Spaces SDK is built upon existing Ably functionality available in Ably's Core SDKs. Understanding which core features are used to provide the abstractions in the Spaces SDK enables you to manage space state and build additional functionality into your application. + * + * Member locations build upon the functionality of the Pub/Sub Channels "presence":/presence-occupancy/presence feature. Members are entered into the presence set when they "enter the space":/spaces/space#enter. + * + */ export default class Locations extends EventEmitter { private lastLocationUpdate: Record = {}; @@ -55,6 +76,24 @@ export default class Locations extends EventEmitter { } } + /** + * > **Documentation source** + * > + * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). + * + * Use the @set()@ method to emit a location event in realtime when a member changes their location. This will be received by all location subscribers to inform them of the location change. A @location@ can be any JSON-serializable object, such as a slide number or element ID. + * + * A member must have been "entered":/spaces/space#enter into the space to set their location. + * + * The @set()@ method is commonly combined with "@addEventListener()@":https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener or a React "synthetic event":https://react.dev/learn/responding-to-events#adding-event-handlers, such as @onClick@ or @onHover@. + * + * The following is an example of a member setting their location to a specific slide number, and element on that slide: + * + * ```[javascript] + * await space.locations.set({ slide: '3', component: 'slide-title' }); + * ``` + * + */ async set(location: unknown) { const self = await this.space.members.getSelf(); @@ -66,6 +105,76 @@ export default class Locations extends EventEmitter { await this.presenceUpdate(update.updateLocation(location)); } + /** + * > **Documentation source** + * > + * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). + * + * Subscribe to location events by registering a listener. Location events are emitted whenever a member changes location by calling "@set()@":#set. Use the @subscribe()@ method on the @locations@ namespace of the space to receive updates. + * + * All location changes are @update@ events. When a location update is received, clear the highlight from the UI component of the member's @previousLocation@ and add it to @currentLocation@. + * + * + * + * The following is an example of subscribing to location events: + * + * ```[javascript] + * space.locations.subscribe('update', (locationUpdate) => { + * console.log(locationUpdate); + * }); + * ``` + * + * The following is an example payload of a location event. Information about location is returned in @currentLocation@ and @previousLocation@: + * + * ```[json] + * { + * "member": { + * "clientId": "clemons#142", + * "connectionId": "hd9743gjDc", + * "isConnected": true, + * "profileData": { + * "username": "Claire Lemons", + * "avatar": "https://slides-internal.com/users/clemons.png" + * }, + * "location": { + * "slide": "3", + * "component": "slide-title" + * }, + * "lastEvent": { + * "name": "update", + * "timestamp": 1972395669758 + * } + * }, + * "previousLocation": { + * "slide": "2", + * "component": null + * }, + * "currentLocation": { + * "slide": "3", + * "component": "slide-title" + * } + * } + * ``` + * + * The following are the properties of a location event payload: + * + * |_. Property |_. Description |_. Type | + * | member.clientId | The "client identifier":/auth/identified-clients for the member. | String | + * | member.connectionId | The unique identifier of the member's "connection":/connect. | String | + * | member.isConnected | Whether the member is connected to Ably or not. | Boolean | + * | member.lastEvent.name | The most recent "event":/spaces/avatar emitted by the member. Will be one of @enter@, @update@, @leave@ or @remove@. | String | + * | member.lastEvent.timestamp | The timestamp of the most recently emitted event. | Number | + * | member.profileData | The optional "profile data":/spaces/avatar#profile-data associated with the member. | Object | + * | previousLocation | The previous location of the member. | Object | + * | currentLocation | The new location of the member. | Object | + * + * + * + */ subscribe( eventOrEvents: K | K[], listener?: EventListener, @@ -88,6 +197,26 @@ export default class Locations extends EventEmitter { } } + /** + * > **Documentation source** + * > + * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). + * + * Unsubscribe from location events to remove previously registered listeners. + * + * The following is an example of removing a listener for location update events: + * + * ```[javascript] + * space.locations.unsubscribe('update', listener); + * ``` + * + * Or remove all listeners: + * + * ```[javascript] + * space.locations.unsubscribe(); + * ``` + * + */ unsubscribe( eventOrEvents: K | K[], listener?: EventListener, @@ -110,11 +239,19 @@ export default class Locations extends EventEmitter { } } + /** + * + * See the documentation for {@link getAll}. + */ async getSelf(): Promise { const self = await this.space.members.getSelf(); return self ? self.location : null; } + /** + * + * See the documentation for {@link getAll}. + */ async getOthers(): Promise> { const members = await this.space.members.getOthers(); @@ -124,6 +261,75 @@ export default class Locations extends EventEmitter { }, {}); } + /** + * > **Documentation source** + * > + * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). + * + * Member locations can also be retrieved in one-off calls. These are local calls and retrieve the location of members retained in memory by the SDK. + * + * The following is an example of retrieving a member's own location: + * + * ```[javascript] + * const myLocation = await space.locations.getSelf(); + * ``` + * + * The following is an example payload returned by @space.locations.getSelf()@. It will return the properties of the member's @location@: + * + * ```[json] + * { + * "slide": "3", + * "component": "slide-title" + * } + * ``` + * + * The following is an example of retrieving the location objects of all members other than the member themselves. + * + * ```[javascript] + * const othersLocations = await space.locations.getOthers(); + * ``` + * + * The following is an example payload returned by @space.locations.getOthers()@: It will return the properties of all member's @location@ by their @connectionId@: + * + * ```[json] + * { + * "xG6H3lnrCn": { + * "slide": "1", + * "component": "textBox-1" + * }, + * "el29SVLktW": { + * "slide": "1", + * "component": "image-2" + * } + * } + * ``` + * + * The following is an example of retrieving the location objects of all members, including the member themselves: + * + * ```[javascript] + * const allLocations = await space.locations.getAll(); + * ``` + * + * The following is an example payload returned by @space.locations.getAll()@. It will return the properties of all member's @location@ by their @connectionId@: + * + * ```[json] + * { + * "xG6H3lnrCn": { + * "slide": "1", + * "component": "textBox-1" + * }, + * "el29SVLktW": { + * "slide": "1", + * "component": "image-2" + * }, + * "dieF3291kT": { + * "slide": "3", + * "component": "slide-title" + * } + * } + * ``` + * + */ async getAll(): Promise> { const members = await this.space.members.getAll(); return members.reduce((acc: Record, member: SpaceMember) => { diff --git a/src/Locks.ts b/src/Locks.ts index 309daffb..0672c612 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -22,6 +22,42 @@ export interface LocksEventMap { update: Lock; } +/** + * > **Documentation source** + * > + * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). + * + * The component locking feature enables members to optimistically lock stateful UI components before editing them. This reduces the chances of conflicting changes being made to the same component by different members. A component could be a cell in a spreadsheet that a member is updating, or an input field on a form they're filling in. + * + * Once a lock has been acquired by a member, the component that it relates to can be updated in the UI to visually indicate to other members that it is locked and and which member has the lock. The component can then be updated once the editing member has released the lock to indicate that it is now unlocked. + * + * Each lock is identified by a unique string ID, and only a single member may hold a lock with a given string at any one time. A lock will exist in one of three "states":#states and may only transition between states in specific circumstances. + * + * + * + * h2(#states). Lock states + * + * Component locking is handled entirely client-side. Members may begin to optimistically edit a component as soon as they call "@acquire()@":#acquire on the lock identifier related to it. Alternatively, you could wait until they receive a @locked@ event and display a spinning symbol in the UI until this is received. In either case a subsequent @unlocked@ event may invalidate that member's lock request if another member acquired it earlier. The time for confirmation of whether a lock request was successful or rejected is, on average, in the hundreds of milliseconds, however your code should handle all possible lock state transitions. + * + * A lock will be in one of the following states: + * + * - @pending@ := A member has requested a lock by calling "@acquire()@":#acquire. + * - @locked@ := The lock is confirmed to be held by the requesting member. + * - @unlocked@ := The lock is confirmed to not be locked by the requesting member, or has been "released":#release by a member previously holding the lock. + * + * The following lock state transitions may occur: + * + * * None → @pending@: a member calls "@acquire()@":#acquire to request a lock. + * * @pending@ → @locked@: the requesting member holds the lock. + * * @pending@ → @unlocked@: the requesting member does not hold the lock, since another member already holds it. + * * @locked@ → @unlocked@: the lock was either explicitly "released":#release by the member, or their request was invalidated by a concurrent request which took precedence. + * * @unlocked@ → @locked@: the requesting member reacquired a lock they previously held. + * + * Only transitions that result in a @locked@ or @unlocked@ status will emit a lock event that members can "@subscribe()@":#subscribe to. + * + */ export default class Locks extends EventEmitter { // locks tracks the local state of locks, which is used to determine whether // a lock's status has changed when processing presence updates. @@ -37,6 +73,35 @@ export default class Locks extends EventEmitter { this.locks = new Map(); } + /** + * > **Documentation source** + * > + * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). + * + * Use the @get()@ method to query whether a lock is currently locked, and by which member if it is. The lock is identifiable by its unique string ID. + * + * The following is an example of checking whether a lock identifier is currently locked: + * + * ```[javascript] + * const isLocked = space.locks.get(id) !== undefined; + * ``` + * + * The following is an example of checking which member holds the lock: + * + * ```[javascript] + * const { member } = space.locks.get(id); + * ``` + * + * The following is an example of viewing the attributes assigned to the lock by the member holding it: + * + * ```[javascript] + * const { request } = space.locks.get(id); + * const viewLock = request.attributes.get(key); + * ``` + * + * If the lock is not currently held by a member, @get()@ will return @undefined@. Otherwise it will return the most recent lock event for the lock. + * + */ get(id: string): Lock | undefined { const locks = this.locks.get(id); if (!locks) return; @@ -49,6 +114,65 @@ export default class Locks extends EventEmitter { // This will be async in the future, when pending requests are no longer processed // in the library. + /** + * > **Documentation source** + * > + * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). + * + * Locks can also be retrieved in one-off calls. These are local calls and retrieve the locks retained in memory by the SDK. + * + * The following is an example of retrieving an array of all currently held locks in a space: + * + * ```[javascript] + * const allLocks = await space.locks.getAll(); + * ``` + * + * The following is an example payload returned by @space.locks.getAll()@: + * + * ```[json] + * [ + * { + * "id": "s1-c2", + * "status": "locked", + * "timestamp": 1247525627533, + * "member": { + * "clientId": "amint#5", + * "connectionId": "hg35a4fgjAs", + * "isConnected": true, + * "lastEvent": { + * "name": "update", + * "timestamp": 173459567340 + * }, + * "location": null, + * "profileData": { + * "username": "Arit Mint", + * "avatar": "https://slides-internal.com/users/amint.png" + * } + * } + * }, + * { + * "id": "s3-c4", + * "status": "locked", + * "timestamp": 1247115627423, + * "member": { + * "clientId": "torange#1", + * "connectionId": "tt7233ghUa", + * "isConnected": true, + * "lastEvent": { + * "name": "update", + * "timestamp": 167759566354 + * }, + * "location": null, + * "profileData": { + * "username": "Tara Orange", + * "avatar": "https://slides-internal.com/users/torange.png" + * } + * } + * } + * ] + * ``` + * + */ async getAll(): Promise { const allLocks: Lock[] = []; @@ -63,6 +187,10 @@ export default class Locks extends EventEmitter { return allLocks; } + /** + * + * See the documentation for {@link getAll}. + */ async getSelf(): Promise { const self = await this.space.members.getSelf(); @@ -71,6 +199,10 @@ export default class Locks extends EventEmitter { return this.getLocksForConnectionId(self.connectionId).filter((lock) => lock.status === 'locked'); } + /** + * + * See the documentation for {@link getAll}. + */ async getOthers(): Promise { const self = await this.space.members.getSelf(); const allLocks = await this.getAll(); @@ -80,6 +212,45 @@ export default class Locks extends EventEmitter { return allLocks.filter((lock) => lock.member.connectionId !== self.connectionId); } + /** + * > **Documentation source** + * > + * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). + * + * Use the @acquire()@ method to attempt to acquire a lock with a given unique ID. Additional @attributes@ may be passed when trying to acquire a lock that can contain a set of arbitrary key-value pairs. An example of using @attributes@ is to store the component ID the lock relates to so that it can be easily updated in the UI with a visual indication of its lock status. + * + * A member must have been "entered":/spaces/space#enter into the space to acquire a lock. + * + * The following is an example of attempting to acquire a lock: + * + * ```[javascript] + * const acquireLock = await space.locks.acquire(id); + * ``` + * + * The following is an example of passing a set of @attributes@ when trying to acquire a lock: + * + * ```[javascript] + * const lockAttributes = new Map(); + * lockAttributes.set('component', 'cell-d3'); + * const acquireLock = await space.locks.acquire(id, { lockAttributes }); + * ``` + * + * The following is an example payload returned by @space.locks.acquire()@. The promise will resolve to a lock request with the @pending@ status: + * + * ```[json] + * { + * "id": "s2-d14", + * "status": "pending", + * "timestamp": 1247525689781, + * "attributes": { + * "componentId": "cell-d14" + * } + * } + * ``` + * + * Once a member requests a lock by calling @acquire()@, the lock is temporarily in the "pending state":#states. An event will be emitted based on whether the lock request was successful (a status of @locked@) or invalidated (a status of @unlocked@). This can be "subscribed":#subscribe to in order for the client to know whether their lock request was successful or not. + * + */ async acquire(id: string, opts?: LockOptions): Promise { const self = await this.space.members.getSelf(); if (!self) { @@ -113,6 +284,26 @@ export default class Locks extends EventEmitter { return lock; } + /** + * > **Documentation source** + * > + * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). + * + * Use the @release()@ method to explicitly release a lock once a member has finished editing the related component. For example, the @release()@ method can be called once a user clicks outside of the component, such as clicking on another cell within a spreadsheet. Any UI indications that the previous cell was locked can then be cleared. + * + * The following is an example of releasing a lock: + * + * ```[javascript] + * await space.locks.release(id); + * ``` + * + * Releasing a lock will emit a lock event with a "lock status":#states of @unlocked@. + * + * + * + */ async release(id: string): Promise { const self = await this.space.members.getSelf(); @@ -131,6 +322,73 @@ export default class Locks extends EventEmitter { this.deleteLock(id, self.connectionId); } + /** + * > **Documentation source** + * > + * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). + * + * Subscribe to lock events by registering a listener. Lock events are emitted whenever the "lock state":#states transitions into @locked@ or @unlocked@. Use the @subscribe()@ method on the @locks@ namespace of the space to receive updates. + * + * All lock events are @update@ events. When a lock event is received, UI components can be updated to add and remove visual indications of which member is locking them, as well as enabling and disabling the ability for other members to edit them. + * + * The following is an example of subscribing to lock events: + * + * ```[javascript] + * space.locks.subscribe('update', (lock) => { + * console.log(lock); + * }); + * ``` + * + * The following is an example payload of a lock event: + * + * ```json + * { + * "id": "s2-d14", + * "status": "unlocked", + * "timestamp": 1247525689781, + * "attributes": { + * "componentId": "cell-d14" + * }, + * "reason": { + * "message": "lock is currently locked", + * "code": 101003, + * "statusCode": 400 + * }, + * "member": { + * "clientId": "smango", + * "connectionId": "hs343gjsdc", + * "isConnected": true, + * "profileData": { + * "username": "Saiorse Mango" + * }, + * "location": { + * "slide": "sheet-2", + * "component": "d-14" + * }, + * "lastEvent": { + * "name": "update", + * "timestamp": 1247525689781 + * } + * } + * } + * ``` + * + * The following are the properties of a lock event payload: + * + * |_. Property |_. Description |_. Type | + * | id | The unique ID of the lock request. | String | + * | status | The lock "status":#states of the event. Will be either @locked@ or @unlocked@. | String | + * | timestamp | The timestamp of the lock event. | Number | + * | attributes | The optional attributes of the lock, such as the ID of the component it relates to. | Object | + * | reason | The reason why the @request.status@ is @unlocked@. | ErrorInfo | + * | member.clientId | The "client identifier":/auth/identified-clients for the member. | String | + * | member.connectionId | The unique identifier of the member's "connection":/connect. | String | + * | member.isConnected | Whether the member is connected to Ably or not. | Boolean | + * | member.lastEvent.name | The most recent "event":/spaces/avatar#events emitted by the member. Will be one of @enter@, @update@, @leave@ or @remove@. | String | + * | member.lastEvent.timestamp | The timestamp of the most recently emitted event. | Number | + * | member.profileData | The optional "profile data":/spaces/avatar#profile-data associated with the member. | Object | + * + */ subscribe(eventOrEvents: K | K[], listener?: EventListener): void; subscribe(listener?: EventListener): void; subscribe( @@ -150,6 +408,26 @@ export default class Locks extends EventEmitter { } } + /** + * > **Documentation source** + * > + * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). + * + * Unsubscribe from lock events to remove previously registered listeners. + * + * The following is an example of removing a listener for lock update events: + * + * ```[javascript] + * space.locks.unsubscribe('update', listener); + * ``` + * + * Or remove all listeners: + * + * ```[javascript] + * space.locks.unsubscribe(); + * ``` + * + */ unsubscribe(eventOrEvents: K | K[], listener?: EventListener): void; unsubscribe(listener?: EventListener): void; unsubscribe( diff --git a/src/Members.ts b/src/Members.ts index b058fc4b..8b714d84 100644 --- a/src/Members.ts +++ b/src/Members.ts @@ -13,6 +13,40 @@ export interface MembersEventMap { remove: SpaceMember; } +/** + * > **Documentation source** + * > + * > The following documentation is copied from the Spaces documentation website. + * + * Avatar stacks are the most common way of showing the online status of members in an application by displaying an avatar for each member. Events are emitted whenever a member enters or leaves a space, or updates their profile data. Additional information can also be provided, such as a profile picture and email address. + * + * Subscribe to the @space.members@ namespace in order to keep your avatar stack updated in realtime. + * + * h2(#events). Event types + * + * The following four event types are emitted by members: + * + * - @enter@ := A new member has entered the space. The member has either entered explicitly by calling "@space.enter()@":/spaces/space#enter, or has attempted to update their profile data before entering a space, which will instead emit an @enter@ event. + * - @update@ := A member has updated their profile data by calling "@space.updateProfileData()@":/spaces/space#update-profile. + * - @leave@ := A member has left the space. The member has either left explicitly by calling "@space.leave()@":/spaces/space#leave, or has abruptly disconnected and not re-established a connection within 15 seconds. + * - @remove@ := A member has been removed from the members list after the "@offlineTimeout@":/spaces/space#options period has elapsed. This enables members to appear greyed out in the avatar stack to indicate that they recently left for the period of time between their @leave@ and @remove@ events. + * + * + * + * + * > **Documentation source** + * > + * > The following documentation is copied from the Spaces documentation website. + * + * h2(#foundations). Avatar stack foundations + * + * The Spaces SDK is built upon existing Ably functionality available in Ably's Core SDKs. Understanding which core features are used to provide the abstractions in the Spaces SDK enables you to manage space state and build additional functionality into your application. + * + * Avatar stacks build upon the functionality of the Pub/Sub Channels "presence":/presence-occupancy/presence feature. Members are entered into the presence set when they "enter the space":/spaces/space#enter. + * + */ class Members extends EventEmitter { private lastMemberUpdate: Record = {}; private leavers: Leavers; @@ -51,21 +85,240 @@ class Members extends EventEmitter { } } + /** + * + * See the documentation for {@link getAll}. + */ async getSelf(): Promise { return this.space.connectionId ? await this.getByConnectionId(this.space.connectionId) : null; } + /** + * > **Documentation source** + * > + * > The following documentation is copied from the Spaces documentation website. + * + * Space membership can be retrieved in one-off calls. These are local calls and retrieve the membership retained in memory by the SDK. One-off calls to retrieve membership can be used for operations such as displaying a member's own profile data to them, or retrieving a list of all other members to use to "update their profile data":/spaces/space#update-profile. + * + * The following is an example of retrieving a member's own member object: + * + * ```[javascript] + * const myMemberInfo = await space.members.getSelf(); + * ``` + * + * The following is an example payload returned by @space.members.getSelf()@: + * + * ```[json] + * { + * "clientId": "clemons#142", + * "connectionId": "hd9743gjDc", + * "isConnected": true, + * "lastEvent": { + * "name": "enter", + * "timestamp": 1677595689759 + * }, + * "location": null, + * "profileData": { + * "username": "Claire Lemons", + * "avatar": "https://slides-internal.com/users/clemons.png" + * } + * } + * ``` + * + * The following is an example of retrieving an array of member objects for all members other than the member themselves. Ths includes members that have recently left the space, but have not yet been removed. + * + * ```[javascript] + * const othersMemberInfo = await space.members.getOthers(); + * ``` + * + * The following is an example payload returned by @space.members.getOthers()@: + * + * ```[json] + * [ + * { + * "clientId": "torange#1", + * "connectionId": "tt7233ghUa", + * "isConnected": true, + * "lastEvent": { + * "name": "enter", + * "timestamp": 167759566354 + * }, + * "location": null, + * "profileData": { + * "username": "Tara Orange", + * "avatar": "https://slides-internal.com/users/torange.png" + * } + * }, + * { + * "clientId": "amint#5", + * "connectionId": "hg35a4fgjAs", + * "isConnected": true, + * "lastEvent": { + * "name": "update", + * "timestamp": 173459567340 + * }, + * "location": null, + * "profileData": { + * "username": "Arit Mint", + * "avatar": "https://slides-internal.com/users/amint.png" + * } + * } + * ] + * ``` + * + * The following is an example of retrieving an array of all member objects, including the member themselves. Ths includes members that have recently left the space, but have not yet been removed. + * + * ```[javascript] + * const allMembers = await space.members.getAll(); + * ``` + * + * The following is an example payload returned by @space.members.getAll()@: + * + * ```[json] + * [ + * { + * "clientId": "clemons#142", + * "connectionId": "hd9743gjDc", + * "isConnected": false, + * "lastEvent": { + * "name": "enter", + * "timestamp": 1677595689759 + * }, + * "location": null, + * "profileData": { + * "username": "Claire Lemons", + * "avatar": "https://slides-internal.com/users/clemons.png" + * } + * }, + * { + * "clientId": "amint#5", + * "connectionId": "hg35a4fgjAs", + * "isConnected": true, + * "lastEvent": { + * "name": "update", + * "timestamp": 173459567340 + * }, + * "location": null, + * "profileData": { + * "username": "Arit Mint", + * "avatar": "https://slides-internal.com/users/amint.png" + * } + * }, + * { + * "clientId": "torange#1", + * "connectionId": "tt7233ghUa", + * "isConnected": true, + * "lastEvent": { + * "name": "enter", + * "timestamp": 167759566354 + * }, + * "location": null, + * "profileData": { + * "username": "Tara Orange", + * "avatar": "https://slides-internal.com/users/torange.png" + * } + * } + * ] + * ``` + * + */ async getAll(): Promise { const presenceMembers = await this.space.channel.presence.get(); const members = presenceMembers.map((m) => this.createMember(m)); return members.concat(this.leavers.getAll().map((l) => l.member)); } + /** + * + * See the documentation for {@link getAll}. + */ async getOthers(): Promise { const members = await this.getAll(); return members.filter((m) => m.connectionId !== this.space.connectionId); } + /** + * > **Documentation source** + * > + * > The following documentation is copied from the Spaces documentation website. + * + * Subscribe to members' online status and profile updates by registering a listener. Member events are emitted whenever a member "enters":/spaces/space#enter or "leaves":/spaces/space#leave the space, or updates their profile data. Use the @subscribe()@ method on the @members@ object of a space to receive updates. + * + * The following is an example of subscribing to the different member event types: + * + * ```[javascript] + * // Subscribe to member enters in a space + * space.members.subscribe('enter', (memberUpdate) => { + * console.log(memberUpdate); + * }); + * + * // Subscribe to member profile data updates in a space + * space.members.subscribe('update', (memberUpdate) => { + * console.log(memberUpdate); + * }); + * + * // Subscribe to member leaves in a space + * space.members.subscribe('leave', (memberUpdate) => { + * console.log(memberUpdate); + * }); + * + * // Subscribe to member removals in a space + * space.members.subscribe('remove', (memberUpdate) => { + * console.log(memberUpdate); + * }); + * ``` + * + * It's also possible to subscribe to multiple event types with the same listener by using an array: + * + * ```[javascript] + * space.members.subscribe(['enter', 'update'], (memberUpdate) => { + * console.log(memberUpdate); + * }); + * ``` + * + * Or subscribe to all event types: + * + * ```[javascript] + * space.members.subscribe((memberUpdate) => { + * console.log(memberUpdate); + * }); + * ``` + * + * The following is an example payload of a member event. The @lastEvent.name@ describes which "event type":#events a payload relates to. + * + * ```[json] + * { + * "clientId": "clemons#142", + * "connectionId": "hd9743gjDc", + * "isConnected": true, + * "lastEvent": { + * "name": "enter", + * "timestamp": 1677595689759 + * }, + * "location": null, + * "profileData": { + * "username": "Claire Lemons", + * "avatar": "https://slides-internal.com/users/clemons.png" + * } + * } + * ``` + * + * The following are the properties of a member event payload: + * + * |_. Property |_. Description |_. Type | + * | clientId | The "client identifier":/auth/identified-clients for the member. | String | + * | connectionId | The unique identifier of the member's "connection":/connect. | String | + * | isConnected | Whether the member is connected to Ably or not. | Boolean | + * | profileData | The optional "profile data":#profile-data associated with the member. | Object | + * | location | The current "location":/spaces/locations of the member. Will be @null@ for @enter@, @leave@ and @remove@ events. | Object | + * | lastEvent.name | The most recent event emitted by the member. | String | + * | lastEvent.timestamp | The timestamp of the most recently emitted event. | Number | + * + * + * + */ subscribe( eventOrEvents: K | K[], listener?: EventListener, @@ -88,6 +341,32 @@ class Members extends EventEmitter { } } + /** + * > **Documentation source** + * > + * > The following documentation is copied from the Spaces documentation website. + * + * Unsubscribe from member events to remove previously registered listeners. + * + * The following is an example of removing a listener for one member event type: + * + * ```[javascript] + * space.members.unsubscribe('enter', listener); + * ``` + * + * It's also possible to remove listeners for multiple member event types: + * + * ```[javascript] + * space.members.unsubscribe(['enter', 'leave'], listener); + * ``` + * + * Or remove all listeners: + * + * ```[javascript] + * space.members.unsubscribe(); + * ``` + * + */ unsubscribe( eventOrEvents: K | K[], listener?: EventListener, diff --git a/src/Space.ts b/src/Space.ts index dfba8a16..71f92a1b 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -34,6 +34,27 @@ export interface SpaceEventMap { update: SpaceEvents.UpdateEvent; } +/** + * > **Documentation source** + * > + * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). + * + * A space is a virtual area of your application in which realtime collaboration between users can take place. You can have any number of virtual spaces within an application, with a single space being anything from a web page, a sheet within a spreadsheet, an individual slide in a slideshow, or the entire slideshow itself. + * + * The following features can be implemented within a space: + * + * * "Avatar stack":/spaces/avatar + * * "Member location":/spaces/locations + * * "Live cursors":/spaces/cursors + * * "Component locking":/spaces/locking + * + * The @space@ namespace consists of a state object that represents the realtime status of all members in a given virtual space. This includes a list of which members are currently online or have recently left and each member's location within the application. The position of members' cursors are excluded from the space state due to their high frequency of updates. In the beta release, which UI components members have locked are also excluded from the space state. + * + * Space state can be "subscribed":#subscribe to in the @space@ namespace. Alternatively, subscription listeners can be registered for individual features, such as avatar stack events and member location updates. These individual subscription listeners are intended to provide flexibility when implementing collaborative features. Individual listeners are client-side filtered events, so irrespective of whether you choose to subscribe to the space state or individual listeners, each event only counts as a single message. + * + * To subscribe to any events in a space, you first need to create or retrieve a space. + * + */ class Space extends EventEmitter { private readonly channelName: string; readonly connectionId: string | undefined; @@ -107,6 +128,44 @@ class Space extends EventEmitter { this.emit('update', { members: await this.members.getAll() }); } + /** + * > **Documentation source** + * > + * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). + * + * Entering a space will register a client as a member and emit an "@enter@":/spaces/members#events event to all subscribers. Use the @enter()@ method to enter a space. + * + * Being entered into a space is required for members to: + * + * * Update their "profile data":#update-profile. + * * Set their "location":/spaces/locations. + * * Set their "cursor position":/spaces/cursors. + * + * The following is an example of entering a space: + * + * ```[javascript] + * await space.enter(); + * ``` + * + * + * > **Documentation source** + * > + * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). + * + * Profile data can be set when "entering":#enter a space. It is optional data that can be used to associate information with a member, such as a preferred username, or profile picture that can be subsequently displayed in their avatar. Profile data can be any arbitrary JSON-serializable object. + * + * Profile data is returned in the payload of all space events. + * + * The following is an example of setting profile data when entering a space: + * + * ```[javascript] + * await space.enter({ + * username: 'Claire Oranges', + * avatar: 'https://slides-internal.com/users/coranges.png', + * }); + * ``` + * + */ async enter(profileData: ProfileData = null): Promise { return new Promise((resolve) => { const presence = this.channel.presence; @@ -129,6 +188,28 @@ class Space extends EventEmitter { }); } + /** + * + * Profile data can be updated at any point after entering a space by calling @updateProfileData()@. This will emit an @update@ event. If a client hasn't yet entered the space, @updateProfileData()@ will instead "enter the space":#enter, with the profile data, and emit an "@enter@":/spaces/members#events event. + * + * The following is an example of updating profile data: + * + * ```[javascript] + * space.updateProfileData({ + * username: 'Claire Lemons', + * avatar: 'https://slides-internal.com/users/clemons.png', + * }); + * ``` + * + * A function can be passed to @updateProfileData()@ in order to update a field based on the existing profile data: + * + * ```[javascript] + * space.updateProfileData(currentProfile => { + * return { ...currentProfile, username: 'Clara Lemons' } + * }); + * ``` + * + */ async updateProfileData(profileDataOrUpdateFn: ProfileData | ((update: ProfileData) => ProfileData)): Promise { const self = await this.members.getSelf(); @@ -154,6 +235,19 @@ class Space extends EventEmitter { } } + /* + * + * Leaving a space will emit a "@leave@":/spaces/members#events event to all subscribers. + * + * The following is an example of explicitly leaving a space: + * + * ```[javascript] + * await space.leave(); + * ``` + * + * Members will implicitly leave a space after 15 seconds if they abruptly disconnect. If experiencing network disruption, and they reconnect within 15 seconds, then they will remain part of the space and no @leave@ event will be emitted. + * + */ async leave(profileData: ProfileData = null) { const self = await this.members.getSelf(); @@ -174,11 +268,97 @@ class Space extends EventEmitter { await this.presenceLeave(data); } + /** + * + * The current state of the space can be retrieved in a one-off call. This will return an array of all @member@ objects currently in the space. This is a local call and retrieves the membership of the space retained in memory by the SDK. + * + * The following is an example of retrieving the current space state. Ths includes members that have recently left the space, but have not yet been removed: + * + * ```[javascript] + * const spaceState = await space.getState(); + * ``` + * + */ async getState(): Promise<{ members: SpaceMember[] }> { const members = await this.members.getAll(); return { members }; } + /** + * + * Subscribe to space state updates by registering a listener. Use the @subscribe()@ method on the @space@ object to receive updates. + * + * The following events will trigger a space event: + * + * * A member enters the space + * * A member leaves the space + * * A member is removed from the space state "after the offlineTimeout period":#options has elapsed + * * A member updates their profile data + * * A member sets a new location + * + * Space state contains a single object called @members@. Any events that trigger a change in space state will always return the current state of the space as an array of @member@ objects. + * + * + * + * The following is an example of subscribing to space events: + * + * ```[javascript] + * space.subscribe('update', (spaceState) => { + * console.log(spaceState.members); + * }); + * ``` + * + * The following is an example payload of a space event. + * + * ```[json] + * [ + * { + * "clientId": "clemons#142", + * "connectionId": "hd9743gjDc", + * "isConnected": false, + * "lastEvent": { + * "name": "leave", + * "timestamp": 1677595689759 + * }, + * "location": null, + * "profileData": { + * "username": "Claire Lemons", + * "avatar": "https://slides-internal.com/users/clemons.png" + * } + * }, + * { + * "clientId": "amint#5", + * "connectionId": "hg35a4fgjAs", + * "isConnected": true, + * "lastEvent": { + * "name": "enter", + * "timestamp": 173459567340 + * }, + * "location": null, + * "profileData": { + * "username": "Arit Mint", + * "avatar": "https://slides-internal.com/users/amint.png" + * } + * }, + * ... + * ] + * ``` + * + * The following are the properties of an individual @member@ within a space event payload: + * + * |_. Property |_. Description |_. Type | + * | clientId | The "client identifier":/auth/identified-clients for the member. | String | + * | connectionId | The unique identifier of the member's "connection":/connect. | String | + * | isConnected | Whether the member is connected to Ably or not. | Boolean | + * | profileData | The optional "profile data":#profile-data associated with the member. | Object | + * | location | The current "location":/spaces/locations of the member. | Object | + * | lastEvent.name | The most recent event emitted by the member. | String | + * | lastEvent.timestamp | The timestamp of the most recently emitted event. | Number | + * + */ subscribe(eventOrEvents: K | K[], listener?: EventListener): void; subscribe(listener?: EventListener): void; subscribe( @@ -198,6 +378,17 @@ class Space extends EventEmitter { } } + /** + * + * Unsubscribe from space events to remove previously registered listeners. + * + * The following is an example of removing a listener: + * + * ```[javascript] + * space.unsubscribe('update', listener); + * ``` + * + */ unsubscribe(eventOrEvents: K | K[], listener?: EventListener): void; unsubscribe(listener?: EventListener): void; unsubscribe( diff --git a/src/Spaces.ts b/src/Spaces.ts index 5f252819..2e7eaddb 100644 --- a/src/Spaces.ts +++ b/src/Spaces.ts @@ -31,6 +31,98 @@ class Spaces { options.agents = { ...(options.agents ?? options.agents), ...agent }; } + /** + * > **Documentation source** + * > + * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). + * + * A @space@ object is a reference to a single space and is uniquely identified by its unicode string name. A space is created, or an existing space is retrieved from the @spaces@ collection using the @get()@ method. + * + * The following restrictions apply to space names: + * + * * Avoid starting names with @[@ or @:@ + * * Ensure names aren't empty + * * Exclude whitespace and wildcards, such as @*@ + * * Use the correct case, whether it be uppercase or lowercase + * + * The following is an example of creating a space: + * + * ```[javascript] + * const space = await spaces.get('board-presentation'); + * ``` + * + * + * > **Documentation source** + * > + * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). + * + * h2(#advanced). Advanced properties + * + * The following sections are only relevant if you want to further customize a space, or understand more about the Spaces SDK. They aren't required to get up and running with the basics. + * + * h3(#options). Space options + * + * An additional set of optional properties may be passed when "creating or retrieving":#create a space to customize the behavior of different features. + * + * The following properties can be customized: + * + * |_. Property |_. Description |_. Type | + * | offlineTimeout | Number of milliseconds after a member loses connection or closes their browser window to wait before they are removed from the member list. The default is 120,000ms (2 minutes). | Number | + * | cursors | A "cursor options":/spaces/cursors#options object for customizing live cursor behavior. | Object | + * | cursors.outboundBatchInterval | The interval, in milliseconds, at which a batch of cursor positions are published. This is multiplied by the number of members in a space, minus 1. The default value is 100ms. | Number | + * | cursors.paginationLimit | The number of pages searched from history for the last published cursor position. The default is 5. | Number | + * + * The following is an example of customizing the space options when calling @spaces.get()@: + * + * ```[javascript] + * const space = await spaces.get('board-presentation', { + * offlineTimeout: 180_000, + * cursors: { paginationLimit: 10 } + * }); + * ``` + * + * h3(#foundations). Space foundations + * + * The Spaces SDK is built upon existing Ably functionality available in Ably's Core SDKs. Understanding which core features are used to provide the abstractions in the Spaces SDK enables you to manage space state and build additional functionality into your application. + * + * A space is created as an Ably "channel":/channels. Members "attach":/channels#attach to the channel and join its "presence set":/presence-occupancy/presence when they "enter":#enter the space. Avatar stacks, member locations and component locking are all handled on this channel. + * + * To manage the state of the space, you can monitor the "state of the underlying channel":/channels#states. The channel object can be accessed through @space.channel@. + * + * The following is an example of registering a listener to wait for a channel to become attached: + * + * ```[javascript] + * space.channel.on('attached', (stateChange) => { + * console.log(stateChange) + * }); + * ``` + * + * + * + * + * > **Documentation source** + * > + * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). + * + * h2(#options). Cursor options + * + * Cursor options are set when creating or retrieving a @space@ instance. They are used to control the behavior of live cursors. + * + * The following cursor options can be set: + * + * h3(#batch). outboundBatchInterval + * + * The @outboundBatchInterval@ is the interval at which a batch of cursor positions are published, in milliseconds, for each client. This is multiplied by the number of members in a space. + * + * The default value is 25ms which is optimal for the majority of use cases. If you wish to optimize the interval further, then decreasing the value will improve performance by further 'smoothing' the movement of cursors at the cost of increasing the number of events sent. Be aware that at a certain point the rate at which a browser is able to render the changes will impact optimizations. + * + * h3(#pagination). paginationLimit + * + * The volume of messages sent can be high when using live cursors. Because of this, the last known position of every members' cursor is obtained from "history":/storage-history/history. The @paginationLimit@ is the number of pages that should be searched to find the last position of each cursor. The default is 5. + * + */ async get(name: string, options?: Subset): Promise { if (typeof name !== 'string' || name.length === 0) { throw ERR_SPACE_NAME_MISSING(); From e9d733ed6f1a8e10fbd75bf68b1c3dcc111d2208 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Tue, 26 Sep 2023 15:44:08 -0300 Subject: [PATCH 16/30] (TODO backport to docs repo) Fix description of lock status values --- src/Locks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Locks.ts b/src/Locks.ts index 0672c612..3bcdde09 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -377,7 +377,7 @@ export default class Locks extends EventEmitter { * * |_. Property |_. Description |_. Type | * | id | The unique ID of the lock request. | String | - * | status | The lock "status":#states of the event. Will be either @locked@ or @unlocked@. | String | + * | status | The lock "status":#states of the event. Will be one of @locked@, @unlocked@ or @pending@. | String | * | timestamp | The timestamp of the lock event. | Number | * | attributes | The optional attributes of the lock, such as the ID of the component it relates to. | Object | * | reason | The reason why the @request.status@ is @unlocked@. | ErrorInfo | From e0c31fea668e9671922dff0a929d73f47a64bcc1 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Wed, 27 Sep 2023 09:43:19 -0300 Subject: [PATCH 17/30] (TODO backport to docs repo) Fix documentation: "instance" refers to Space class --- src/Spaces.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Spaces.ts b/src/Spaces.ts index 2e7eaddb..6dbbb0ae 100644 --- a/src/Spaces.ts +++ b/src/Spaces.ts @@ -108,7 +108,7 @@ class Spaces { * * h2(#options). Cursor options * - * Cursor options are set when creating or retrieving a @space@ instance. They are used to control the behavior of live cursors. + * Cursor options are set when creating or retrieving a @Space@ instance. They are used to control the behavior of live cursors. * * The following cursor options can be set: * From 9d24f6ad7532f81f8ded9079ae3c902a6c741f42 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Wed, 20 Sep 2023 14:27:17 -0300 Subject: [PATCH 18/30] Add class-definitions.md documentation as TSDoc comments Copied all documentation from that document to appropriate places in code. --- src/Cursors.ts | 107 +++++++++++++++++++++++++ src/Locations.ts | 97 ++++++++++++++++++++++ src/Locks.ts | 156 ++++++++++++++++++++++++++++++++++++ src/Members.ts | 141 ++++++++++++++++++++++++++++++++ src/Space.ts | 86 ++++++++++++++++++++ src/Spaces.ts | 69 ++++++++++++++++ src/types.ts | 203 +++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 859 insertions(+) diff --git a/src/Cursors.ts b/src/Cursors.ts index ab300c90..f228953f 100644 --- a/src/Cursors.ts +++ b/src/Cursors.ts @@ -47,6 +47,13 @@ const CURSORS_CHANNEL_TAG = '::$cursors'; * * The channel is only created when a member calls @space.cursors.set()@. The live cursors channel object can be accessed through @space.cursors.channel@. To monitor the "underlying state of the cursors channel":/channels#states, the channel object can be accessed through @space.cursors.channel@. * + * + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Handles tracking of member cursors within a space. Inherits from [EventEmitter](/docs/usage.md#event-emitters). + * */ export default class Cursors extends EventEmitter { private readonly cursorBatching: CursorBatching; @@ -136,6 +143,27 @@ export default class Cursors extends EventEmitter { * | position.y | The position of the member's cursor on the Y-axis. | Number | * | data | An optional arbitrary JSON-serializable object containing additional information about the cursor. | Object | * + * + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Set the position of a cursor. If a member has not yet entered the space, this method will error. + * + * A event payload returned contains an object with 2 properties. `position` is an object with 2 required properties, `x` and `y`. These represent the position of the cursor on a 2D plane. A second optional property, `data` can also be passed. This is an object of any shape and is meant for data associated with the cursor movement (like drag or hover calculation results): + * + * ```ts + * type set = (update: { position: CursorPosition, data?: CursorData }) => void; + * ``` + * + * Example usage: + * + * ```ts + * window.addEventListener('mousemove', ({ clientX, clientY }) => { + * space.cursors.set({ position: { x: clientX, y: clientY }, data: { color: "red" } }); + * }); + * ``` + * */ async set(cursor: Pick) { const self = await this.space.members.getSelf(); @@ -210,6 +238,23 @@ export default class Cursors extends EventEmitter { * }); * ``` * + * + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Listen to `CursorUpdate` events. See [EventEmitter](/docs/usage.md#event-emitters) for overloaded usage. + * + * Available events: + * + * - ##### **update** + * + * Emits an event when a new cursor position is set. The argument supplied to the event listener is a [CursorUpdate](#cursorupdate). + * + * ```ts + * space.cursors.subscribe('update', (cursorUpdate: CursorUpdate) => {}); + * ``` + * */ subscribe( eventOrEvents: K | K[], @@ -260,6 +305,17 @@ export default class Cursors extends EventEmitter { * space.cursors.unsubscribe(); * ``` * + * + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Remove all event listeners, all event listeners for an event, or specific listeners. See [EventEmitter](/docs/usage.md#event-emitters) for detailed usage. + * + * ```ts + * space.cursors.unsubscribe('update'); + * ``` + * */ unsubscribe( eventOrEvents: K | K[], @@ -293,6 +349,23 @@ export default class Cursors extends EventEmitter { /** * * See the documentation for {@link getAll}. + * + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Get the last `CursorUpdate` object for self. + * + * ```ts + * type getSelf = () => Promise; + * ``` + * + * Example: + * + * ```ts + * const selfPosition = await space.cursors.getSelf(); + * ``` + * */ async getSelf(): Promise { const self = await this.space.members.getSelf(); @@ -305,6 +378,23 @@ export default class Cursors extends EventEmitter { /** * * See the documentation for {@link getAll}. + * + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Get the last `CursorUpdate` object for everyone else but yourself. + * + * ```ts + * type getOthers = () => Promise>; + * ``` + * + * Example: + * + * ```ts + * const otherPositions = await space.cursors.getOthers(); + * ``` + * */ async getOthers(): Promise> { const self = await this.space.members.getSelf(); @@ -408,6 +498,23 @@ export default class Cursors extends EventEmitter { * } * ``` * + * + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Get the last `CursorUpdate` object for all the members. + * + * ```ts + * type getAll = () => Promise>; + * ``` + * + * Example: + * + * ```ts + * const allLatestPositions = await space.cursors.getAll(); + * ``` + * */ async getAll(): Promise> { const channel = this.getChannel(); diff --git a/src/Locations.ts b/src/Locations.ts index 7764f991..99c6fedb 100644 --- a/src/Locations.ts +++ b/src/Locations.ts @@ -38,6 +38,13 @@ export interface LocationsEventMap { * * Member locations build upon the functionality of the Pub/Sub Channels "presence":/presence-occupancy/presence feature. Members are entered into the presence set when they "enter the space":/spaces/space#enter. * + * + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Handles the tracking of member locations within a space. Inherits from [EventEmitter](/docs/usage.md#event-emitters). + * */ export default class Locations extends EventEmitter { private lastLocationUpdate: Record = {}; @@ -93,6 +100,17 @@ export default class Locations extends EventEmitter { * await space.locations.set({ slide: '3', component: 'slide-title' }); * ``` * + * + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Set your current location. [Location](#location-1) can be any JSON-serializable object. Emits a [locationUpdate](#locationupdate) event to all connected clients in this space. + * + * ```ts + * type set = (update: Location) => Promise; + * ``` + * */ async set(location: unknown) { const self = await this.space.members.getSelf(); @@ -174,6 +192,23 @@ export default class Locations extends EventEmitter { *

Member location subscription listeners only trigger on events related to members' locations. Each event only contains the payload of the member that triggered it. Alternatively, "space state":/spaces/space can be subscribed to which returns an array of all members with their latest state every time any event is triggered.

* * + * + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Listen to events for locations. See [EventEmitter](/docs/usage.md#event-emitters) for overloaded usage. + * + * Available events: + * + * - ##### **update** + * + * Fires when a member updates their location. The argument supplied to the event listener is a [LocationUpdate](#locationupdate-1). + * + * ```ts + * space.locations.subscribe('update', (locationUpdate: LocationUpdate) => {}); + * ``` + * */ subscribe( eventOrEvents: K | K[], @@ -216,6 +251,17 @@ export default class Locations extends EventEmitter { * space.locations.unsubscribe(); * ``` * + * + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Remove all event listeners, all event listeners for an event, or specific listeners. See [EventEmitter](/docs/usage.md#event-emitters) for detailed usage. + * + * ```ts + * space.locations.unsubscribe('update'); + * ``` + * */ unsubscribe( eventOrEvents: K | K[], @@ -242,6 +288,23 @@ export default class Locations extends EventEmitter { /** * * See the documentation for {@link getAll}. + * + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Get location for self. + * + * ```ts + * type getSelf = () => Promise; + * ``` + * + * Example: + * + * ```ts + * const myLocation = await space.locations.getSelf(); + * ``` + * */ async getSelf(): Promise { const self = await this.space.members.getSelf(); @@ -251,6 +314,23 @@ export default class Locations extends EventEmitter { /** * * See the documentation for {@link getAll}. + * + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Get location for other members + * + * ```ts + * type getOthers = () => Promise>; + * ``` + * + * Example: + * + * ```ts + * const otherLocations = await space.locations.getOthers() + * ``` + * */ async getOthers(): Promise> { const members = await this.space.members.getOthers(); @@ -329,6 +409,23 @@ export default class Locations extends EventEmitter { * } * ``` * + * + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Get location for all members. + * + * ```ts + * type getAll = () => Promise>; + * ``` + * + * Example: + * + * ```ts + * const allLocations = await space.locations.getAll(); + * ``` + * */ async getAll(): Promise> { const members = await this.space.members.getAll(); diff --git a/src/Locks.ts b/src/Locks.ts index 3bcdde09..b6ed1120 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -8,6 +8,18 @@ import EventEmitter, { InvalidArgumentError, inspect, type EventListener } from import SpaceUpdate from './SpaceUpdate.js'; +/** + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Additional attributes that can be set when acquiring a lock. + * + * ```ts + * type LockAttributes = Map; + * ``` + * + */ export class LockAttributes extends Map { toJSON() { return Object.fromEntries(this); @@ -57,6 +69,13 @@ export interface LocksEventMap { * * Only transitions that result in a @locked@ or @unlocked@ status will emit a lock event that members can "@subscribe()@":#subscribe to. * + * + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Provides a mechanism to "lock" a component, reducing the chances of conflict in an application whilst being edited by multiple members. Inherits from [EventEmitter](/docs/usage.md#event-emitters). + * */ export default class Locks extends EventEmitter { // locks tracks the local state of locks, which is used to determine whether @@ -101,6 +120,24 @@ export default class Locks extends EventEmitter { * * If the lock is not currently held by a member, @get()@ will return @undefined@. Otherwise it will return the most recent lock event for the lock. * + * + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Get a lock by its id. + * + * ```ts + * type get = (lockId: string) => Lock | undefined + * ``` + * + * Example: + * + * ```ts + * const id = "/slide/1/element/3"; + * const lock = space.locks.get(id); + * ``` + * */ get(id: string): Lock | undefined { const locks = this.locks.get(id); @@ -172,6 +209,23 @@ export default class Locks extends EventEmitter { * ] * ``` * + * + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Get all locks that have the `locked` status. + * + * ```ts + * type getAll = () => Promise + * ``` + * + * Example: + * + * ```ts + * const locks = await space.locks.getAll(); + * ``` + * */ async getAll(): Promise { const allLocks: Lock[] = []; @@ -190,6 +244,23 @@ export default class Locks extends EventEmitter { /** * * See the documentation for {@link getAll}. + * + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Get all locks belonging to self that have the `locked` status. + * + * ```ts + * type getSelf = () => Promise + * ``` + * + * Example: + * + * ```ts + * const locks = await space.locks.getSelf(); + * ``` + * */ async getSelf(): Promise { const self = await this.space.members.getSelf(); @@ -202,6 +273,23 @@ export default class Locks extends EventEmitter { /** * * See the documentation for {@link getAll}. + * + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Get all locks belonging to all members except self that have the `locked` status. + * + * ```ts + * type getOthers = () => Promise + * ``` + * + * Example: + * + * ```ts + * const locks = await space.locks.getOthers(); + * ``` + * */ async getOthers(): Promise { const self = await this.space.members.getSelf(); @@ -250,6 +338,26 @@ export default class Locks extends EventEmitter { * * Once a member requests a lock by calling @acquire()@, the lock is temporarily in the "pending state":#states. An event will be emitted based on whether the lock request was successful (a status of @locked@) or invalidated (a status of @unlocked@). This can be "subscribed":#subscribe to in order for the client to know whether their lock request was successful or not. * + * + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Send a request to acquire a lock. Returns a Promise which resolves once the request has been sent. A resolved Promise holds a `pending` [Lock](#lock). An error will be thrown if a lock request with a status of `pending` or `locked` already exists, returning a rejected promise. + * + * When a lock acquisition by a member is confirmed with the `locked` status, an `update` event will be emitted. Hence to handle lock acquisition, `acquire()` needs to always be used together with `subscribe()`. + * + * ```ts + * type acquire = (lockId: string) => Promise; + * ``` + * + * Example: + * + * ```ts + * const id = "/slide/1/element/3"; + * const lockRequest = await space.locks.acquire(id); + * ``` + * */ async acquire(id: string, opts?: LockOptions): Promise { const self = await this.space.members.getSelf(); @@ -303,6 +411,24 @@ export default class Locks extends EventEmitter { *

When a member "leaves":/spaces/space#leave a space, their locks are automatically released.

* * + * + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Releases a previously requested lock. + * + * ```ts + * type release = (lockId: string) => Promise; + * ``` + * + * Example: + * + * ```ts + * const id = "/slide/1/element/3"; + * await space.locks.release(id); + * ``` + * */ async release(id: string): Promise { const self = await this.space.members.getSelf(); @@ -388,6 +514,25 @@ export default class Locks extends EventEmitter { * | member.lastEvent.timestamp | The timestamp of the most recently emitted event. | Number | * | member.profileData | The optional "profile data":/spaces/avatar#profile-data associated with the member. | Object | * + * + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Listen to lock events. See [EventEmitter](/docs/usage.md#event-emitters) for overloaded usage. + * + * Available events: + * + * - ##### **update** + * + * Listen to changes to locks. + * + * ```ts + * space.locks.subscribe('update', (lock: Lock) => {}) + * ``` + * + * The argument supplied to the callback is a [Lock](#lock), representing the lock request and it's status. + * */ subscribe(eventOrEvents: K | K[], listener?: EventListener): void; subscribe(listener?: EventListener): void; @@ -427,6 +572,17 @@ export default class Locks extends EventEmitter { * space.locks.unsubscribe(); * ``` * + * + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Remove all event listeners, all event listeners for an event, or specific listeners. See [EventEmitter](/docs/usage.md#event-emitters) for detailed usage. + * + * ```ts + * space.locks.unsubscribe('update'); + * ``` + * */ unsubscribe(eventOrEvents: K | K[], listener?: EventListener): void; unsubscribe(listener?: EventListener): void; diff --git a/src/Members.ts b/src/Members.ts index 8b714d84..18c3505a 100644 --- a/src/Members.ts +++ b/src/Members.ts @@ -46,6 +46,13 @@ export interface MembersEventMap { * * Avatar stacks build upon the functionality of the Pub/Sub Channels "presence":/presence-occupancy/presence feature. Members are entered into the presence set when they "enter the space":/spaces/space#enter. * + * + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Handles members within a space. + * */ class Members extends EventEmitter { private lastMemberUpdate: Record = {}; @@ -88,6 +95,23 @@ class Members extends EventEmitter { /** * * See the documentation for {@link getAll}. + * + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Returns a Promise which resolves to the [SpaceMember](#spacemember) object relating to the local connection. Will resolve to `null` if the client hasn't entered the space yet. + * + * ```ts + * type getSelf = () => Promise; + * ``` + * + * Example: + * + * ```ts + * const myMember = await space.members.getSelf(); + * ``` + * */ async getSelf(): Promise { return this.space.connectionId ? await this.getByConnectionId(this.space.connectionId) : null; @@ -221,6 +245,23 @@ class Members extends EventEmitter { * ] * ``` * + * + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Returns a Promise which resolves to an array of all [SpaceMember](#spacemember) objects (members) currently in the space, including any who have left and not yet timed out. (_see: [offlineTimeout](#spaceoptions)_) + * + * ```ts + * type getAll = () => Promise; + * ``` + * + * Example: + * + * ```ts + * const allMembers = await space.members.getAll(); + * ``` + * */ async getAll(): Promise { const presenceMembers = await this.space.channel.presence.get(); @@ -231,6 +272,23 @@ class Members extends EventEmitter { /** * * See the documentation for {@link getAll}. + * + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Returns a Promise which resolves to an array of all [SpaceMember](#spacemember) objects (members) currently in the space, excluding your own member object. + * + * ```ts + * type getSelf = () => Promise; + * ``` + * + * Example: + * + * ```ts + * const otherMembers = await space.members.getOthers(); + * ``` + * */ async getOthers(): Promise { const members = await this.getAll(); @@ -318,6 +376,71 @@ class Members extends EventEmitter { *

Avatar stack subscription listeners only trigger on events related to members' online status and profile updates. Each event only contains the payload of the member that triggered it. Alternatively, "space state":/spaces/space can be subscribed to which returns an array of all members with their latest state every time any event is triggered.

* * + * + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Listen to member events for the space. See [EventEmitter](/docs/usage.md#event-emitters) for overloaded usage. + * + * The argument supplied to the callback is the [SpaceMember](#spacemember) object representing the member that triggered the event. + * + * Example: + * + * ```ts + * space.members.subscribe((member: SpaceMember) => {}); + * ``` + * + * Available events: + * + * - ##### **enter** + * + * Listen to enter events of members. + * + * ```ts + * space.members.subscribe('enter', (member: SpaceMember) => {}) + * ``` + * The argument supplied to the callback is a [SpaceMember](#spacemember) object representing the member entering the space. + * + * - ##### **leave** + * + * Listen to leave events of members. The leave event will be issued when a member calls `space.leave()` or is disconnected. + * + * ```ts + * space.members.subscribe('leave', (member: SpaceMember) => {}) + * ``` + * + * The argument supplied to the callback is a [SpaceMember](#spacemember) object representing the member leaving the space. + * + * - ##### **remove** + * + * Listen to remove events of members. The remove event will be triggered when the [offlineTimeout](#spaceoptions) has passed. + * + * ```ts + * space.members.subscribe('remove', (member: SpaceMember) => {}) + * ``` + * + * The argument supplied to the callback is a [SpaceMember](#spacemember) object representing the member removed from the space. + * + * - ##### **updateProfile** + * + * Listen to profile update events of members. + * + * ```ts + * space.members.subscribe('updateProfile', (member: SpaceMember) => {}) + * ``` + * The argument supplied to the callback is a [SpaceMember](#spacemember) object representing the member entering the space. + * + * - ##### **update** + * + * Listen to `enter`, `leave`, `updateProfile` and `remove` events. + * + * ```ts + * space.members.subscribe('update', (member: SpaceMember) => {}) + * ``` + * + * The argument supplied to the callback is a [SpaceMember](#spacemember) object representing the member affected by the change. + * */ subscribe( eventOrEvents: K | K[], @@ -366,6 +489,24 @@ class Members extends EventEmitter { * space.members.unsubscribe(); * ``` * + * + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Remove all the event listeners or specific listeners. See [EventEmitter](/docs/usage.md#event-emitters) for detailed usage. + * + * ```ts + * // Unsubscribe from all events + * space.members.unsubscribe(); + * + * // Unsubscribe from enter events + * space.members.unsubscribe('enter'); + * + * // Unsubscribe from leave events + * space.members.unsubscribe('leave'); + * ``` + * */ unsubscribe( eventOrEvents: K | K[], diff --git a/src/Space.ts b/src/Space.ts index 71f92a1b..4d14b3e0 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -54,13 +54,56 @@ export interface SpaceEventMap { * * To subscribe to any events in a space, you first need to create or retrieve a space. * + * + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * An instance of a Space created using [spaces.get](#get). Inherits from [EventEmitter](/docs/usage.md#event-emitters). + * */ class Space extends EventEmitter { private readonly channelName: string; readonly connectionId: string | undefined; readonly options: SpaceOptions; + /** + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * An instance of [Locations](#locations). + * + * ```ts + * type locations = instanceof Locations; + * ``` + * + */ readonly locations: Locations; + /** + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * An instance of [Cursors](#cursors). + * + * ```ts + * type cursors = instanceof Cursors; + * ``` + * + */ readonly cursors: Cursors; + /** + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * An instance of [Members](#members). + * + * ```ts + * type members = instanceof Members; + * ``` + * + */ readonly members: Members; readonly channel: Types.RealtimeChannelPromise; readonly locks: Locks; @@ -165,6 +208,17 @@ class Space extends EventEmitter { * }); * ``` * + * + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Enter the space. Can optionally take `profileData`. This data can be an arbitrary JSON-serializable object which will be attached to the [member object](#spacemember). Returns all current space members. + * + * ```ts + * type enter = (profileData?: Record) => Promise; + * ``` + * */ async enter(profileData: ProfileData = null): Promise { return new Promise((resolve) => { @@ -209,6 +263,27 @@ class Space extends EventEmitter { * }); * ``` * + * + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * 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; + * ``` + * + * 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 }; + * }) + * ``` + * */ async updateProfileData(profileDataOrUpdateFn: ProfileData | ((update: ProfileData) => ProfileData)): Promise { const self = await this.members.getSelf(); @@ -247,6 +322,17 @@ class Space extends EventEmitter { * * Members will implicitly leave a space after 15 seconds if they abruptly disconnect. If experiencing network disruption, and they reconnect within 15 seconds, then they will remain part of the space and no @leave@ event will be emitted. * + * + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Leave the space. Can optionally take `profileData`. This triggers the `leave` event, but does not immediately remove the member from the space. See [offlineTimeout](#spaceoptions). + * + * ```ts + * type leave = (profileData?: Record) => Promise; + * ``` + * */ async leave(profileData: ProfileData = null) { const self = await this.members.getSelf(); diff --git a/src/Spaces.ts b/src/Spaces.ts index 6dbbb0ae..ac998399 100644 --- a/src/Spaces.ts +++ b/src/Spaces.ts @@ -14,11 +14,69 @@ export interface ClientWithOptions extends Types.RealtimePromise { class Spaces { private spaces: Record = {}; + /** + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Instance of the [Ably-JS](https://github.com/ably/ably-js#introduction) client that was passed to the [constructor](#constructor). + * + * ```ts + * type client = Ably.RealtimePromise; + * ``` + * + */ client: Types.RealtimePromise; + /** + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Instance of the [Ably-JS](https://github.com/ably/ably-js#introduction) connection, belonging to the client that was passed to the [constructor](#constructor). + * + * ```ts + * type connection = Ably.ConnectionPromise; + * ``` + * + */ connection: Types.ConnectionPromise; + /** + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Version of the Spaces library. + * + * ```ts + * type version = string; + * ``` + * + */ readonly version = '0.1.3'; + /** + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Create a new instance of the Space SDK by passing an instance of the realtime, promise-based [Ably client](https://github.com/ably/ably-js): + * + * ```ts + * import { Realtime } from 'ably/promise'; + * import Spaces from '@ably/spaces'; + * + * const client = new Realtime.Promise({ key: "", clientId: "" }); + * const spaces = new Spaces(client); + * ``` + * + * Please note that a [clientId](https://ably.com/docs/auth/identified-clients?lang=javascript) is required. + * + * An API key will required for [basic authentication](https://ably.com/docs/auth/basic?lang=javascript). We strongly recommended that you use [token authentication](https://ably.com/docs/realtime/authentication#token-authentication) in any production environments. + * + * Refer to the [Ably docs for the JS SDK](https://ably.com/docs/getting-started/setup?lang=javascript) for information on setting up a realtime promise client. + * + */ constructor(client: Types.RealtimePromise) { this.client = client; this.connection = client.connection; @@ -122,6 +180,17 @@ class Spaces { * * The volume of messages sent can be high when using live cursors. Because of this, the last known position of every members' cursor is obtained from "history":/storage-history/history. The @paginationLimit@ is the number of pages that should be searched to find the last position of each cursor. The default is 5. * + * + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Get or create a Space instance. Returns a [Space](#space) instance. Configure the space by passing [SpaceOptions](#spaceoptions) as the second argument. + * + * ```ts + * type get = (name: string, options?: SpaceOptions) => Promise; + * ``` + * */ async get(name: string, options?: Subset): Promise { if (typeof name !== 'string' || name.length === 0) { diff --git a/src/types.ts b/src/types.ts index 96a16ba7..63968b9f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,18 +1,92 @@ import { Types } from 'ably'; import type { LockAttributes } from './Locks.js'; +/** + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * ```ts + * type CursorsOptions = { + * outboundBatchInterval?: number; + * paginationLimit?: number; + * }; + * ``` + * + */ export interface CursorsOptions { + /** + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * The interval in milliseconds at which a batch of cursor positions are published. This is multiplied by the number of members in the space minus 1. The default value is 25ms. + * + */ outboundBatchInterval: number; + /** + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * The number of pages searched from [history](https://ably.com/docs/storage-history/history) for the last published cursor position. The default is 5. + * + */ paginationLimit: number; } +/** + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Represents a cursors position. + * + * ```ts + * type CursorPosition = { + * x: number; + * y: number; + * }; + * ``` + * + */ export interface CursorPosition { x: number; y: number; } +/** + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Represent data that can be associated with a cursor update. + * + * ```ts + * type CursorData = Record; + * ``` + * + */ export type CursorData = Record; +/** + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Represents an update to a cursor. + * + * ```ts + * type CursorUpdate = { + * name: string; + * clientId: string; + * connectionId: string; + * position: CursorPosition; + * data?: CursorData; + * }; + * ``` + * + */ export interface CursorUpdate { clientId: string; connectionId: string; @@ -20,19 +94,117 @@ export interface CursorUpdate { data?: CursorData; } +/** + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Used to configure a Space instance on creation. + * + * ```ts + * type SpaceOptions = { + * offlineTimeout?: number; + * cursors?: CursorsOptions; + * }; + * ``` + * + */ export interface SpaceOptions { + /** + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Number of milliseconds after a user loses connection or closes their browser window to wait before their [SpaceMember](#spacemember) object is removed from the members list. The default is 120000ms (2 minutes). + * + */ offlineTimeout: number; + /** + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Options relating to configuring the cursors API (see below). + * + */ cursors: CursorsOptions; } export type ProfileData = Record | null; +/** + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * A SpaceMember represents a member within a Space instance. Each new connection that enters will create a new member, even if they have the same [`clientId`](https://ably.com/docs/auth/identified-clients?lang=javascript). + * + * ```ts + * type SpaceMember = { + * clientId: string; + * connectionId: string; + * isConnected: boolean; + * profileData: Record; + * location: Location; + * lastEvent: PresenceEvent; + * }; + * ``` + * + */ export interface SpaceMember { + /** + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * The client identifier for the user, provided to the ably client instance. + * + */ clientId: string; + /** + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Identifier for the connection used by the user. This is a unique identifier. + * + */ connectionId: string; + /** + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Whether the user is connected to Ably. + * + */ isConnected: boolean; + /** + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Optional user data that can be attached to a user, such as a username or image to display in an avatar stack. + * + */ profileData: ProfileData; + /** + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * The current location of the user within the space. + * + */ location: unknown; + /** + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * The most recent event emitted by [presence](https://ably.com/docs/presence-occupancy/presence?lang=javascript) and its timestamp. Events will be either `enter`, `leave`, `update` or `present`. + * + */ lastEvent: { name: Types.PresenceAction; timestamp: number; @@ -45,8 +217,39 @@ export namespace LockStatuses { export type Unlocked = 'unlocked'; } +/** + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Represents a status of a lock. + * + * ```ts + * type LockStatus = 'pending' | 'locked' | 'unlocked'; + * ``` + * + */ export type LockStatus = LockStatuses.Pending | LockStatuses.Locked | LockStatuses.Unlocked; +/** + * > **Documentation source** + * > + * > The following documentation is copied from `docs/class-definitions.md`. + * + * Represents a Lock. + * + * ```ts + * type Lock = { + * id: string; + * status: LockStatus; + * member: SpaceMember; + * timestamp: number; + * attributes?: LockAttributes; + * reason?: Types.ErrorInfo; + * }; + * ``` + * + */ export type Lock = { id: string; status: LockStatus; From f3b83924d08f0126c0a76896dec73646e1115326 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Thu, 21 Sep 2023 16:47:02 -0300 Subject: [PATCH 19/30] Split

from end of link MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In an upcoming commit, I’m going to use Pandoc to convert the website documentation from Textile to Markdown. However, Pandoc’s parser (incorrectly, I think) thinks that the

tag belongs to the link’s URL, so split it up. --- src/Spaces.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Spaces.ts b/src/Spaces.ts index ac998399..9e401858 100644 --- a/src/Spaces.ts +++ b/src/Spaces.ts @@ -156,7 +156,9 @@ class Spaces { * ``` * *

* * From bff065b6c59052cfd2ceb60d5716a65de8424257 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Tue, 26 Sep 2023 16:07:27 -0300 Subject: [PATCH 20/30] Add a script to convert website documentation from Textile to Markdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We’ll use this very shortly. --- package.json | 4 +- scripts/convert_website_documentation.ts | 183 +++++++++++++++++++++++ 2 files changed, 185 insertions(+), 2 deletions(-) create mode 100644 scripts/convert_website_documentation.ts diff --git a/package.json b/package.json index b06ddb0a..623737b3 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,8 @@ "scripts": { "lint": "eslint .", "lint:fix": "eslint --fix .", - "format": "prettier --write --ignore-path .gitignore src demo", - "format:check": "prettier --check --ignore-path .gitignore src demo", + "format": "prettier --write --ignore-path .gitignore src demo scripts", + "format:check": "prettier --check --ignore-path .gitignore src demo scripts", "test": "vitest run", "test:watch": "vitest watch", "coverage": "vitest run --coverage", diff --git a/scripts/convert_website_documentation.ts b/scripts/convert_website_documentation.ts new file mode 100644 index 00000000..a79e4807 --- /dev/null +++ b/scripts/convert_website_documentation.ts @@ -0,0 +1,183 @@ +import { readdir, lstat, readFile, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { execFile } from 'node:child_process'; + +async function processDirectory(path: string) { + const entries = await readdir(path); + + for (const entry of entries) { + const pathToEntry = join(path, entry); + + const stat = await lstat(pathToEntry); + if (stat.isDirectory()) { + await processDirectory(pathToEntry); + continue; + } + + if (entry.endsWith('.ts')) { + await processFile(pathToEntry); + } + } +} + +async function processFile(path: string) { + console.log(`Processing: ${path}`); + const contents = await readFile(path, { encoding: 'utf-8' }); + + const beginWebsiteDocumentationRegexp = //g; + const endWebsiteDocumentationRegexp = //g; + + const lines = contents.split('\n'); + + const startLineIndices: number[] = []; + const endLineIndices: number[] = []; + + for (const [lineIndex, line] of lines.entries()) { + if (beginWebsiteDocumentationRegexp.exec(line)) { + startLineIndices.push(lineIndex); + } + if (endWebsiteDocumentationRegexp.exec(line)) { + endLineIndices.push(lineIndex); + } + } + + for (let documentationIndex = 0; documentationIndex < startLineIndices.length; documentationIndex++) { + const startLineIndex = startLineIndices[documentationIndex]; + const endLineIndex = endLineIndices[documentationIndex]; + + const documentation = lines.slice(startLineIndex + 1, endLineIndex).join('\n'); + const documentationLineCount = endLineIndex - startLineIndex - 1; + + // Convert the documentation comment. + const converted = await convertWebsiteDocumentationCommentFragment(documentation); + + // Replace the documentation comment in `lines`. + const convertedLines = converted.split('\n'); + lines.splice(startLineIndex + 1, documentationLineCount, ...convertedLines); + + const addedLinesCount = convertedLines.length - documentationLineCount; + + // Shift the line indices to reflect the length of the converted documentation comment. + for ( + let indexOfDocumentationToShiftLineNumbers = documentationIndex + 1; + indexOfDocumentationToShiftLineNumbers < startLineIndices.length; + indexOfDocumentationToShiftLineNumbers++ + ) { + startLineIndices[indexOfDocumentationToShiftLineNumbers] += addedLinesCount; + endLineIndices[indexOfDocumentationToShiftLineNumbers] += addedLinesCount; + } + } + + // Write the new contents of the file. + const newContents = lines.join('\n'); + await writeFile(path, newContents, { encoding: 'utf-8' }); +} + +async function convertWebsiteDocumentationCommentFragment(commentFragment: string) { + const prefixStrippingResult = strippingPrefixOfCommentFragment(commentFragment); + const tagged = tagCommentFragmentLines(prefixStrippingResult.content); + + const lines: string[] = []; + + for (const taggedLines of tagged) { + switch (taggedLines.type) { + case 'textile': + lines.push(...(await convertTextileLines(taggedLines.lines))); + break; + case 'codeBlock': + lines.push(...convertCodeBlockLines(taggedLines.lines)); + break; + } + } + + return restoringPrefixOfCommentFragment(lines.join('\n'), prefixStrippingResult.prefix); +} + +async function convertTextileLines(textileLines: string[]) { + const pandocStdoutPromise = new Promise((resolve, reject) => { + const childProcess = execFile( + '/opt/homebrew/bin/pandoc', + // We choose gfm over commonmark for tables support. + ['--from', 'textile', '--to', 'gfm', '--wrap=preserve'], + (error, stdout, stderr) => { + if (error) { + reject(error); + } else { + resolve(stdout); + } + }, + ); + + // I don’t fully understand how writing works and whether this always succeeds in writing the full thing; keep an eye out for any weirdness and revisit if necessary. + childProcess.stdin!.write(textileLines.join('\n')); + childProcess.stdin!.end(); + }); + + const pandocStdout = await pandocStdoutPromise; + + return pandocStdout.split('\n'); +} + +function convertCodeBlockLines(codeBlockLines: string[]) { + // remove square brackets from language tag + const firstLine = codeBlockLines[0].replace(/[[\]]/g, ''); + return [firstLine, ...codeBlockLines.slice(1)]; +} + +type TaggedLines = { type: 'textile' | 'codeBlock'; lines: string[] }; + +function tagCommentFragmentLines(commentFragment: string) { + const lines = commentFragment.split('\n'); + + const result: TaggedLines[] = []; + + let current: TaggedLines | null = null; + + for (const line of lines) { + if (line.startsWith('```')) { + if (current && current.type === 'codeBlock') { + // end of code block + current.lines.push(line); + result.push(current); + current = null; + } else { + if (current) { + result.push(current); + } + + // start of code block + current = { type: 'codeBlock', lines: [line] }; + } + } else { + if (current) { + current.lines.push(line); + } else { + current = { type: 'textile', lines: [line] }; + } + } + } + + if (current) { + result.push(current); + current = null; + } + + return result; +} + +function strippingPrefixOfCommentFragment(commentFragment: string) { + const lines = commentFragment.split('\n'); + const prefix = /\s+\* /g.exec(lines[0])![0]; + const newLines = lines.map((line) => line.substring(prefix.length)); + + return { content: newLines.join('\n'), prefix }; +} + +function restoringPrefixOfCommentFragment(content: string, prefix: string) { + const lines = content.split('\n'); + const newLines = lines.map((line) => `${prefix}${line}`); + + return newLines.join('\n'); +} + +processDirectory('src'); From 60d482e6b840e36ed450060c226c108d72ae024f Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Wed, 20 Sep 2023 17:31:17 -0300 Subject: [PATCH 21/30] Convert website documentation from Textile to Markdown Ran `npx ts-node convert_website_documentation.ts && npm run format`. --- src/Cursors.ts | 135 ++++++++++++++++++++++++++++------------------- src/Locations.ts | 94 +++++++++++++++++---------------- src/Locks.ts | 128 +++++++++++++++++++++++--------------------- src/Members.ts | 112 +++++++++++++++++++++------------------ src/Space.ts | 100 +++++++++++++++++++---------------- src/Spaces.ts | 67 ++++++++++++----------- 6 files changed, 347 insertions(+), 289 deletions(-) diff --git a/src/Cursors.ts b/src/Cursors.ts index f228953f..f06e3702 100644 --- a/src/Cursors.ts +++ b/src/Cursors.ts @@ -26,26 +26,32 @@ const CURSORS_CHANNEL_TAG = '::$cursors'; * * Cursor events are emitted whenever a member moves their mouse within a space. In order to optimize the efficiency and frequency of updates, cursor position events are automatically batched. The batching interval may be customized in order to further optimize for increased performance versus the number of events published. * - * Live cursor updates are not available as part of the "space state":/spaces/space#subscribe and must be subscribed to using "@space.cursors.subscribe()@":#subscribe. + * Live cursor updates are not available as part of the [space state](/spaces/space#subscribe) and must be subscribed to using [`space.cursors.subscribe()`](#subscribe). * * + * * * * > **Documentation source** * > * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). * - * h2(#foundations). Live cursor foundations + * ## Live cursor foundations + * + * The Spaces SDK is built upon existing Ably functionality available in Ably’s Core SDKs. Understanding which core features are used to provide the abstractions in the Spaces SDK enables you to manage space state and build additional functionality into your application. * - * The Spaces SDK is built upon existing Ably functionality available in Ably's Core SDKs. Understanding which core features are used to provide the abstractions in the Spaces SDK enables you to manage space state and build additional functionality into your application. + * Live cursors build upon the functionality of the Pub/Sub Channels [presence](/presence-occupancy/presence) feature. * - * Live cursors build upon the functionality of the Pub/Sub Channels "presence":/presence-occupancy/presence feature. + * Due to the high frequency at which updates are streamed for cursor movements, live cursors utilizes its own [channel](/channels). The other features of the Spaces SDK, such as avatar stacks, member locations and component locking all share a single channel. For this same reason, cursor position updates are not included in the [space state](/spaces/space) and may only be subscribed to on the `cursors` namespace. * - * Due to the high frequency at which updates are streamed for cursor movements, live cursors utilizes its own "channel":/channels. The other features of the Spaces SDK, such as avatar stacks, member locations and component locking all share a single channel. For this same reason, cursor position updates are not included in the "space state":/spaces/space and may only be subscribed to on the @cursors@ namespace. + * The channel is only created when a member calls `space.cursors.set()`. The live cursors channel object can be accessed through `space.cursors.channel`. To monitor the [underlying state of the cursors channel](/channels#states), the channel object can be accessed through `space.cursors.channel`. * - * The channel is only created when a member calls @space.cursors.set()@. The live cursors channel object can be accessed through @space.cursors.channel@. To monitor the "underlying state of the cursors channel":/channels#states, the channel object can be accessed through @space.cursors.channel@. * * * > **Documentation source** @@ -88,36 +94,58 @@ export default class Cursors extends EventEmitter { * > * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). * - * Set the position of a member's cursor using the @set()@ method. A position must contain an X-axis value and a Y-axis value to set the cursor position on a 2D plane. Calling @set()@ will emit a cursor event so that other members are informed of the cursor movement in realtime. + * Set the position of a member’s cursor using the `set()` method. A position must contain an X-axis value and a Y-axis value to set the cursor position on a 2D plane. Calling `set()` will emit a cursor event so that other members are informed of the cursor movement in realtime. * - * A member must have been "entered":/spaces/space#enter into the space to set their cursor position. + * A member must have been [entered](/spaces/space#enter) into the space to set their cursor position. * - * The @set()@ method takes the following parameters: + * The `set()` method takes the following parameters: * - * |_. Parameter |_. Description |_. Type | - * | position.x | The position of the member's cursor on the X-axis. | Number | - * | position.y | The position of the member's cursor on the Y-axis. | Number | - * | data | An optional arbitrary JSON-serializable object containing additional information about the cursor, such as a color. | Object | + * | Parameter | Description | Type | + * |------------|---------------------------------------------------------------------------------------------------------------------|--------| + * | position.x | The position of the member’s cursor on the X-axis. | Number | + * | position.y | The position of the member’s cursor on the Y-axis. | Number | + * | data | An optional arbitrary JSON-serializable object containing additional information about the cursor, such as a color. | Object | * * * - * The following is an example of a member setting their cursor position by adding an event listener to obtain their cursor coordinates and then publishing their position using the @set()@ method: + * The following is an example of a member setting their cursor position by adding an event listener to obtain their cursor coordinates and then publishing their position using the `set()` method: * - * ```[javascript] + * ```javascript * window.addEventListener('mousemove', ({ clientX, clientY }) => { * space.cursors.set({ position: { x: clientX, y: clientY }, data: { color: 'red' } }); * }); * ``` + * The following is an example payload of a cursor event. Cursor events are uniquely identifiable by the `connectionId` of a cursor. * - * The following is an example payload of a cursor event. Cursor events are uniquely identifiable by the @connectionId@ of a cursor. - * - * ```[json] + * ```json * { * "hd9743gjDc": { * "connectionId": "hd9743gjDc", @@ -132,16 +160,17 @@ export default class Cursors extends EventEmitter { * } * } * ``` - * * The following are the properties of a cursor event payload: * - * |_. Property |_. Description |_. Type | - * | connectionId | The unique identifier of the member's "connection":/connect. | String | - * | clientId | The "client identifier":/auth/identified-clients for the member. | String | - * | position | An object containing the position of a member's cursor. | Object | - * | position.x | The position of the member's cursor on the X-axis. | Number | - * | position.y | The position of the member's cursor on the Y-axis. | Number | - * | data | An optional arbitrary JSON-serializable object containing additional information about the cursor. | Object | + * | Property | Description | Type | + * |--------------|----------------------------------------------------------------------------------------------------|--------| + * | connectionId | The unique identifier of the member’s [connection](/connect). | String | + * | clientId | The [client identifier](/auth/identified-clients) for the member. | String | + * | position | An object containing the position of a member’s cursor. | Object | + * | position.x | The position of the member’s cursor on the X-axis. | Number | + * | position.y | The position of the member’s cursor on the Y-axis. | Number | + * | data | An optional arbitrary JSON-serializable object containing additional information about the cursor. | Object | + * * * * > **Documentation source** @@ -224,15 +253,19 @@ export default class Cursors extends EventEmitter { * > * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). * - * Subscribe to cursor events by registering a listener. Cursor events are emitted whenever a member moves their cursor by calling @set()@. Use the @subscribe()@ method on the @cursors@ object of a space to receive updates. + * Subscribe to cursor events by registering a listener. Cursor events are emitted whenever a member moves their cursor by calling `set()`. Use the `subscribe()` method on the `cursors` object of a space to receive updates. * * * * The following is an example of subscribing to cursor events: * - * ```[javascript] + * ```javascript * space.cursors.subscribe('update', (cursorUpdate) => { * console.log(cursorUpdate); * }); @@ -295,13 +328,12 @@ export default class Cursors extends EventEmitter { * * The following is an example of removing a listener for cursor update events: * - * ```[javascript] + * ```javascript * space.cursors.unsubscribe(`update`, listener); * ``` - * * Or remove all listeners: * - * ```[javascript] + * ```javascript * space.cursors.unsubscribe(); * ``` * @@ -413,15 +445,14 @@ export default class Cursors extends EventEmitter { * * Cursor positions can be retrieved in one-off calls. These are local calls that retrieve the latest position of cursors retained in memory by the SDK. * - * The following is an example of retrieving a member's own cursor position: + * The following is an example of retrieving a member’s own cursor position: * - * ```[javascript] + * ```javascript * const myCursor = await space.cursors.getSelf(); * ``` + * The following is an example payload returned by `space.cursors.getSelf()`: * - * The following is an example payload returned by @space.cursors.getSelf()@: - * - * ```[json] + * ```json * { * “clientId”: “DzOBJqgGXzyUBb816Oa6i”, * “connectionId”: “__UJBKZchX”, @@ -431,16 +462,14 @@ export default class Cursors extends EventEmitter { * } * } * ``` - * * The following is an example of retrieving the cursor positions for all members other than the member themselves: * - * ```[javascript] + * ```javascript * const othersCursors = await space.cursors.getOthers(); * ``` + * The following is an example payload returned by `space.cursors.getOthers()`: * - * The following is an example payload returned by @space.cursors.getOthers()@: - * - * ```[json] + * ```json * { * "3ej3q7yZZz": { * "clientId": "yyXidHatpP3hJpMpXZi8W", @@ -460,16 +489,14 @@ export default class Cursors extends EventEmitter { * } * } * ``` + * The following is an example of retrieving the cursor positions for all members, including the member themselves. `getAll()` is useful for retrieving the initial position of members’ cursors. * - * The following is an example of retrieving the cursor positions for all members, including the member themselves. @getAll()@ is useful for retrieving the initial position of members' cursors. - * - * ```[javascript] + * ```javascript * const allCursors = await space.cursors.getAll(); * ``` + * The following is an example payload returned by `space.cursors.getAll()`: * - * The following is an example payload returned by @space.cursors.getAll()@: - * - * ```[json] + * ```json * { * "3ej3q7yZZz": { * "clientId": "yyXidHatpP3hJpMpXZi8W", diff --git a/src/Locations.ts b/src/Locations.ts index 99c6fedb..d5401cb8 100644 --- a/src/Locations.ts +++ b/src/Locations.ts @@ -23,20 +23,22 @@ export interface LocationsEventMap { * > * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). * - * The member location feature enables you to track where members are within a space, to see which part of your application they're interacting with. A location could be the form field they have selected, the cell they're currently editing in a spreadsheet, or the slide they're viewing within a slide deck. Multiple members can be present in the same location. + * The member location feature enables you to track where members are within a space, to see which part of your application they’re interacting with. A location could be the form field they have selected, the cell they’re currently editing in a spreadsheet, or the slide they’re viewing within a slide deck. Multiple members can be present in the same location. + * + * Member locations are used to visually display which component other members currently have selected, or are currently active on. Events are emitted whenever a member sets their location, such as when they click on a new cell, or slide. Events are received by members subscribed to location events and the UI component can be highlighted with the active member’s profile data to visually display their location. * - * Member locations are used to visually display which component other members currently have selected, or are currently active on. Events are emitted whenever a member sets their location, such as when they click on a new cell, or slide. Events are received by members subscribed to location events and the UI component can be highlighted with the active member's profile data to visually display their location. * * * > **Documentation source** * > * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). * - * h2(#foundations). Member location foundations + * ## Member location foundations + * + * The Spaces SDK is built upon existing Ably functionality available in Ably’s Core SDKs. Understanding which core features are used to provide the abstractions in the Spaces SDK enables you to manage space state and build additional functionality into your application. * - * The Spaces SDK is built upon existing Ably functionality available in Ably's Core SDKs. Understanding which core features are used to provide the abstractions in the Spaces SDK enables you to manage space state and build additional functionality into your application. + * Member locations build upon the functionality of the Pub/Sub Channels [presence](/presence-occupancy/presence) feature. Members are entered into the presence set when they [enter the space](/spaces/space#enter). * - * Member locations build upon the functionality of the Pub/Sub Channels "presence":/presence-occupancy/presence feature. Members are entered into the presence set when they "enter the space":/spaces/space#enter. * * * > **Documentation source** @@ -88,15 +90,15 @@ export default class Locations extends EventEmitter { * > * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). * - * Use the @set()@ method to emit a location event in realtime when a member changes their location. This will be received by all location subscribers to inform them of the location change. A @location@ can be any JSON-serializable object, such as a slide number or element ID. + * Use the `set()` method to emit a location event in realtime when a member changes their location. This will be received by all location subscribers to inform them of the location change. A `location` can be any JSON-serializable object, such as a slide number or element ID. * - * A member must have been "entered":/spaces/space#enter into the space to set their location. + * A member must have been [entered](/spaces/space#enter) into the space to set their location. * - * The @set()@ method is commonly combined with "@addEventListener()@":https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener or a React "synthetic event":https://react.dev/learn/responding-to-events#adding-event-handlers, such as @onClick@ or @onHover@. + * The `set()` method is commonly combined with [`addEventListener()`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) or a React [synthetic event](https://react.dev/learn/responding-to-events#adding-event-handlers), such as `onClick` or `onHover`. * * The following is an example of a member setting their location to a specific slide number, and element on that slide: * - * ```[javascript] + * ```javascript * await space.locations.set({ slide: '3', component: 'slide-title' }); * ``` * @@ -128,25 +130,28 @@ export default class Locations extends EventEmitter { * > * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). * - * Subscribe to location events by registering a listener. Location events are emitted whenever a member changes location by calling "@set()@":#set. Use the @subscribe()@ method on the @locations@ namespace of the space to receive updates. + * Subscribe to location events by registering a listener. Location events are emitted whenever a member changes location by calling [`set()`](#set). Use the `subscribe()` method on the `locations` namespace of the space to receive updates. * - * All location changes are @update@ events. When a location update is received, clear the highlight from the UI component of the member's @previousLocation@ and add it to @currentLocation@. + * All location changes are `update` events. When a location update is received, clear the highlight from the UI component of the member’s `previousLocation` and add it to `currentLocation`. * * * * The following is an example of subscribing to location events: * - * ```[javascript] + * ```javascript * space.locations.subscribe('update', (locationUpdate) => { * console.log(locationUpdate); * }); * ``` + * The following is an example payload of a location event. Information about location is returned in `currentLocation` and `previousLocation`: * - * The following is an example payload of a location event. Information about location is returned in @currentLocation@ and @previousLocation@: - * - * ```[json] + * ```json * { * "member": { * "clientId": "clemons#142", @@ -175,22 +180,27 @@ export default class Locations extends EventEmitter { * } * } * ``` - * * The following are the properties of a location event payload: * - * |_. Property |_. Description |_. Type | - * | member.clientId | The "client identifier":/auth/identified-clients for the member. | String | - * | member.connectionId | The unique identifier of the member's "connection":/connect. | String | - * | member.isConnected | Whether the member is connected to Ably or not. | Boolean | - * | member.lastEvent.name | The most recent "event":/spaces/avatar emitted by the member. Will be one of @enter@, @update@, @leave@ or @remove@. | String | - * | member.lastEvent.timestamp | The timestamp of the most recently emitted event. | Number | - * | member.profileData | The optional "profile data":/spaces/avatar#profile-data associated with the member. | Object | - * | previousLocation | The previous location of the member. | Object | - * | currentLocation | The new location of the member. | Object | + * | Property | Description | Type | + * |----------------------------|-----------------------------------------------------------------------------------------------------------------------|---------| + * | member.clientId | The [client identifier](/auth/identified-clients) for the member. | String | + * | member.connectionId | The unique identifier of the member’s [connection](/connect). | String | + * | member.isConnected | Whether the member is connected to Ably or not. | Boolean | + * | member.lastEvent.name | The most recent [event](/spaces/avatar) emitted by the member. Will be one of `enter`, `update`, `leave` or `remove`. | String | + * | member.lastEvent.timestamp | The timestamp of the most recently emitted event. | Number | + * | member.profileData | The optional [profile data](/spaces/avatar#profile-data) associated with the member. | Object | + * | previousLocation | The previous location of the member. | Object | + * | currentLocation | The new location of the member. | Object | * * + * * * * > **Documentation source** @@ -241,13 +251,12 @@ export default class Locations extends EventEmitter { * * The following is an example of removing a listener for location update events: * - * ```[javascript] + * ```javascript * space.locations.unsubscribe('update', listener); * ``` - * * Or remove all listeners: * - * ```[javascript] + * ```javascript * space.locations.unsubscribe(); * ``` * @@ -348,30 +357,27 @@ export default class Locations extends EventEmitter { * * Member locations can also be retrieved in one-off calls. These are local calls and retrieve the location of members retained in memory by the SDK. * - * The following is an example of retrieving a member's own location: + * The following is an example of retrieving a member’s own location: * - * ```[javascript] + * ```javascript * const myLocation = await space.locations.getSelf(); * ``` + * The following is an example payload returned by `space.locations.getSelf()`. It will return the properties of the member’s `location`: * - * The following is an example payload returned by @space.locations.getSelf()@. It will return the properties of the member's @location@: - * - * ```[json] + * ```json * { * "slide": "3", * "component": "slide-title" * } * ``` - * * The following is an example of retrieving the location objects of all members other than the member themselves. * - * ```[javascript] + * ```javascript * const othersLocations = await space.locations.getOthers(); * ``` + * The following is an example payload returned by `space.locations.getOthers()`: It will return the properties of all member’s `location` by their `connectionId`: * - * The following is an example payload returned by @space.locations.getOthers()@: It will return the properties of all member's @location@ by their @connectionId@: - * - * ```[json] + * ```json * { * "xG6H3lnrCn": { * "slide": "1", @@ -383,16 +389,14 @@ export default class Locations extends EventEmitter { * } * } * ``` - * * The following is an example of retrieving the location objects of all members, including the member themselves: * - * ```[javascript] + * ```javascript * const allLocations = await space.locations.getAll(); * ``` + * The following is an example payload returned by `space.locations.getAll()`. It will return the properties of all member’s `location` by their `connectionId`: * - * The following is an example payload returned by @space.locations.getAll()@. It will return the properties of all member's @location@ by their @connectionId@: - * - * ```[json] + * ```json * { * "xG6H3lnrCn": { * "slide": "1", diff --git a/src/Locks.ts b/src/Locks.ts index b6ed1120..60672271 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -39,35 +39,45 @@ export interface LocksEventMap { * > * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). * - * The component locking feature enables members to optimistically lock stateful UI components before editing them. This reduces the chances of conflicting changes being made to the same component by different members. A component could be a cell in a spreadsheet that a member is updating, or an input field on a form they're filling in. + * The component locking feature enables members to optimistically lock stateful UI components before editing them. This reduces the chances of conflicting changes being made to the same component by different members. A component could be a cell in a spreadsheet that a member is updating, or an input field on a form they’re filling in. * * Once a lock has been acquired by a member, the component that it relates to can be updated in the UI to visually indicate to other members that it is locked and and which member has the lock. The component can then be updated once the editing member has released the lock to indicate that it is now unlocked. * - * Each lock is identified by a unique string ID, and only a single member may hold a lock with a given string at any one time. A lock will exist in one of three "states":#states and may only transition between states in specific circumstances. + * Each lock is identified by a unique string ID, and only a single member may hold a lock with a given string at any one time. A lock will exist in one of three [states](#states) and may only transition between states in specific circumstances. * * * - * h2(#states). Lock states + * ## Lock states * - * Component locking is handled entirely client-side. Members may begin to optimistically edit a component as soon as they call "@acquire()@":#acquire on the lock identifier related to it. Alternatively, you could wait until they receive a @locked@ event and display a spinning symbol in the UI until this is received. In either case a subsequent @unlocked@ event may invalidate that member's lock request if another member acquired it earlier. The time for confirmation of whether a lock request was successful or rejected is, on average, in the hundreds of milliseconds, however your code should handle all possible lock state transitions. + * Component locking is handled entirely client-side. Members may begin to optimistically edit a component as soon as they call [`acquire()`](#acquire) on the lock identifier related to it. Alternatively, you could wait until they receive a `locked` event and display a spinning symbol in the UI until this is received. In either case a subsequent `unlocked` event may invalidate that member’s lock request if another member acquired it earlier. The time for confirmation of whether a lock request was successful or rejected is, on average, in the hundreds of milliseconds, however your code should handle all possible lock state transitions. * * A lock will be in one of the following states: * - * - @pending@ := A member has requested a lock by calling "@acquire()@":#acquire. - * - @locked@ := The lock is confirmed to be held by the requesting member. - * - @unlocked@ := The lock is confirmed to not be locked by the requesting member, or has been "released":#release by a member previously holding the lock. + * `pending` + * A member has requested a lock by calling [`acquire()`](#acquire). + * + * `locked` + * The lock is confirmed to be held by the requesting member. + * + * `unlocked` + * The lock is confirmed to not be locked by the requesting member, or has been [released](#release) by a member previously holding the lock. * * The following lock state transitions may occur: * - * * None → @pending@: a member calls "@acquire()@":#acquire to request a lock. - * * @pending@ → @locked@: the requesting member holds the lock. - * * @pending@ → @unlocked@: the requesting member does not hold the lock, since another member already holds it. - * * @locked@ → @unlocked@: the lock was either explicitly "released":#release by the member, or their request was invalidated by a concurrent request which took precedence. - * * @unlocked@ → @locked@: the requesting member reacquired a lock they previously held. + * - None → `pending`: a member calls [`acquire()`](#acquire) to request a lock. + * - `pending` → `locked`: the requesting member holds the lock. + * - `pending` → `unlocked`: the requesting member does not hold the lock, since another member already holds it. + * - `locked` → `unlocked`: the lock was either explicitly [released](#release) by the member, or their request was invalidated by a concurrent request which took precedence. + * - `unlocked` → `locked`: the requesting member reacquired a lock they previously held. + * + * Only transitions that result in a `locked` or `unlocked` status will emit a lock event that members can [`subscribe()`](#subscribe) to. * - * Only transitions that result in a @locked@ or @unlocked@ status will emit a lock event that members can "@subscribe()@":#subscribe to. * * * > **Documentation source** @@ -97,28 +107,26 @@ export default class Locks extends EventEmitter { * > * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). * - * Use the @get()@ method to query whether a lock is currently locked, and by which member if it is. The lock is identifiable by its unique string ID. + * Use the `get()` method to query whether a lock is currently locked, and by which member if it is. The lock is identifiable by its unique string ID. * * The following is an example of checking whether a lock identifier is currently locked: * - * ```[javascript] + * ```javascript * const isLocked = space.locks.get(id) !== undefined; * ``` - * * The following is an example of checking which member holds the lock: * - * ```[javascript] + * ```javascript * const { member } = space.locks.get(id); * ``` - * * The following is an example of viewing the attributes assigned to the lock by the member holding it: * - * ```[javascript] + * ```javascript * const { request } = space.locks.get(id); * const viewLock = request.attributes.get(key); * ``` + * If the lock is not currently held by a member, `get()` will return `undefined`. Otherwise it will return the most recent lock event for the lock. * - * If the lock is not currently held by a member, @get()@ will return @undefined@. Otherwise it will return the most recent lock event for the lock. * * * > **Documentation source** @@ -160,13 +168,12 @@ export default class Locks extends EventEmitter { * * The following is an example of retrieving an array of all currently held locks in a space: * - * ```[javascript] + * ```javascript * const allLocks = await space.locks.getAll(); * ``` + * The following is an example payload returned by `space.locks.getAll()`: * - * The following is an example payload returned by @space.locks.getAll()@: - * - * ```[json] + * ```json * [ * { * "id": "s1-c2", @@ -305,27 +312,25 @@ export default class Locks extends EventEmitter { * > * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). * - * Use the @acquire()@ method to attempt to acquire a lock with a given unique ID. Additional @attributes@ may be passed when trying to acquire a lock that can contain a set of arbitrary key-value pairs. An example of using @attributes@ is to store the component ID the lock relates to so that it can be easily updated in the UI with a visual indication of its lock status. + * Use the `acquire()` method to attempt to acquire a lock with a given unique ID. Additional `attributes` may be passed when trying to acquire a lock that can contain a set of arbitrary key-value pairs. An example of using `attributes` is to store the component ID the lock relates to so that it can be easily updated in the UI with a visual indication of its lock status. * - * A member must have been "entered":/spaces/space#enter into the space to acquire a lock. + * A member must have been [entered](/spaces/space#enter) into the space to acquire a lock. * * The following is an example of attempting to acquire a lock: * - * ```[javascript] + * ```javascript * const acquireLock = await space.locks.acquire(id); * ``` + * The following is an example of passing a set of `attributes` when trying to acquire a lock: * - * The following is an example of passing a set of @attributes@ when trying to acquire a lock: - * - * ```[javascript] + * ```javascript * const lockAttributes = new Map(); * lockAttributes.set('component', 'cell-d3'); * const acquireLock = await space.locks.acquire(id, { lockAttributes }); * ``` + * The following is an example payload returned by `space.locks.acquire()`. The promise will resolve to a lock request with the `pending` status: * - * The following is an example payload returned by @space.locks.acquire()@. The promise will resolve to a lock request with the @pending@ status: - * - * ```[json] + * ```json * { * "id": "s2-d14", * "status": "pending", @@ -335,8 +340,8 @@ export default class Locks extends EventEmitter { * } * } * ``` + * Once a member requests a lock by calling `acquire()`, the lock is temporarily in the [pending state](#states). An event will be emitted based on whether the lock request was successful (a status of `locked`) or invalidated (a status of `unlocked`). This can be [subscribed](#subscribe) to in order for the client to know whether their lock request was successful or not. * - * Once a member requests a lock by calling @acquire()@, the lock is temporarily in the "pending state":#states. An event will be emitted based on whether the lock request was successful (a status of @locked@) or invalidated (a status of @unlocked@). This can be "subscribed":#subscribe to in order for the client to know whether their lock request was successful or not. * * * > **Documentation source** @@ -397,19 +402,23 @@ export default class Locks extends EventEmitter { * > * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). * - * Use the @release()@ method to explicitly release a lock once a member has finished editing the related component. For example, the @release()@ method can be called once a user clicks outside of the component, such as clicking on another cell within a spreadsheet. Any UI indications that the previous cell was locked can then be cleared. + * Use the `release()` method to explicitly release a lock once a member has finished editing the related component. For example, the `release()` method can be called once a user clicks outside of the component, such as clicking on another cell within a spreadsheet. Any UI indications that the previous cell was locked can then be cleared. * * The following is an example of releasing a lock: * - * ```[javascript] + * ```javascript * await space.locks.release(id); * ``` - * - * Releasing a lock will emit a lock event with a "lock status":#states of @unlocked@. + * Releasing a lock will emit a lock event with a [lock status](#states) of `unlocked`. * * + * * * * > **Documentation source** @@ -453,18 +462,17 @@ export default class Locks extends EventEmitter { * > * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). * - * Subscribe to lock events by registering a listener. Lock events are emitted whenever the "lock state":#states transitions into @locked@ or @unlocked@. Use the @subscribe()@ method on the @locks@ namespace of the space to receive updates. + * Subscribe to lock events by registering a listener. Lock events are emitted whenever the [lock state](#states) transitions into `locked` or `unlocked`. Use the `subscribe()` method on the `locks` namespace of the space to receive updates. * - * All lock events are @update@ events. When a lock event is received, UI components can be updated to add and remove visual indications of which member is locking them, as well as enabling and disabling the ability for other members to edit them. + * All lock events are `update` events. When a lock event is received, UI components can be updated to add and remove visual indications of which member is locking them, as well as enabling and disabling the ability for other members to edit them. * * The following is an example of subscribing to lock events: * - * ```[javascript] + * ```javascript * space.locks.subscribe('update', (lock) => { * console.log(lock); * }); * ``` - * * The following is an example payload of a lock event: * * ```json @@ -498,21 +506,22 @@ export default class Locks extends EventEmitter { * } * } * ``` - * * The following are the properties of a lock event payload: * - * |_. Property |_. Description |_. Type | - * | id | The unique ID of the lock request. | String | - * | status | The lock "status":#states of the event. Will be one of @locked@, @unlocked@ or @pending@. | String | - * | timestamp | The timestamp of the lock event. | Number | - * | attributes | The optional attributes of the lock, such as the ID of the component it relates to. | Object | - * | reason | The reason why the @request.status@ is @unlocked@. | ErrorInfo | - * | member.clientId | The "client identifier":/auth/identified-clients for the member. | String | - * | member.connectionId | The unique identifier of the member's "connection":/connect. | String | - * | member.isConnected | Whether the member is connected to Ably or not. | Boolean | - * | member.lastEvent.name | The most recent "event":/spaces/avatar#events emitted by the member. Will be one of @enter@, @update@, @leave@ or @remove@. | String | - * | member.lastEvent.timestamp | The timestamp of the most recently emitted event. | Number | - * | member.profileData | The optional "profile data":/spaces/avatar#profile-data associated with the member. | Object | + * | Property | Description | Type | + * |----------------------------|------------------------------------------------------------------------------------------------------------------------------|-----------| + * | id | The unique ID of the lock request. | String | + * | status | The lock [status](#states) of the event. Will be one of `locked`, `unlocked` or `pending`. | String | + * | timestamp | The timestamp of the lock event. | Number | + * | attributes | The optional attributes of the lock, such as the ID of the component it relates to. | Object | + * | reason | The reason why the `request.status` is `unlocked`. | ErrorInfo | + * | member.clientId | The [client identifier](/auth/identified-clients) for the member. | String | + * | member.connectionId | The unique identifier of the member’s [connection](/connect). | String | + * | member.isConnected | Whether the member is connected to Ably or not. | Boolean | + * | member.lastEvent.name | The most recent [event](/spaces/avatar#events) emitted by the member. Will be one of `enter`, `update`, `leave` or `remove`. | String | + * | member.lastEvent.timestamp | The timestamp of the most recently emitted event. | Number | + * | member.profileData | The optional [profile data](/spaces/avatar#profile-data) associated with the member. | Object | + * * * * > **Documentation source** @@ -562,13 +571,12 @@ export default class Locks extends EventEmitter { * * The following is an example of removing a listener for lock update events: * - * ```[javascript] + * ```javascript * space.locks.unsubscribe('update', listener); * ``` - * * Or remove all listeners: * - * ```[javascript] + * ```javascript * space.locks.unsubscribe(); * ``` * diff --git a/src/Members.ts b/src/Members.ts index 18c3505a..b6b2c754 100644 --- a/src/Members.ts +++ b/src/Members.ts @@ -20,31 +20,44 @@ export interface MembersEventMap { * * Avatar stacks are the most common way of showing the online status of members in an application by displaying an avatar for each member. Events are emitted whenever a member enters or leaves a space, or updates their profile data. Additional information can also be provided, such as a profile picture and email address. * - * Subscribe to the @space.members@ namespace in order to keep your avatar stack updated in realtime. + * Subscribe to the `space.members` namespace in order to keep your avatar stack updated in realtime. * - * h2(#events). Event types + * ## Event types * * The following four event types are emitted by members: * - * - @enter@ := A new member has entered the space. The member has either entered explicitly by calling "@space.enter()@":/spaces/space#enter, or has attempted to update their profile data before entering a space, which will instead emit an @enter@ event. - * - @update@ := A member has updated their profile data by calling "@space.updateProfileData()@":/spaces/space#update-profile. - * - @leave@ := A member has left the space. The member has either left explicitly by calling "@space.leave()@":/spaces/space#leave, or has abruptly disconnected and not re-established a connection within 15 seconds. - * - @remove@ := A member has been removed from the members list after the "@offlineTimeout@":/spaces/space#options period has elapsed. This enables members to appear greyed out in the avatar stack to indicate that they recently left for the period of time between their @leave@ and @remove@ events. + * `enter` + * A new member has entered the space. The member has either entered explicitly by calling [`space.enter()`](/spaces/space#enter), or has attempted to update their profile data before entering a space, which will instead emit an `enter` event. + * + * `update` + * A member has updated their profile data by calling [`space.updateProfileData()`](/spaces/space#update-profile). + * + * `leave` + * A member has left the space. The member has either left explicitly by calling [`space.leave()`](/spaces/space#leave), or has abruptly disconnected and not re-established a connection within 15 seconds. + * + * `remove` + * A member has been removed from the members list after the [`offlineTimeout`](/spaces/space#options) period has elapsed. This enables members to appear greyed out in the avatar stack to indicate that they recently left for the period of time between their `leave` and `remove` events. * * + * * * * > **Documentation source** * > * > The following documentation is copied from the Spaces documentation website. * - * h2(#foundations). Avatar stack foundations + * ## Avatar stack foundations * - * The Spaces SDK is built upon existing Ably functionality available in Ably's Core SDKs. Understanding which core features are used to provide the abstractions in the Spaces SDK enables you to manage space state and build additional functionality into your application. + * The Spaces SDK is built upon existing Ably functionality available in Ably’s Core SDKs. Understanding which core features are used to provide the abstractions in the Spaces SDK enables you to manage space state and build additional functionality into your application. + * + * Avatar stacks build upon the functionality of the Pub/Sub Channels [presence](/presence-occupancy/presence) feature. Members are entered into the presence set when they [enter the space](/spaces/space#enter). * - * Avatar stacks build upon the functionality of the Pub/Sub Channels "presence":/presence-occupancy/presence feature. Members are entered into the presence set when they "enter the space":/spaces/space#enter. * * * > **Documentation source** @@ -122,17 +135,16 @@ class Members extends EventEmitter { * > * > The following documentation is copied from the Spaces documentation website. * - * Space membership can be retrieved in one-off calls. These are local calls and retrieve the membership retained in memory by the SDK. One-off calls to retrieve membership can be used for operations such as displaying a member's own profile data to them, or retrieving a list of all other members to use to "update their profile data":/spaces/space#update-profile. + * Space membership can be retrieved in one-off calls. These are local calls and retrieve the membership retained in memory by the SDK. One-off calls to retrieve membership can be used for operations such as displaying a member’s own profile data to them, or retrieving a list of all other members to use to [update their profile data](/spaces/space#update-profile). * - * The following is an example of retrieving a member's own member object: + * The following is an example of retrieving a member’s own member object: * - * ```[javascript] + * ```javascript * const myMemberInfo = await space.members.getSelf(); * ``` + * The following is an example payload returned by `space.members.getSelf()`: * - * The following is an example payload returned by @space.members.getSelf()@: - * - * ```[json] + * ```json * { * "clientId": "clemons#142", * "connectionId": "hd9743gjDc", @@ -148,16 +160,14 @@ class Members extends EventEmitter { * } * } * ``` - * * The following is an example of retrieving an array of member objects for all members other than the member themselves. Ths includes members that have recently left the space, but have not yet been removed. * - * ```[javascript] + * ```javascript * const othersMemberInfo = await space.members.getOthers(); * ``` + * The following is an example payload returned by `space.members.getOthers()`: * - * The following is an example payload returned by @space.members.getOthers()@: - * - * ```[json] + * ```json * [ * { * "clientId": "torange#1", @@ -189,16 +199,14 @@ class Members extends EventEmitter { * } * ] * ``` - * * The following is an example of retrieving an array of all member objects, including the member themselves. Ths includes members that have recently left the space, but have not yet been removed. * - * ```[javascript] + * ```javascript * const allMembers = await space.members.getAll(); * ``` + * The following is an example payload returned by `space.members.getAll()`: * - * The following is an example payload returned by @space.members.getAll()@: - * - * ```[json] + * ```json * [ * { * "clientId": "clemons#142", @@ -300,11 +308,11 @@ class Members extends EventEmitter { * > * > The following documentation is copied from the Spaces documentation website. * - * Subscribe to members' online status and profile updates by registering a listener. Member events are emitted whenever a member "enters":/spaces/space#enter or "leaves":/spaces/space#leave the space, or updates their profile data. Use the @subscribe()@ method on the @members@ object of a space to receive updates. + * Subscribe to members’ online status and profile updates by registering a listener. Member events are emitted whenever a member [enters](/spaces/space#enter) or [leaves](/spaces/space#leave) the space, or updates their profile data. Use the `subscribe()` method on the `members` object of a space to receive updates. * * The following is an example of subscribing to the different member event types: * - * ```[javascript] + * ```javascript * // Subscribe to member enters in a space * space.members.subscribe('enter', (memberUpdate) => { * console.log(memberUpdate); @@ -325,26 +333,23 @@ class Members extends EventEmitter { * console.log(memberUpdate); * }); * ``` + * It’s also possible to subscribe to multiple event types with the same listener by using an array: * - * It's also possible to subscribe to multiple event types with the same listener by using an array: - * - * ```[javascript] + * ```javascript * space.members.subscribe(['enter', 'update'], (memberUpdate) => { * console.log(memberUpdate); * }); * ``` - * * Or subscribe to all event types: * - * ```[javascript] + * ```javascript * space.members.subscribe((memberUpdate) => { * console.log(memberUpdate); * }); * ``` + * The following is an example payload of a member event. The `lastEvent.name` describes which [event type](#events) a payload relates to. * - * The following is an example payload of a member event. The @lastEvent.name@ describes which "event type":#events a payload relates to. - * - * ```[json] + * ```json * { * "clientId": "clemons#142", * "connectionId": "hd9743gjDc", @@ -360,21 +365,26 @@ class Members extends EventEmitter { * } * } * ``` - * * The following are the properties of a member event payload: * - * |_. Property |_. Description |_. Type | - * | clientId | The "client identifier":/auth/identified-clients for the member. | String | - * | connectionId | The unique identifier of the member's "connection":/connect. | String | - * | isConnected | Whether the member is connected to Ably or not. | Boolean | - * | profileData | The optional "profile data":#profile-data associated with the member. | Object | - * | location | The current "location":/spaces/locations of the member. Will be @null@ for @enter@, @leave@ and @remove@ events. | Object | - * | lastEvent.name | The most recent event emitted by the member. | String | - * | lastEvent.timestamp | The timestamp of the most recently emitted event. | Number | + * | Property | Description | Type | + * |---------------------|-------------------------------------------------------------------------------------------------------------------|---------| + * | clientId | The [client identifier](/auth/identified-clients) for the member. | String | + * | connectionId | The unique identifier of the member’s [connection](/connect). | String | + * | isConnected | Whether the member is connected to Ably or not. | Boolean | + * | profileData | The optional [profile data](#profile-data) associated with the member. | Object | + * | location | The current [location](/spaces/locations) of the member. Will be `null` for `enter`, `leave` and `remove` events. | Object | + * | lastEvent.name | The most recent event emitted by the member. | String | + * | lastEvent.timestamp | The timestamp of the most recently emitted event. | Number | * * + * * * * > **Documentation source** @@ -473,19 +483,17 @@ class Members extends EventEmitter { * * The following is an example of removing a listener for one member event type: * - * ```[javascript] + * ```javascript * space.members.unsubscribe('enter', listener); * ``` + * It’s also possible to remove listeners for multiple member event types: * - * It's also possible to remove listeners for multiple member event types: - * - * ```[javascript] + * ```javascript * space.members.unsubscribe(['enter', 'leave'], listener); * ``` - * * Or remove all listeners: * - * ```[javascript] + * ```javascript * space.members.unsubscribe(); * ``` * diff --git a/src/Space.ts b/src/Space.ts index 4d14b3e0..d9d63ebb 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -43,16 +43,17 @@ export interface SpaceEventMap { * * The following features can be implemented within a space: * - * * "Avatar stack":/spaces/avatar - * * "Member location":/spaces/locations - * * "Live cursors":/spaces/cursors - * * "Component locking":/spaces/locking + * - [Avatar stack](/spaces/avatar) + * - [Member location](/spaces/locations) + * - [Live cursors](/spaces/cursors) + * - [Component locking](/spaces/locking) * - * The @space@ namespace consists of a state object that represents the realtime status of all members in a given virtual space. This includes a list of which members are currently online or have recently left and each member's location within the application. The position of members' cursors are excluded from the space state due to their high frequency of updates. In the beta release, which UI components members have locked are also excluded from the space state. + * The `space` namespace consists of a state object that represents the realtime status of all members in a given virtual space. This includes a list of which members are currently online or have recently left and each member’s location within the application. The position of members’ cursors are excluded from the space state due to their high frequency of updates. In the beta release, which UI components members have locked are also excluded from the space state. * - * Space state can be "subscribed":#subscribe to in the @space@ namespace. Alternatively, subscription listeners can be registered for individual features, such as avatar stack events and member location updates. These individual subscription listeners are intended to provide flexibility when implementing collaborative features. Individual listeners are client-side filtered events, so irrespective of whether you choose to subscribe to the space state or individual listeners, each event only counts as a single message. + * Space state can be [subscribed](#subscribe) to in the `space` namespace. Alternatively, subscription listeners can be registered for individual features, such as avatar stack events and member location updates. These individual subscription listeners are intended to provide flexibility when implementing collaborative features. Individual listeners are client-side filtered events, so irrespective of whether you choose to subscribe to the space state or individual listeners, each event only counts as a single message. * * To subscribe to any events in a space, you first need to create or retrieve a space. + * * * * > **Documentation source** @@ -176,17 +177,17 @@ class Space extends EventEmitter { * > * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). * - * Entering a space will register a client as a member and emit an "@enter@":/spaces/members#events event to all subscribers. Use the @enter()@ method to enter a space. + * Entering a space will register a client as a member and emit an [`enter`](/spaces/members#events) event to all subscribers. Use the `enter()` method to enter a space. * * Being entered into a space is required for members to: * - * * Update their "profile data":#update-profile. - * * Set their "location":/spaces/locations. - * * Set their "cursor position":/spaces/cursors. + * - Update their [profile data](#update-profile). + * - Set their [location](/spaces/locations). + * - Set their [cursor position](/spaces/cursors). * * The following is an example of entering a space: * - * ```[javascript] + * ```javascript * await space.enter(); * ``` * @@ -195,13 +196,13 @@ class Space extends EventEmitter { * > * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). * - * Profile data can be set when "entering":#enter a space. It is optional data that can be used to associate information with a member, such as a preferred username, or profile picture that can be subsequently displayed in their avatar. Profile data can be any arbitrary JSON-serializable object. + * Profile data can be set when [entering](#enter) a space. It is optional data that can be used to associate information with a member, such as a preferred username, or profile picture that can be subsequently displayed in their avatar. Profile data can be any arbitrary JSON-serializable object. * * Profile data is returned in the payload of all space events. * * The following is an example of setting profile data when entering a space: * - * ```[javascript] + * ```javascript * await space.enter({ * username: 'Claire Oranges', * avatar: 'https://slides-internal.com/users/coranges.png', @@ -244,20 +245,19 @@ class Space extends EventEmitter { /** * - * Profile data can be updated at any point after entering a space by calling @updateProfileData()@. This will emit an @update@ event. If a client hasn't yet entered the space, @updateProfileData()@ will instead "enter the space":#enter, with the profile data, and emit an "@enter@":/spaces/members#events event. + * Profile data can be updated at any point after entering a space by calling `updateProfileData()`. This will emit an `update` event. If a client hasn’t yet entered the space, `updateProfileData()` will instead [enter the space](#enter), with the profile data, and emit an [`enter`](/spaces/members#events) event. * * The following is an example of updating profile data: * - * ```[javascript] + * ```javascript * space.updateProfileData({ * username: 'Claire Lemons', * avatar: 'https://slides-internal.com/users/clemons.png', * }); * ``` + * A function can be passed to `updateProfileData()` in order to update a field based on the existing profile data: * - * A function can be passed to @updateProfileData()@ in order to update a field based on the existing profile data: - * - * ```[javascript] + * ```javascript * space.updateProfileData(currentProfile => { * return { ...currentProfile, username: 'Clara Lemons' } * }); @@ -312,15 +312,15 @@ class Space extends EventEmitter { /* * - * Leaving a space will emit a "@leave@":/spaces/members#events event to all subscribers. + * Leaving a space will emit a [`leave`](/spaces/members#events) event to all subscribers. * * The following is an example of explicitly leaving a space: * - * ```[javascript] + * ```javascript * await space.leave(); * ``` + * Members will implicitly leave a space after 15 seconds if they abruptly disconnect. If experiencing network disruption, and they reconnect within 15 seconds, then they will remain part of the space and no `leave` event will be emitted. * - * Members will implicitly leave a space after 15 seconds if they abruptly disconnect. If experiencing network disruption, and they reconnect within 15 seconds, then they will remain part of the space and no @leave@ event will be emitted. * * * > **Documentation source** @@ -356,11 +356,11 @@ class Space extends EventEmitter { /** * - * The current state of the space can be retrieved in a one-off call. This will return an array of all @member@ objects currently in the space. This is a local call and retrieves the membership of the space retained in memory by the SDK. + * The current state of the space can be retrieved in a one-off call. This will return an array of all `member` objects currently in the space. This is a local call and retrieves the membership of the space retained in memory by the SDK. * * The following is an example of retrieving the current space state. Ths includes members that have recently left the space, but have not yet been removed: * - * ```[javascript] + * ```javascript * const spaceState = await space.getState(); * ``` * @@ -372,34 +372,41 @@ class Space extends EventEmitter { /** * - * Subscribe to space state updates by registering a listener. Use the @subscribe()@ method on the @space@ object to receive updates. + * Subscribe to space state updates by registering a listener. Use the `subscribe()` method on the `space` object to receive updates. * * The following events will trigger a space event: * - * * A member enters the space - * * A member leaves the space - * * A member is removed from the space state "after the offlineTimeout period":#options has elapsed - * * A member updates their profile data - * * A member sets a new location + * - A member enters the space + * - A member leaves the space + * - A member is removed from the space state [after the offlineTimeout period](#options) has elapsed + * - A member updates their profile data + * - A member sets a new location * - * Space state contains a single object called @members@. Any events that trigger a change in space state will always return the current state of the space as an array of @member@ objects. + * Space state contains a single object called `members`. Any events that trigger a change in space state will always return the current state of the space as an array of `member` objects. * * * * The following is an example of subscribing to space events: * - * ```[javascript] + * ```javascript * space.subscribe('update', (spaceState) => { * console.log(spaceState.members); * }); * ``` - * * The following is an example payload of a space event. * - * ```[json] + * ```json * [ * { * "clientId": "clemons#142", @@ -432,17 +439,18 @@ class Space extends EventEmitter { * ... * ] * ``` + * The following are the properties of an individual `member` within a space event payload: + * + * | Property | Description | Type | + * |---------------------|------------------------------------------------------------------------|---------| + * | clientId | The [client identifier](/auth/identified-clients) for the member. | String | + * | connectionId | The unique identifier of the member’s [connection](/connect). | String | + * | isConnected | Whether the member is connected to Ably or not. | Boolean | + * | profileData | The optional [profile data](#profile-data) associated with the member. | Object | + * | location | The current [location](/spaces/locations) of the member. | Object | + * | lastEvent.name | The most recent event emitted by the member. | String | + * | lastEvent.timestamp | The timestamp of the most recently emitted event. | Number | * - * The following are the properties of an individual @member@ within a space event payload: - * - * |_. Property |_. Description |_. Type | - * | clientId | The "client identifier":/auth/identified-clients for the member. | String | - * | connectionId | The unique identifier of the member's "connection":/connect. | String | - * | isConnected | Whether the member is connected to Ably or not. | Boolean | - * | profileData | The optional "profile data":#profile-data associated with the member. | Object | - * | location | The current "location":/spaces/locations of the member. | Object | - * | lastEvent.name | The most recent event emitted by the member. | String | - * | lastEvent.timestamp | The timestamp of the most recently emitted event. | Number | * */ subscribe(eventOrEvents: K | K[], listener?: EventListener): void; @@ -470,7 +478,7 @@ class Space extends EventEmitter { * * The following is an example of removing a listener: * - * ```[javascript] + * ```javascript * space.unsubscribe('update', listener); * ``` * diff --git a/src/Spaces.ts b/src/Spaces.ts index 9e401858..1df3039c 100644 --- a/src/Spaces.ts +++ b/src/Spaces.ts @@ -94,18 +94,18 @@ class Spaces { * > * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). * - * A @space@ object is a reference to a single space and is uniquely identified by its unicode string name. A space is created, or an existing space is retrieved from the @spaces@ collection using the @get()@ method. + * A `space` object is a reference to a single space and is uniquely identified by its unicode string name. A space is created, or an existing space is retrieved from the `spaces` collection using the `get()` method. * * The following restrictions apply to space names: * - * * Avoid starting names with @[@ or @:@ - * * Ensure names aren't empty - * * Exclude whitespace and wildcards, such as @*@ - * * Use the correct case, whether it be uppercase or lowercase + * - Avoid starting names with `[` or `:` + * - Ensure names aren’t empty + * - Exclude whitespace and wildcards, such as `*` + * - Use the correct case, whether it be uppercase or lowercase * * The following is an example of creating a space: * - * ```[javascript] + * ```javascript * const space = await spaces.get('board-presentation'); * ``` * @@ -114,73 +114,76 @@ class Spaces { * > * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). * - * h2(#advanced). Advanced properties + * ## Advanced properties * - * The following sections are only relevant if you want to further customize a space, or understand more about the Spaces SDK. They aren't required to get up and running with the basics. + * The following sections are only relevant if you want to further customize a space, or understand more about the Spaces SDK. They aren’t required to get up and running with the basics. * - * h3(#options). Space options + * ### Space options * - * An additional set of optional properties may be passed when "creating or retrieving":#create a space to customize the behavior of different features. + * An additional set of optional properties may be passed when [creating or retrieving](#create) a space to customize the behavior of different features. * * The following properties can be customized: * - * |_. Property |_. Description |_. Type | - * | offlineTimeout | Number of milliseconds after a member loses connection or closes their browser window to wait before they are removed from the member list. The default is 120,000ms (2 minutes). | Number | - * | cursors | A "cursor options":/spaces/cursors#options object for customizing live cursor behavior. | Object | - * | cursors.outboundBatchInterval | The interval, in milliseconds, at which a batch of cursor positions are published. This is multiplied by the number of members in a space, minus 1. The default value is 100ms. | Number | - * | cursors.paginationLimit | The number of pages searched from history for the last published cursor position. The default is 5. | Number | + * | Property | Description | Type | + * |-------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------| + * | offlineTimeout | Number of milliseconds after a member loses connection or closes their browser window to wait before they are removed from the member list. The default is 120,000ms (2 minutes). | Number | + * | cursors | A [cursor options](/spaces/cursors#options) object for customizing live cursor behavior. | Object | + * | cursors.outboundBatchInterval | The interval, in milliseconds, at which a batch of cursor positions are published. This is multiplied by the number of members in a space, minus 1. The default value is 100ms. | Number | + * | cursors.paginationLimit | The number of pages searched from history for the last published cursor position. The default is 5. | Number | * - * The following is an example of customizing the space options when calling @spaces.get()@: + * The following is an example of customizing the space options when calling `spaces.get()`: * - * ```[javascript] + * ```javascript * const space = await spaces.get('board-presentation', { * offlineTimeout: 180_000, * cursors: { paginationLimit: 10 } * }); * ``` + * ### Space foundations * - * h3(#foundations). Space foundations + * The Spaces SDK is built upon existing Ably functionality available in Ably’s Core SDKs. Understanding which core features are used to provide the abstractions in the Spaces SDK enables you to manage space state and build additional functionality into your application. * - * The Spaces SDK is built upon existing Ably functionality available in Ably's Core SDKs. Understanding which core features are used to provide the abstractions in the Spaces SDK enables you to manage space state and build additional functionality into your application. + * A space is created as an Ably [channel](/channels). Members [attach](/channels#attach) to the channel and join its [presence set](/presence-occupancy/presence) when they [enter](#enter) the space. Avatar stacks, member locations and component locking are all handled on this channel. * - * A space is created as an Ably "channel":/channels. Members "attach":/channels#attach to the channel and join its "presence set":/presence-occupancy/presence when they "enter":#enter the space. Avatar stacks, member locations and component locking are all handled on this channel. - * - * To manage the state of the space, you can monitor the "state of the underlying channel":/channels#states. The channel object can be accessed through @space.channel@. + * To manage the state of the space, you can monitor the [state of the underlying channel](/channels#states). The channel object can be accessed through `space.channel`. * * The following is an example of registering a listener to wait for a channel to become attached: * - * ```[javascript] + * ```javascript * space.channel.on('attached', (stateChange) => { * console.log(stateChange) * }); * ``` - * * + * * * * > **Documentation source** * > * > The following documentation is copied from the [Spaces documentation website](https://ably.com/docs/spaces). * - * h2(#options). Cursor options + * ## Cursor options * - * Cursor options are set when creating or retrieving a @Space@ instance. They are used to control the behavior of live cursors. + * Cursor options are set when creating or retrieving a `Space` instance. They are used to control the behavior of live cursors. * * The following cursor options can be set: * - * h3(#batch). outboundBatchInterval + * ### outboundBatchInterval + * + * The `outboundBatchInterval` is the interval at which a batch of cursor positions are published, in milliseconds, for each client. This is multiplied by the number of members in a space. * - * The @outboundBatchInterval@ is the interval at which a batch of cursor positions are published, in milliseconds, for each client. This is multiplied by the number of members in a space. + * The default value is 25ms which is optimal for the majority of use cases. If you wish to optimize the interval further, then decreasing the value will improve performance by further ‘smoothing’ the movement of cursors at the cost of increasing the number of events sent. Be aware that at a certain point the rate at which a browser is able to render the changes will impact optimizations. * - * The default value is 25ms which is optimal for the majority of use cases. If you wish to optimize the interval further, then decreasing the value will improve performance by further 'smoothing' the movement of cursors at the cost of increasing the number of events sent. Be aware that at a certain point the rate at which a browser is able to render the changes will impact optimizations. + * ### paginationLimit * - * h3(#pagination). paginationLimit + * The volume of messages sent can be high when using live cursors. Because of this, the last known position of every members’ cursor is obtained from [history](/storage-history/history). The `paginationLimit` is the number of pages that should be searched to find the last position of each cursor. The default is 5. * - * The volume of messages sent can be high when using live cursors. Because of this, the last known position of every members' cursor is obtained from "history":/storage-history/history. The @paginationLimit@ is the number of pages that should be searched to find the last position of each cursor. The default is 5. * * * > **Documentation source** From 8a03caf21d106efa9119be80069a3940029eb0b1 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Tue, 26 Sep 2023 16:09:08 -0300 Subject: [PATCH 22/30] Remove scripts/convert_website_documentation.ts This reverts bff065b; the script is no longer needed after we ran it in 60d482e. --- package.json | 4 +- scripts/convert_website_documentation.ts | 183 ----------------------- 2 files changed, 2 insertions(+), 185 deletions(-) delete mode 100644 scripts/convert_website_documentation.ts diff --git a/package.json b/package.json index 623737b3..b06ddb0a 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,8 @@ "scripts": { "lint": "eslint .", "lint:fix": "eslint --fix .", - "format": "prettier --write --ignore-path .gitignore src demo scripts", - "format:check": "prettier --check --ignore-path .gitignore src demo scripts", + "format": "prettier --write --ignore-path .gitignore src demo", + "format:check": "prettier --check --ignore-path .gitignore src demo", "test": "vitest run", "test:watch": "vitest watch", "coverage": "vitest run --coverage", diff --git a/scripts/convert_website_documentation.ts b/scripts/convert_website_documentation.ts deleted file mode 100644 index a79e4807..00000000 --- a/scripts/convert_website_documentation.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { readdir, lstat, readFile, writeFile } from 'node:fs/promises'; -import { join } from 'node:path'; -import { execFile } from 'node:child_process'; - -async function processDirectory(path: string) { - const entries = await readdir(path); - - for (const entry of entries) { - const pathToEntry = join(path, entry); - - const stat = await lstat(pathToEntry); - if (stat.isDirectory()) { - await processDirectory(pathToEntry); - continue; - } - - if (entry.endsWith('.ts')) { - await processFile(pathToEntry); - } - } -} - -async function processFile(path: string) { - console.log(`Processing: ${path}`); - const contents = await readFile(path, { encoding: 'utf-8' }); - - const beginWebsiteDocumentationRegexp = //g; - const endWebsiteDocumentationRegexp = //g; - - const lines = contents.split('\n'); - - const startLineIndices: number[] = []; - const endLineIndices: number[] = []; - - for (const [lineIndex, line] of lines.entries()) { - if (beginWebsiteDocumentationRegexp.exec(line)) { - startLineIndices.push(lineIndex); - } - if (endWebsiteDocumentationRegexp.exec(line)) { - endLineIndices.push(lineIndex); - } - } - - for (let documentationIndex = 0; documentationIndex < startLineIndices.length; documentationIndex++) { - const startLineIndex = startLineIndices[documentationIndex]; - const endLineIndex = endLineIndices[documentationIndex]; - - const documentation = lines.slice(startLineIndex + 1, endLineIndex).join('\n'); - const documentationLineCount = endLineIndex - startLineIndex - 1; - - // Convert the documentation comment. - const converted = await convertWebsiteDocumentationCommentFragment(documentation); - - // Replace the documentation comment in `lines`. - const convertedLines = converted.split('\n'); - lines.splice(startLineIndex + 1, documentationLineCount, ...convertedLines); - - const addedLinesCount = convertedLines.length - documentationLineCount; - - // Shift the line indices to reflect the length of the converted documentation comment. - for ( - let indexOfDocumentationToShiftLineNumbers = documentationIndex + 1; - indexOfDocumentationToShiftLineNumbers < startLineIndices.length; - indexOfDocumentationToShiftLineNumbers++ - ) { - startLineIndices[indexOfDocumentationToShiftLineNumbers] += addedLinesCount; - endLineIndices[indexOfDocumentationToShiftLineNumbers] += addedLinesCount; - } - } - - // Write the new contents of the file. - const newContents = lines.join('\n'); - await writeFile(path, newContents, { encoding: 'utf-8' }); -} - -async function convertWebsiteDocumentationCommentFragment(commentFragment: string) { - const prefixStrippingResult = strippingPrefixOfCommentFragment(commentFragment); - const tagged = tagCommentFragmentLines(prefixStrippingResult.content); - - const lines: string[] = []; - - for (const taggedLines of tagged) { - switch (taggedLines.type) { - case 'textile': - lines.push(...(await convertTextileLines(taggedLines.lines))); - break; - case 'codeBlock': - lines.push(...convertCodeBlockLines(taggedLines.lines)); - break; - } - } - - return restoringPrefixOfCommentFragment(lines.join('\n'), prefixStrippingResult.prefix); -} - -async function convertTextileLines(textileLines: string[]) { - const pandocStdoutPromise = new Promise((resolve, reject) => { - const childProcess = execFile( - '/opt/homebrew/bin/pandoc', - // We choose gfm over commonmark for tables support. - ['--from', 'textile', '--to', 'gfm', '--wrap=preserve'], - (error, stdout, stderr) => { - if (error) { - reject(error); - } else { - resolve(stdout); - } - }, - ); - - // I don’t fully understand how writing works and whether this always succeeds in writing the full thing; keep an eye out for any weirdness and revisit if necessary. - childProcess.stdin!.write(textileLines.join('\n')); - childProcess.stdin!.end(); - }); - - const pandocStdout = await pandocStdoutPromise; - - return pandocStdout.split('\n'); -} - -function convertCodeBlockLines(codeBlockLines: string[]) { - // remove square brackets from language tag - const firstLine = codeBlockLines[0].replace(/[[\]]/g, ''); - return [firstLine, ...codeBlockLines.slice(1)]; -} - -type TaggedLines = { type: 'textile' | 'codeBlock'; lines: string[] }; - -function tagCommentFragmentLines(commentFragment: string) { - const lines = commentFragment.split('\n'); - - const result: TaggedLines[] = []; - - let current: TaggedLines | null = null; - - for (const line of lines) { - if (line.startsWith('```')) { - if (current && current.type === 'codeBlock') { - // end of code block - current.lines.push(line); - result.push(current); - current = null; - } else { - if (current) { - result.push(current); - } - - // start of code block - current = { type: 'codeBlock', lines: [line] }; - } - } else { - if (current) { - current.lines.push(line); - } else { - current = { type: 'textile', lines: [line] }; - } - } - } - - if (current) { - result.push(current); - current = null; - } - - return result; -} - -function strippingPrefixOfCommentFragment(commentFragment: string) { - const lines = commentFragment.split('\n'); - const prefix = /\s+\* /g.exec(lines[0])![0]; - const newLines = lines.map((line) => line.substring(prefix.length)); - - return { content: newLines.join('\n'), prefix }; -} - -function restoringPrefixOfCommentFragment(content: string, prefix: string) { - const lines = content.split('\n'); - const newLines = lines.map((line) => `${prefix}${line}`); - - return newLines.join('\n'); -} - -processDirectory('src'); From 2cf88baa4893fb1b6f291c992337cbc2beeaf60a Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Thu, 21 Sep 2023 17:14:55 -0300 Subject: [PATCH 23/30] Convert