From 7d9e2677697850f104091ce23d8d71991d0ee3ac Mon Sep 17 00:00:00 2001 From: Lewis Marshall Date: Mon, 7 Aug 2023 00:53:36 +0100 Subject: [PATCH] locks: Support releasing locks Signed-off-by: Lewis Marshall --- src/Errors.ts | 6 ++++ src/Locks.mockClient.test.ts | 68 ++++++++++++++++++++++++++++++++++++ src/Locks.ts | 36 ++++++++++++++++++- 3 files changed, 109 insertions(+), 1 deletion(-) diff --git a/src/Errors.ts b/src/Errors.ts index be5f73ca..2b965308 100644 --- a/src/Errors.ts +++ b/src/Errors.ts @@ -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, +}); diff --git a/src/Locks.mockClient.test.ts b/src/Locks.mockClient.test.ts index 23a6c32b..9e23757b 100644 --- a/src/Locks.mockClient.test.ts +++ b/src/Locks.mockClient.test.ts @@ -221,5 +221,73 @@ describe('Locks (mockClient)', () => { } }); }); + + it('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('errors if releasing before entering the space', ({ space }) => { + expect(space.locks.release('test')).rejects.toThrowError(); + }); + + it('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); + }); }); }); diff --git a/src/Locks.ts b/src/Locks.ts index e876eed0..9b6cbda7 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -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, @@ -87,6 +87,20 @@ export default class Locks extends EventEmitter { return req; } + async release(id: string): Promise { + 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>( listenerOrEvents?: K | K[] | EventListener, listener?: EventListener, @@ -128,6 +142,16 @@ export default class Locks extends EventEmitter { } 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; } @@ -149,6 +173,16 @@ export default class Locks extends EventEmitter { 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