Git Product home page Git Product logo

ably / spaces Goto Github PK

View Code? Open in Web Editor NEW
42.0 14.0 6.0 7.1 MB

The Spaces SDK enables you build collaborative spaces for your application and enable features like Avatar stack, Live cursors, Member location, Component locking and more.

Home Page: https://space.ably.dev/

License: Apache License 2.0

TypeScript 98.69% JavaScript 0.87% HTML 0.44%
realtime-collaboration active-users avatar-stack collaboration live-cursor presence real-time realtime websockets live-user-location

spaces's Introduction

Ably Spaces SDK

The Spaces SDK contains a purpose built set of APIs that help you build collaborative environments for your apps to quickly enable remote team collaboration. Try out a live demo of a slideshow application for an example of realtime collaboration powered by the Spaces SDK.

Example collaboration GIF

Development status CI status License

Ably is the scalable and five nines reliable middleware, used by developers at over 500 companies worldwide, to quickly build realtime applications - from collaborative experiences and live chat, to data broadcast and notifications. Supported by its globally distributed infrastructure, Ably offers 25 SDKs for various programming languages and platforms, as well as partner integrations with technologies including Datadog, Kafka, and Vercel, that accelerate the delivery of realtime experiences.


Realtime collaboration

Rather than having to coordinate resources on calls, or send documents and spreadsheets back and forth using a combination of tools, having in-app realtime collaboration features has proven to boost productivity in remote workplaces. Such features enable end users to have contextual awareness of other users within an application. This means knowing:

Who is in the application?

One of the most important aspects of collaboration is knowing who else you're working with. The most common way to display this is using an "Avatar Stack" to show who else is currently online, and those that have recently gone offline.

Where is each user within the application?

Knowing where each user is within an application helps you understand their intentions without having to explicitly ask them. For example, seeing that a colleague is currently viewing slide 2 of a presentation deck means that you can carry out your own edits to slide 3 without interfering with their work. Displaying the locations of your users can be achieved by highlighting the UI element they have selected, displaying a miniature avatar stack on the slide they are viewing, or showing the live location of their cursors. In Spaces, we call this "Member Location".

What is everyone doing in the application?

Changes to the app state made by users not only need to be synced with your backend for validation and long term storage, but also be immediately reflected in the UI so that users are always viewing the latest information. For example, in a spreadsheet application, if one user has entered a value in a cell, all other users need to see that change instantly. Pub/Sub Channels help flexibly broadcast live updates in a collaborative space.

SDK Development Status

The Spaces SDK is currently under development. If you are interested in being an early adopter and providing feedback then you can sign up for early access and are welcome to provide us with feedback.

The next section gives you an overview of how to use the SDK. Alternatively, you can jump to:

Prerequisites

To start using this SDK, you will need the following:

  • An Ably account
    • You can sign up to the generous free tier.
  • An Ably API key
    • Use the default or create a new API key in an app within your Ably account dashboard.
    • Make sure your API key has the following capabilities: publish, subscribe, presence and history.

Installation and authentication

Option 1: Using NPM

Install the Ably JavaScript SDK and the Spaces SDK:

npm install ably @ably/spaces

To instantiate the Spaces SDK, create an Ably client and pass it into the Spaces constructor:

import Spaces from '@ably/spaces';
import { Realtime } from 'ably';

const client = new Realtime.Promise({ key: "<API-key>", clientId: "<client-ID>" });
const spaces = new Spaces(client);

You can use basic authentication i.e. the API Key directly for testing purposes, however it is strongly recommended that you use token authentication in production environments.

To use Spaces you must also set a clientId so that clients are identifiable. If you are prototyping, you can use a package like nanoid to generate an ID.

Option 2: Using a CDN

You can also use Spaces with a CDN, such as unpkg:

<script src="https://cdn.ably.com/lib/ably.min-1.js"></script>
<script src="https://cdn.ably.com/spaces/0.1.2/iife/index.bundle.js"></script>

After this, instantiate the SDK in the same way as in the NPM option above:

const client = new Ably.Realtime.Promise({ key: "<API-key>", clientId: "<client-ID>" });
const spaces = new Spaces(client);

Spaces for React Developers

A set of React Hooks are available which make it seamless to use Spaces in any React application. See the React Hooks documentation for further details.

Creating a new Space

A space is the virtual area of your application where you want to enable synchronous collaboration. A space can be anything from a web page, a sheet within a spreadsheet, an individual slide in a slideshow, or the slideshow itself. A space has a participant state containing online and recently left members, their profile details, their locations and any locks they have acquired for the UI components.

Create a space and subscribe to any updates to the participant state.

// Create a new space
const space = await spaces.get('demoSlideshow');

