Skip to content

Commit

Permalink
locks: Support releasing locks
Browse files Browse the repository at this point in the history
Signed-off-by: Lewis Marshall <lewis.marshall@ably.com>
  • Loading branch information
lmars committed Aug 6, 2023
1 parent f5ff5e9 commit 7d9e267
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 1 deletion.
6 changes: 6 additions & 0 deletions src/Errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,9 @@ export const ERR_LOCK_INVALIDATED = new ErrorInfo({
code: 40052,
statusCode: 400,
});

export const ERR_LOCK_RELEASED = new ErrorInfo({
message: 'lock was released',
code: 40053,
statusCode: 400,
});
68 changes: 68 additions & 0 deletions src/Locks.mockClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,5 +221,73 @@ describe('Locks (mockClient)', () => {
}
});
});

it<SpaceTestContext>('sets a released request to UNLOCKED', async ({ space }) => {
await space.enter();
const member = space.getSelf();

let msg = Realtime.PresenceMessage.fromValues({
connectionId: member.connectionId,
extras: {
locks: [
{
id: lockID,
status: LockStatus.PENDING,
timestamp: Date.now(),
},
],
},
});
space.locks.processPresenceMessage(msg);

const emitSpy = vi.spyOn(space.locks, 'emit');

msg = Realtime.PresenceMessage.fromValues({
connectionId: member.connectionId,
extras: undefined,
});
space.locks.processPresenceMessage(msg);

const lock = member.locks.get(lockID);
expect(lock).not.toBeDefined();
expect(emitSpy).toHaveBeenCalledWith('update', lockEvent(member, LockStatus.UNLOCKED));
});
});

describe('release', () => {
it<SpaceTestContext>('errors if releasing before entering the space', ({ space }) => {
expect(space.locks.release('test')).rejects.toThrowError();
});

it<SpaceTestContext>('removes the identified lock request from presence extras', async ({ space, presence }) => {
await space.enter();
const member = space.getSelf();

const lockID = 'test';
const msg = Realtime.PresenceMessage.fromValues({
connectionId: member.connectionId,
extras: {
locks: [
{
id: lockID,
status: LockStatus.PENDING,
timestamp: Date.now(),
},
],
},
});
space.locks.processPresenceMessage(msg);
expect(space.locks.get(lockID)).toBeDefined();

const presenceUpdate = vi.spyOn(presence, 'update');

await space.locks.release(lockID);

const presenceMessage = {
data: { profileData: {} },
extras: undefined,
};
expect(presenceUpdate).toHaveBeenCalledWith(presenceMessage);
});
});
});
36 changes: 35 additions & 1 deletion src/Locks.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Types } from 'ably';

import Space, { SpaceMember } from './Space.js';
import { ERR_LOCK_IS_LOCKED, ERR_LOCK_INVALIDATED, ERR_LOCK_REQUEST_EXISTS } from './Errors.js';
import { ERR_LOCK_IS_LOCKED, ERR_LOCK_INVALIDATED, ERR_LOCK_REQUEST_EXISTS, ERR_LOCK_RELEASED } from './Errors.js';
import EventEmitter, {
InvalidArgumentError,
inspect,
Expand Down Expand Up @@ -87,6 +87,20 @@ export default class Locks extends EventEmitter<LockEventMap> {
return req;
}

async release(id: string): Promise<void> {
const self = this.space.getSelf();
if (!self) {
throw new Error('Must enter a space before acquiring a lock');
}

if (!self.locks) {
return;
}
self.locks.delete(id);

await this.space.updateSelf(self);
}

subscribe<K extends EventKey<LockEventMap>>(
listenerOrEvents?: K | K[] | EventListener<LockEventMap[K]>,
listener?: EventListener<LockEventMap[K]>,
Expand Down Expand Up @@ -128,6 +142,16 @@ export default class Locks extends EventEmitter<LockEventMap> {
}

if (!message.extras || !message.extras.locks || !Array.isArray(message.extras.locks)) {
// there are no locks in presence, so release any existing locks for the
// member
if (member.locks && member.locks.size > 0) {
for (const [id, lock] of member.locks.entries()) {
lock.status = LockStatus.UNLOCKED;
lock.reason = ERR_LOCK_RELEASED;
member.locks.delete(id);
this.emit('update', { member, request: lock });
}
}
return;
}

Expand All @@ -149,6 +173,16 @@ export default class Locks extends EventEmitter<LockEventMap> {

member.locks.set(lock.id, lock);
});

// handle locks which have been removed from presence extras
for (const [id, lock] of member.locks.entries()) {
if (!message.extras.locks.some((req) => req.id === id)) {
lock.status = LockStatus.UNLOCKED;
lock.reason = ERR_LOCK_RELEASED;
member.locks.delete(id);
this.emit('update', { member, request: lock });
}
}
}

// process a PENDING lock request by determining whether it should be
Expand Down

0 comments on commit 7d9e267

Please sign in to comment.