diff --git a/src/Locks.test.ts b/src/Locks.test.ts index 409bf00a..9803acf8 100644 --- a/src/Locks.test.ts +++ b/src/Locks.test.ts @@ -126,6 +126,35 @@ describe('Locks', () => { expect(emitSpy).toHaveBeenCalledWith('update', lockEvent(member, 'locked')); }); + it('keeps a resolved lock when a later presence message still lists it as PENDING', async ({ + space, + }) => { + const member = (await space.members.getSelf())!; + const timestamp = Date.now(); + const pendingA = { id: 'a', status: 'pending', timestamp }; + const pendingB = { id: 'b', status: 'pending', timestamp: timestamp + 1 }; + + // Acquiring two locks concurrently publishes one presence message per lock, + // and the second message repeats the first lock as PENDING. + await space.locks.processPresenceMessage( + Realtime.PresenceMessage.fromValues({ + action: 'update', + connectionId: member.connectionId, + extras: { locks: [{ ...pendingA }] }, + }), + ); + await space.locks.processPresenceMessage( + Realtime.PresenceMessage.fromValues({ + action: 'update', + connectionId: member.connectionId, + extras: { locks: [{ ...pendingA }, { ...pendingB }] }, + }), + ); + + expect(space.locks.getLock('a', member.connectionId)!.status).toBe('locked'); + expect(space.locks.getLock('b', member.connectionId)!.status).toBe('locked'); + }); + // use table driven tests to check whether a PENDING request for "self" // (i.e. the current member) transitions to LOCKED or UNLOCKED when the // referenced lock is already held by some other member with timestamp diff --git a/src/Locks.ts b/src/Locks.ts index 8d73a21f..c9edbc4b 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -374,6 +374,18 @@ export default class Locks extends EventEmitter { message.extras.locks.forEach((lock: Lock) => { const existing = this.getLock(lock.id, member.connectionId); + // a presence message published before this request was resolved still lists + // it as PENDING (for example when several locks are acquired concurrently), + // so do not move an already resolved request back to PENDING + if ( + lock.status === 'pending' && + existing && + existing.status !== 'pending' && + existing.timestamp === lock.timestamp + ) { + return; + } + // special-case the handling of PENDING requests, which will eventually // be done by the Ably system, at which point this can be removed if (lock.status === 'pending' && (!existing || existing.status === 'pending')) {