// Subscribe to space state events
space.subscribe('update', (spaceState) => {
  console.log(spaceState.members);
});

// Enter a space, publishing an update event, including optional profile data
await space.enter({
  username: 'Claire Lemons',
  avatar: 'https://slides-internal.com/users/clemons.png',
});

The following is an example event payload received by subscribers when a user enters a space:

[
  {
    "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"
    }
  }
]

Space members

The members namespace contains methods dedicated to building avatar stacks. Subscribe to members entering, leaving, being removed from the Space (after a timeout) or updating their profile information.

// Subscribe to all member events in a space
space.members.subscribe((memberUpdate) => {
  console.log(memberUpdate);
});

// Subscribe to member enter events only
space.members.subscribe('enter', (memberJoined) => {
  console.log(memberJoined);
});

// Subscribe to member leave events only
space.members.subscribe('leave', (memberLeft) => {
  console.log(memberLeft);
});

// Subscribe to member remove events only
space.members.subscribe('remove', (memberRemoved) => {
  console.log(memberRemoved);
});

// Subscribe to profile updates on members only
space.members.subscribe('updateProfile', (memberProfileUpdated) => {
  console.log(memberProfileUpdated);
});

// Subscribe to all updates to members
space.members.subscribe('update', (memberUpdate) => {
  console.log(memberUpdate);
});

The following is an example event payload received by subscribers when member updates occur in a space:

{
  "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"
  }
}

Getting a snapshot of space members

Space members has methods to get the current snapshot of member state:

// Get all members in a space
const allMembers = await space.members.getAll();

// Get your own member object
const myMemberInfo = await space.members.getSelf();

// Get everyone else's member object but yourself
const othersMemberInfo = await space.members.getOthers();

Member locations

The locations namespace contains methods dedicated to building member locations, enabling you to track where users are within an application. A location could be a form field, multiple cells in a spreadsheet or a slide in a slide deck editor.

// You need to enter a space before publishing your location
await space.enter({
  username: 'Claire Lemons',
  avatar: 'https://slides-internal.com/users/clemons.png',
});

// Publish your location based on the UI element selected
await space.locations.set({ slide: '3', component: 'slide-title' });

// Subscribe to location events from all members in a space
space.locations.subscribe('update', (locationUpdate) => {
  console.log(locationUpdate);
});

The following is an example event payload received by subscribers when a member changes location:

{
  "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": 1
    }
  },
  "previousLocation": {
    "slide": "2",
    "component": null
  },
  "currentLocation": {
    "slide": "3",
    "component": "slide-title"
  }
}

Getting a snapshot of member locations

Member locations has methods to get the current snapshot of member state:

// Get a snapshot of all the member locations
const allLocations = await space.locations.getAll();

// Get a snapshot of my location
const myLocation = await space.locations.getSelf();

// Get a snapshot of everyone else's locations
const othersLocations = await space.locations.getOthers();

Live cursors

The cursors namespace contains methods dedicated to building live cursors, allowing you to track a member's pointer position updates across an application. Events can also include associated data, such as pointer attributes and the IDs of associated UI elements:

// You need to enter a space before publishing your cursor updates
await space.enter({
  username: 'Claire Lemons',
  avatar: 'https://slides-internal.com/users/clemons.png',
});

// Subscribe to events published on "mousemove" by all members
space.cursors.subscribe('update', (cursorUpdate) => {
  console.log(cursorUpdate);
});

// Publish a your cursor position on "mousemove" including optional data
window.addEventListener('mousemove', ({ clientX, clientY }) => {
  space.cursors.set({ position: { x: clientX, y: clientY }, data: { color: 'red' } });
});

The following is an example event payload received by subscribers when a member moves their cursor:

{
  "connectionId": "hd9743gjDc",
  "clientId": "clemons#142",
  "position": { "x": 864, "y": 32 },
  "data": { "color": "red" }
}

Getting a snapshot of member cursors

Member cursors has methods to get the current snapshot of member state:

// Get a snapshot of all the cursors
const allCursors = await space.cursors.getAll();

// Get a snapshot of my cursor
const myCursor = await space.cursors.getSelf();

// Get a snapshot of everyone else's cursors
const othersCursors = await space.cursors.getOthers();

Component Locking

Use the Component Locking API to lock stateful components whilst being edited by members to reduce the chances of conflicting changes being made.

Locks are identified using a unique string, and the Spaces SDK maintains that at most one member holds a lock with a given string at any given time.

The Component Locking API supports four operations: Query, Acquire, Release, and Subscribe.

Query

space.locks.get is used to query whether a lock identifier is currently locked and by whom. It returns a Lock type which has the following fields:

