Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

docs: Add Component Locking section to README #112

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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,3 +314,103 @@ 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:

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

For example:

```ts
// 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`:

```ts
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.

```ts
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.

```ts
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`.

```ts
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
166 changes: 160 additions & 6 deletions docs/class-definitions.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,13 +168,15 @@ Handles members within a space.

Listen to member events for the space. See [EventEmitter](/docs/usage.md#event-emitters) for overloaded usage.

```ts
space.members.subscribe((member: SpaceMember) => {});
```
The argument supplied to the callback is the [SpaceMember](#spacemember) object representing the member that triggered the event.

Example:

The argument supplied to the callback is the [SpaceMember](#spacemember) object representing the member that triggered the event.
```ts
space.members.subscribe((member: SpaceMember) => {});
```

Available events:
Available events:

- ##### **enter**

Expand Down Expand Up @@ -449,7 +451,7 @@ Set the position of a cursor. If a member has not yet entered the space, this me
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 })
type set = (update: { position: CursorPosition, data?: CursorData }) => void;
```

Example usage:
Expand Down Expand Up @@ -544,3 +546,155 @@ Represent data that can be associated with a cursor update.
```ts
type CursorData = Record<string, unknown>;
```

## Component Locking

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).

### Methods

#### acquire()

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<Lock>;
```

Example:

```ts
const id = "/slide/1/element/3";
const lockRequest = await space.locks.acquire(id);
```

#### release()

Releases a previously requested lock.

```ts
type release = (lockId: string) => Promise<void>;
```

Example:

```ts
const id = "/slide/1/element/3";
await space.locks.release(id);
```

#### subscribe()

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.

#### unsubscribe()

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');
```

#### get()

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);
```

#### getSelf()

Get all locks belonging to self that have the `locked` status.

```ts
type getSelf = () => Promise<Lock[]>
```

Example:

```ts
const locks = await space.locks.getSelf();
```

#### getOthers()

Get all locks belonging to all members except self that have the `locked` status.

```ts
type getOthers = () => Promise<Lock[]>
```

Example:

```ts
const locks = await space.locks.getOthers();
```

#### getAll()

Get all locks that have the `locked` status.

```ts
type getAll = () => Promise<Lock[]>
```

Example:

```ts
const locks = await space.locks.getAll();
```

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we possibly add sample response payloads in each of these methods?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that's necessary - it's always an array of Locks, as signified by the type. I think that would just create more reading for the developer who'd look for differences in those payloads maybe.

### Related types

#### Lock

Represents a Lock.

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

#### LockStatus

Represents a status of a lock.

```ts
type LockStatus = 'pending' | 'locked' | 'unlocked';
```

#### LockAttributes

Additional attributes that can be set when acquiring a lock.

```ts
type LockAttributes = Map<string, string>;
```
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"format": "prettier --write --ignore-path .gitignore src demo",
"format:check": "prettier --check --ignore-path .gitignore src demo",
"test": "vitest run",
"watch": "vitest watch",
"test:watch": "vitest watch",
"coverage": "vitest run --coverage",
"build": "npm run build:mjs && npm run build:cjs && npm run build:iife",
"build:mjs": "npx tsc --project tsconfig.mjs.json && cp res/package.mjs.json dist/mjs/package.json",
Expand Down