diff --git a/docs/usage.md b/docs/usage.md
index d1fe7677..b8ead65a 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -1,117 +1,188 @@
-# In-depth usage
+# Usage
-- [Authenticate and instantiate](#authenticate-and-instantiate)
-- [Create a space](#create-a-space)
-- [Subscribe to member updates](#subscribe-to-member-updates)
- - [Request member updates](#request-member-updates)
-- [Enter a space](#enter-a-space)
- - [Leave a space](#leave-a-space)
-- [Subscribe to location updates](#subscribe-to-location-updates)
- - [Track an individual location or user](#track-an-individual-location-or-user)
-- [Set a location](#set-a-location)
-- [Subscribe to cursor updates](#subscribe-to-cursor-updates)
- - [Request cursor locations](#request-cursor-locations)
-- [Set a cursor location](#set-a-cursor-location)
+## Prerequisites
-## Authenticate and instantiate
+### Ably API key
-Install the Ably JavaScript SDK and the Collaborative Spaces SDK:
+To use Spaces, you will need the following:
+
+- An Ably account. You can [sign up](https://ably.com/signup) for free.
+- An Ably API key. You can create API keys in an app within your [Ably account](https://ably.com/dashboard).
+ - The API key needs the following [capabilities](https://ably.com/docs/realtime/authentication#capabilities-explained): `publish`, `subscribe`, `presence` and `history`.
+
+### Environment
+
+Spaces is built on top of the [Ably JavaScript SDK](https://github.com/ably/ably-js). Although the SDK supports Node.js and other JavaScript environments, at the time of writing our main target is an ES6 compatible browser environment.
+
+## Installation
+
+### NPM
+
+```sh
+npm install @ably-labs/spaces
+```
+
+If you need the Ably client (see [Authentication & instantiation](#authentication-and-instantiation))
```sh
npm install ably
-npm install ably-labs/spaces
```
-Import the SDKs and then instantiate the Collaborative Spaces SDK with your Ably API key:
+### CDN
+
+You can also use Spaces with a CDN like [unpkg](https://www.unpkg.com/):
+
+```html
+
+
+```
+
+Note that when you use a CDN, you need to include Ably Client as well, the Spaces bundle does not include it.
+
+## Authentication and instantiation
+
+Spaces use an [Ably promise-based realtime client](https://github.com/ably/ably-js#using-the-async-api-style). You can either pass an existing client to Spaces or pass the [client options](https://ably.com/docs/api/realtime-sdk?lang=javascript#client-options) directly to the spaces constructor.
+
+To instantiate with options, you will need at minimum an [Ably API key](#ably-api-key) and a [clientId](https://ably.com/docs/auth/identified-clients?lang=javascripts). A clientId represents an identity of an connection. In most cases this will something like the id of a user:
+
+```ts
+import Spaces from '@ably-labs/spaces';
+
+const spaces = new Spaces({ key: ABLY_API_KEY, clientId: 'someClientId' });
+```
+
+If you already have an ably client in your application, you can just pass it directly to Spaces (the client will still need to instatiated with a clientId):
```ts
import { Realtime } from 'ably/promise';
import Spaces from '@ably-labs/spaces';
-const spaces = new Spaces(ABLY_API_KEY);
+const client = new Realtime.Promise(options);
+const spaces = new Spaces(client);
```
-In the above example, the client can be accessed using `spaces.ably` to use functionality in the Ably JavaScript SDK.
+In both scenarios, you can access the client via `spaces.ably`.
+
+To learn more about authenticating with ably, see our [authentication documentation](https://ably.com/docs/auth).
## Create a space
A space is the virtual area of an application you want to collaborate in, such as a web page, or slideshow. A `space` is uniquely identified by its name. A space is created, or an existing space retrieved from the `spaces` collection by calling the `get()` method. You can only connect to one space in a single operation. The following is an example of creating a space called "demonSlideshow":
```ts
-const space = spaces.get('demoSlideshow');
+const space = await spaces.get('demoSlideshow');
```
-A set of `spaceOptions` can be passed to space when creating or retrieving it. `spaceOptions` enable additional properties to be set for the space. The following properties can be set:
+### Options
-| Property | Description | Type |
-| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ |
-| offlineTimeout | The time in milliseconds before a member is removed from a space after they have disconnected. The default is 120,000 (2 minutes). | number |
-| outboundBatchInterval | 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 100. | number |
-| paginationLimit | The number of pages searched from [history](https://ably.com/docs/realtime/history) for the last published cursor position. The default is 5. | number |
-
-The following is an example of setting `offlineTimeout` to 3 minutes when creating a space:
-
-```ts
-const space = spaces.get('demoSlideshow', { offlineTimeout: 180_000 });
-```
+A set of `spaceOptions` can be passed to space when creating or retrieving it. The following properties can be set:
-## Subscribe to member updates
+| Property | Description | Type |
+| -------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ------ |
+| offlineTimeout | The time in milliseconds before a member is removed from a space after they have disconnected. The default is 120000ms (2 minutes). | number |
+| cursors | Options relating to configuring the cursors API (see below) | object |
-Subscribe to `membersUpdate` events in order to display which users are present in a space, such as in an avatar stack. The `membersUpdate` events for a space notify subscribers when clients join and leave it, or when a user's location changes. Use the `space.on()` method to register a listener for `membersUpdate` events.
+| Property | Description | Type |
+| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------ |
+| outboundBatchInterval | 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 100ms. | number |
+| paginationLimit | The number of pages searched from [history](https://ably.com/docs/realtime/history) for the last published cursor position. The default is 5. | number |
-The following is an example of subscribing to updates for a space:
+The following is an example of setting `offlineTimeout` to 3 minutes and a `paginationLimit` of 10:
```ts
-space.on('membersUpdate', (members) => {
- console.log(members);
-});
+const space = await spaces.get('demoSlideshow', { offlineTimeout: 180_000, cursors: { paginationLimit: 10 } });
```
-The following is an example `membersUpdate` event received by subscribers when a user enters a space:
+## Members
-```json
-[
- {
- "clientId": "clemons#142",
- "isConnected": true,
- "lastEvent": {
- "name": "enter",
- "timestamp": 1677595689759
- },
- "location": null,
- "profileData": {
- "username": "Claire Lemons",
- "avatar": "https://slides-internal.com/users/clemons.png"
- }
+Members is a core concept of the library. When you enter a space, you become a `member`. On the client, your own membership is to referred to as `self`. You can get your `self` by calling `space.getSelf`. To get all the members (including self), call `space.getMembers`. These method will return (respectively an object and array of):
+
+```js
+{
+ "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 `spaceMember` event:
+#### clientId
+
+The client identifier for the user, provided to the ably client instance.
+
+_Type: string_
+
+#### connectionId
+
+Identifier for the connection used by the user. This is a unique identifier.
+
+_Type: string_
+
+#### isConnected
+
+Whether the user is connected to Ably or not.
+
+_Type: boolean_
+
+#### lastEvent
+
+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`.
-| Property | Description | Type |
-| ----------- | ------------------------------------------------------------------------------------------------------------- | --------------------------------- |
-| clientId | The client identifier for the user. | string |
-| isConnected | Whether the user is connected to Ably or not. | boolean |
-| lastEvent | The most recent event emitted by the user and its timestamp. Events will be either `enter` or `leave`. | {name: string, timestamp: number} |
-| location | The current location of the user within the space. | any |
-| profileData | Optional user data that can be attached to a user, such as a username or image to display in an avatar stack. | object |
+_Type: { name: string; timestamp: number }_
-To stop subscribing to member events, users can call the `space.off()` method.
+#### location
-### Request member updates
+The current location of the user within the space.
-In addition to subscribing to events to see who joins and leaves a space, it is also possible to query the membership of a space in a one-off request using `getMembers()`. This returns an array of `spaceMember` objects.
+_Type: string | object | null_
-The `getSelf()` method can be used to return the `spaceMember` object for the local client.
+#### profileData
-## Enter a space
+Optional user data that can be attached to a user, such as a username or image to display in an avatar stack.
-When a user enters a space they should call `enter()`. This publishes an event to all subscribers of that space.
+_Type: object | null_
-`space.enter()` can take an optional object called `profileData` so that users can include convenient metadata to update an avatar stack, such as a username and profile picture.
+### Listen to members updates
-The following is an example of entering a space with `profileData`:
+The `space` instance is an `EventEmitter`. Events will be emitted for updates to members (includeing self). You can listen to the following events:
+
+#### enter
+
+Emitted when a member enters a space. Called with the member entering the space.
+
+#### leave
+
+Emitted when a member leaves a space. Called with the member leaving the space.
+
+#### membersUpdate
+
+Emitted when members enter, leave and their location is updated. Called with an array of all the members in the space.
+
+```ts
+space.on('membersUpdate', (members) => {
+ console.log(members);
+});
+```
+
+To stop listening to member events, users can call the `space.off()` method. See [Event emitters](#event-emitters) for options and usage.
+
+### Enter a space
+
+To become a member of space (and use the other APIs, like location or cursors) a client needs to enter a space.
+
+```ts
+space.enter();
+```
+
+This method can take an optional object called `profileData` so that users can include convenient metadata to update an avatar stack, such as a username and profile picture.
```ts
space.enter({
@@ -126,26 +197,48 @@ A leave event is sent when a user leaves a space. This can occur for one of the
- `space.leave()` is called explicitly.
- The user closes the tab.
-- The user is abruptly disconnected from the internet for longer than 2 minutes.
- - Note that the time before they are seen has having left the space is configurable using [`offlineTimeout`](#create-a-space).
+- The user is abruptly disconnected from the internet for longer than 2 minutes
+
+A leave event does not remove the member immediately from members. Instead, they are removed after a timeout which is configurable by the [`offlineTimeout` option](#options). This allows the UI to display an intermediate state before disconnection/reconnection.
+
+As with `enter`, you can update the `profileData` on leave:
+
+```ts
+space.leave({
+ username: 'Claire Lemons',
+ avatar: 'https://slides-internal.com/users/inactive.png',
+});
+```
-## Subscribe to location updates
+## Location
-Subscribe to `locationUpdate` events in order to display where users are within a space, such as which slide number they are currently viewing, or which cell or component they have selected. `locationUpdate` events are sent when a user calls [`locations.set()`](#set-a-location) to update their location when they change position, or select a new UI element. Use the `locations.on()` method to register a listener for `locationUpdates` events.
+Each member can set a location for themselves:
-Locations are defined by you, so that they are most relevant to the application. For example, it could be only the ID of an HTML element, or a map describing a slide number and slide element.
+```ts
+space.locations.set({ slide: '3', component: 'slide-title' });
+```
-Note that updates to user locations are also received in [`memberUpdates`](#subscribe-to-member-updates) events. This can be useful for managing and reacting to local app state changes, whereas it is often simpler to listen to individual events to update UI elements.
+A location does not have a prescribed shape. In your UI it can represent a single location (an id of a field in form), multiple locations (id's of multiple cells in a spredsheet) or a hierarchy (a field in one of the multiple forms visible on screen).
-The following is an example of subscribing to location updates for a space:
+The location property will be set on the [member](#members).
+
+Because locations are part of members, a `memberUpdate` event will be emitted when a member updates their location. When a member leaves, their location is set to `null`.
```ts
-space.locations.on('locationUpdate', (update) => {
- console.log(update);
+space.on('membersUpdate', (members) => {
+ console.log(members);
});
```
-The following is an example `locationUpdate` event received by subscribers when a user changes location:
+However, it's possible to listen just location updates. `locations` is an [event emitter](#event-emitters) and will emith the `locationUpdate` event:
+
+```ts
+space.locations.on('locationUpdate', (locationUpdate) => {
+ console.log(locationUpdate);
+});
+```
+
+This event will include the member affected by the change, as well as their previous and current locations:
```json
{
@@ -177,79 +270,92 @@ The following is an example `locationUpdate` event received by subscribers when
}
```
-The following are the properties of a `locationUpdate` event:
-
-| Property | Description | Type |
-| ---------------- | --------------------------------------------------- | --------------------------------------------- |
-| member | The details of the user that has changed location. | [`spaceMember`](#subscribe-to-member-updates) |
-| previousLocation | The previous location of the user within the space. | any |
-| currentLocation | The current location of the user within the space. | any |
+### Track an individual location or member
-To stop subscribing to location updates, users can call the `locations.off()` method.
+To listen only to events for a specific member or location, we can use `createTracker`. This method filters messages based on a user provided predicate.
-### Track an individual location or user
+_Note that this is client-side filtering only, messages are still sent/received for updates._
-It is also possible to track a specific location or user, rather than subscribing to all events using `createTracker()`. All events will still be streamed to the client, however they will be filtered client-side.
-
-The following is an example of creating a tracker for a specific user based on their `clientId`, and then subscribing to updates for only that user:
+Create a tracker for a specific member based on their `clientId`, and then listen to updates for only that user:
```ts
-const memberTracker = space.locations.createTracker((change) => change.member.clientId === 'clemons#142');
+const memberTracker = space.locations.createTracker(
+ (locationUpdate) => locationUpdate.member.clientId === 'clemons#142',
+);
-memberTracker.on((change) => {
- console.log(change);
+memberTracker.on((locationUpdate) => {
+ console.log(locationUpdate);
});
```
-The following is an example of creating a tracker for a specific location, such as a single UI element, and then subscribing to updates for only that location:
+Create a tracker for a specific location, such as a single UI element, and listen to updates:
```ts
-const locationTracker = space.locations.createTracker((change) => change.previousLocation === 'slide-title');
+const locationTracker = space.locations.createTracker(
+ (locationUpdate) =>
+ locationUpdate.previousLocation === 'slide-title' || locationUpdate.currentLocation === 'slide-title',
+);
-locationTracker.on((change) => {
- console.log(change);
+locationTracker.on((locationUpdate) => {
+ console.log(locationUpdate);
});
```
-## Set a location
+## Live Cursors
+
+A common feature of collaborative apps is to show where a users cursors is positioned in realtime. It's easy to accomplish this with `cursors` API.
-Users call `locations.set()` to publish a `locationUpdate` that will be received by all users subscribed to location updates. This should be called to update their location when they change position, or select a new UI element.
+Unlike a location, you can have multiple cursors. This can be used to represent multiple devices interacting with the UI or different ways of interacting with the browser (like scrolling).
-The following is an example of setting a location update:
+The most common use case is however to show the current mouse/touchpad position.
+
+To get started, you'll need to get a named cursor instance:
```ts
-space.locations.set({ slide: '3', component: 'slide-title' });
+const mouseMove = space.cursors.get('mouseMove');
```
-## Subscribe to cursor updates
+This instance can emit events for [`self`](#members) and listen to all positions emitted for the given named cursor (`mouse`), for all members (including self).
-Subscribe to `cursorUpdate` events in order to display the location of user cursors within a space as they move. Use the `space.cursors.on()` method to register a listener for `cursorUpdate` events.
+```ts
+window.addEventListner('mousemove', ({ clientX, clientY }) => {
+ mouseMove.set({ position: { x: clientX, y: clientY } });
+});
+```
+
+`set` takes 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 property, `data` can 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
+window.addEventListner('mousemove', ({ clientX, clientY }) => {
+ mouseMove.set({ position: { x: clientX, y: clientY }, data: '' });
+});
+```
-The following is an example of subscribing to all cursor updates in a space:
+The cursor instance is an [event emitter](#event-emitters):
```ts
-space.cursors.on((event) => {
- console.log(event);
+mouseMove.on('cursorUpdate', (cursorUpdate) => {
+ console.log(cursorUpdate);
});
```
-The following is an example of subscribing to events for only a specific cursor in a space:
+As is the `cursors` namespace itself, emitting events for all named cursors:
```ts
-space.cursors.get('user-a-cursor').on((event) => {
- console.log(event);
+space.cursors.on('cursorsUpdate', (cursorUpdate) => {
+ console.log(cursorUpdate);
});
```
-The following is an example `cursorUpdate` event received by subscribers when a cursor changes position:
+The following is an `cursorUpdate` event received by listeners when a cursor sets their position or data:
```json
{
- "name": "user-a-cursor",
+ "name": "mousemove",
"connectionId": "hd9743gjDc",
- "clientId": "clemons@slides.com",
+ "clientId": "clemons#142",
"position": { "x": 864, "y": 32 },
- "cursorData": { "color": "red" }
+ "data": { "color": "red" }
}
```
@@ -263,66 +369,115 @@ The following are the properties of a `cursorUpdate` event:
| position | The x and y coordinates of the cursor. | cursorPosition |
| data | Optional additional information about a cursor, such as its color. | cursorData |
-### Request cursor locations
+`cursorPosition`
+
+| Property | Description | Type |
+| -------- | ------------ | ------ |
+| x | x coordinate | number |
+| y | y coordinate | number |
+
+### Inital cursor position and data
-To retrieve the initial location of all cursors within a space, you can use the `cursors.getAll()` method. This returns a `cursorsUpdate` which contains a list of cursors by `connectionId`.
+To retrieve the initial position & data of all cursors within a space, you can use the `cursors.getAll()` method. This returns an object keyed by `connectionId` and cursor name. The value is the last `cursorUpdate` set by the given `connectionId`.
-The following is an example of calling `getAll()` to return all cursor positions:
+Example of calling `getAll` to return all cursor positions:
```ts
-const allCursors = space.cursors.getAll();
+const allCursors = await space.cursors.getAll();
```
-The following is an example `cursorsUpdate` received when calling `getAll()`:
-
-```json
+```ts
{
- "": {
- "pointer": {
- "name": "pointer",
- "connectionId": "someConnectionId",
- "clientId": "someClientId",
+ "hd9743gjDc": {
+ "mousemove": {
+ "name": "mousemove",
+ "connectionId": "hd9743gjDc",
+ "clientId": "clemons#142",
"position": {
"x": 864,
"y": 32
},
- "cursorData": {
+ "data": {
"color": "red"
}
}
- },
- "": {
- "pointer": null
}
}
```
-The following optional properties can be passed to `getAll()`:
+Example of calling `getAll` to get the last positions for the named cursor `mousemove`:
+
+```ts
+const allCursors = await space.cursors.getAll('mousemove');
+```
+
+```ts
+{
+ "hd9743gjDc": {
+ "name": "mousemove",
+ "connectionId": "hd9743gjDc",
+ "clientId": "clemons#142",
+ "position": {
+ "x": 864,
+ "y": 32
+ },
+ "data": {
+ "color": "red"
+ }
+ }
+}
+```
| Property | Description | Type |
| -------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------ |
| name | The name of a cursor to retrieve the position for. This name is set by a user when they [create a cursor instance](#set-a-cursor-position). | string |
-## Set a cursor location
+## Event Emitters
-Users create a cursor instance using the `space.cursors.get()` method and then use the `set()` method to publish a `cursorUpdate` event when their cursor moves.
+All Spaces APIs inherit from an [EventEmitter class](/src/utilities/EventEmitter.ts) and support the same event API.
-The following is an example of creating a cursor instance and updating its location:
+Calling `on` with a single function argument will subscribe to all events on that emitter.
```ts
-const pointer = space.cursors.get('user-a-cursor');
+space.on(() => {});
+```
-pointer.set({ position: { x: clientX, y: clientY } });
+Calling `on` with a named event & a function argument will subscribe to that event only.
+
+```ts
+space.on(`membersUpdate`, () => {});
```
-The following optional properties can be passed to `set()`:
+Calling `on` with an array of named events & a function argument will subscribe to those events.
-| Property | Description | Type |
-| -------- | ---------------------------- | ---- |
-| data | Optional cursor information. | |
+```ts
+space.on([`membersUpdate`], () => {});
+```
-The following is an example of passing `data` to the `set()` property to set the cursor color for a user:
+Calling `off` with no argumnets will remove all registered listeners.
```ts
-pointer.set({ position: { x: clientX, y: clientY }, data: { color: 'red' } });
+space.off();
```
+
+Calling `off` with a single named event will remove all listeners registered for that event.
+
+```ts
+space.off(`membersUpdate`);
+```
+
+Calling `off` with an array of named events will remove all listeners registered for those events.
+
+```ts
+space.off([`membersUpdate`]);
+```
+
+Calling `off` and adding a listener function as the second argument to both of the above will remove only that listener.
+
+```ts
+const listener = () => {};
+space.off(`membersUpdate`, listener);
+space.off([`membersUpdate`], listener);
+```
+
+As with the native DOM api, this only works if the listener is the same reference as the one passed to `on`.