type Lock = {
  id: string;
  status: LockStatus;
  member: SpaceMember;
  timestamp: number;
  attributes?: LockAttributes;
  reason?: Types.ErrorInfo;
};

For example:

// check if the id is locked
const isLocked = space.locks.get(id) !== undefined;

// check which member has the lock
const { member } = space.locks.get(id);

// check the lock attributes assigned by the member holding the lock
const { attributes } = space.locks.get(id);
const value = attributes.get(key);

space.locks.getAll returns all lock identifiers which are currently locked as an array of Lock:

const allLocks = space.locks.getAll();

for (const lock of allLocks) {
  // ...
}

Acquire

space.locks.acquire sends a request to acquire a lock using presence.

It returns a Promise which resolves once the presence request has been sent.

const req = await space.locks.acquire(id);

// or with some attributes
const attributes = new Map();
attributes.set('key', 'value');
const req = await space.locks.acquire(id, { attributes });

It throws an error if a lock request already exists for the given identifier with a status of pending or locked.

Release

space.locks.release releases a previously requested lock by removing it from presence.

It returns a Promise which resolves once the presence request has been sent.

await space.locks.release(id);

Subscribe

space.locks.subscribe subscribes to changes in lock status across all members.

The callback is called with a value of type Lock.

space.locks.subscribe('update', (lock) => {
  // lock.member is the requesting member
  // lock.request is the request made by the member
});

// or with destructuring:
space.locks.subscribe('update', ({ member, request }) => {
  // request.status is the status of the request, one of PENDING, LOCKED, or UNLOCKED
  // request.reason is an ErrorInfo if the status is UNLOCKED
});

Such changes occur when:

  • a pending request transitions to locked because the requesting member now holds the lock
  • a pending request transitions to unlocked because the requesting member does not hold the lock since another member already does
  • a locked request transitions to unlocked because the lock was either released or invalidated by a concurrent request which took precedence
  • an unlocked request transitions to locked because the requesting member reacquired a lock

spaces's People

Contributors

dependabot[bot] avatar dpiatek avatar ferdipret avatar lawrence-forooghian avatar lmars avatar m-hulbert avatar mymmij avatar owenpearson avatar snikidev avatar srushtika avatar surminus avatar tomczoink avatar ttypic avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

spaces's Issues

Feature request for React Hooks: Handle Spaces initialization in SpacesProvider

When the <SpacesProvider> React component is used, most of the time, the spaces object needs to be created first.

From the documentation:

const ably = new Realtime.Promise({ key: "VS4Rww.ijkxYg:*****", clientId: 'clemons' });
const spaces = new Spaces(ably);

root.render(
  <SpacesProvider client={spaces}>
    <SpaceProvider name="my-space">
      <App />
    </SpaceProvider>
  </SpacesProvider>
)

It would be nice if, alternatively to the spaces object, the key and the clientId could be passed to the SpacesProvider and the SpacesProvider cares about initializing Ably and Spaces:

  <SpacesProvider key="VS4Rww.ijkxYg:*****" clientId="clemons">
    <SpaceProvider name="my-space">
      <App />
    </SpaceProvider>
  </SpacesProvider>

Or alternatively have an object, which is passed to Realtime.Promise().

This would save some boilerplate code and also would make sure that implementers don't have to remember to put the Spaces initialization into a useMemo()/ useEffect() hook ;)

Feature Request: `autoEnter` for SpaceProvider

Would it be possible to optionally enter spaces automatically? Personally, I see more use cases where I immediately want to enter a space than not.

Suggestion:

  <SpacesProvider client={spaces}>
    <SpaceProvider name="my-space" autoEnter={true} user={{ name: 'Luigi'}}>
      <App />
    </SpaceProvider>
  </SpacesProvider>

Right now we're solving this in a "hacky" way so we don't have to do it in each component separately:

<SpacesProvider client={spaces}>
  <SpaceProvider name="my-space">
    <EnterSpace user={user}>
      {children}
    </EnterSpace>
  </SpaceProvider>
</SpacesProvider>

...

const EnterSpace = ({ children, user }) => {
  const { space } = useSpace();
  const [spaceEntered, setSpaceEntered] = useState(false);
  useEffect(() => {
    const enterSpace = async () => {
      await space.enter(user);
      setSpaceEntered(true);
    };
    const leaveSpace = async () => {
      await space.leave();
    };
    if (space) {
      enterSpace();
    }
    return () => {
      if (space) {
        leaveSpace();
      }
    };
  }, [space, user]);

  if (!spaceEntered) {
    return <LoadingIndicator />
  }

  return <>{children}</>;
};

Or is there something we're missing?

Live Cursors Interpolation

We're using the ably live cursors (via spaces) for a collaboration feature in our product. When clients are using different sized screens the cursors are not being interpolated correctly hence they show up in the wrong position on clients. I've attempted to solve this by converting the (x,y) position from pixels to a percentage of the canvas but it is still wrong for the horizontal X axis.

percentage = (position / canvasLen) * 100

Thew above formula is applied to the X and Y axis. I've also tried to scale the pixel positions like below (which also doesn't work):

    function transformCoordinates(
        client1_x: number,
        client1_y: number,
        client1_width: number,
        client1_height: number,
        client2_width: number,
        client2_height: number
    ): [number, number] {
        // Calculate the scaling factors for width and height dynamically based on the x-coordinate
        const scale_x: number = client2_width / client1_width;
        const scale_y: number = client2_height / client1_height;

        // Adjust the scaling factor based on the x-coordinate (you can customize this adjustment as needed)
        const adjusted_scale_x: number = scale_x * (client1_x / client1_width);

        // Apply the adjusted scaling factor to the x-coordinate
        const client2_x: number = Math.floor(client1_x * adjusted_scale_x);
        const client2_y: number = Math.floor(client1_y * scale_y);

        return [client2_x, client2_y];
    }

Please share any examples or cases of how to do this using Ably.

Handling creating multiple spaces within same component/page (React)

I am currently using the spaces SDK and I'm running into some issues with using the SpaceProvider for a specific feature for my teams application. Within in a page (or dialog in our case) we are interacting with a 3d viewing tool to view design files. For each file we would like to have a collaborative "space" using your product. Within the page we have an ability to switch between design files/documents that you can view using the 3d viewing tool. Ideally each design file/document has a space instead of one large one which could create confusion with loads of members potentially viewing several different documents. Using the SpaceProvider opens up for the use of useSpace() which is extremely handy which also seems to open up the useMembers() hook (these are very helpful hooks). However these seem to only be accessible when wrapping components with the SpaceProvider. I am encountering a few issues with the SpaceProvider.

  1. SpaceProvider is manually creating a space as soon as I open up the dialog, which isn't the greatest considering it is a choice for a user to enter a space and work collaboratively or simply just individually use the viewer.

  2. Secondly with the idea of each document having its own space every time the user chooses to switch within the viewer I'd expect the SpaceProvider which has its name field updating based on the document to update correctly (this is not the case, it is staying as the first space created).

user opens the dialog
const {space} = useSpace();
Space {any: Array(0), events: {…}, anyOnce: Array(0), eventsOnce: {…}, presenceUpdate: ƒ, …}

const {self} = useMembers()
null
^Showcasing the user is not in the space due to the fact that they have not decided to enter it, yet the space was created anyways
useEffect to update name
useEffect(() => {
        if (selectedDocument) {
            setName(`cad-spin-${selectedDocument.id}`);
        } else {
            setName("");
        }
}, [selectedDocument]);

Space provider to wrap the dialog/page
<SpacesProvider client={spaces}>
            <SpaceProvider name={name} key={name}>
                {children}
            </SpaceProvider>
</SpacesProvider>

What instead is required to happen is for the dialog to re-render (im using the key to do this), which in turn causes this flickering when switching between documents (not ideal). This is also now directly coupled with the space being created regardless of the users decision to actually interact with spaces, therefore causing a flicker when just individually using the viewer (not inside a space).

Our ideal and dream solution would be to enter the viewer, users chooses to use spaces, they create or get the space for that document, they then enter it and see all members within that space. User then chooses to switch documents, same sequence applies. Eliminate the re-rendering, allowing for a smooth and UX appropriate flow for the user (ideally allowing us to use you hooks you provide).

How exactly can I get around this? I've noticed it isn't necessary to use the SpaceProvider and gain access to your hooks, but those are very useful, leading to a potential recreation of those hooks to suit my specific use case.

A loom to showcase the problem in more detail.
https://www.loom.com/share/bf4dbf051b5447bdbf8e9a507e620174?sid=ae4f7ba5-b063-4105-8beb-da2eb122f873

Feature Request: Takeover/force lock

In time-sensitive situations where every second counts, we need to be able to take over a lock, even though a resource is already locked by someone else. Is that something you could support? E.g.

await space.locks.acquire('my-resource', { force: true });

A use case is an article on a news site where the situation changes rapidly. If some editor is blissfully unaware and typing away on something and the editor-in-chief doesn't have time to call the currently editing editor or find him, they should be able to force-lock the article (effectively kicking out the previous editor) to update the content.

Not able to initialise the spaces on nextjs, react

I installed the spaces sdk, and stumbled upon this error. Can anyone help me resolve this?

Let me know If I made any mistake when initialising the space.

image image image

I'm trying to use the spaces sdk, but it looks I'm not able to get this working. Using the latest version of nextjs, react, antd version 5.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.