Skip to content

fix: unlock update hasn't triggered after lock release #164

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

Merged
merged 4 commits into from
Sep 7, 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
39 changes: 22 additions & 17 deletions demo/src/components/SpacesContext.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,38 @@
import * as React from 'react';
import { AblyProvider } from '@ably-labs/react-hooks';

import Spaces, { type Space } from '@ably/spaces';
import { Realtime } from 'ably';
import { nanoid } from 'nanoid';

import { getSpaceNameFromUrl } from '../utils';

const clientId = nanoid();

export const ably = new Realtime.Promise({
authUrl: `/api/ably-token-request?clientId=${clientId}`,
clientId,
});

const spaces = new Spaces(ably);

export const SpacesContext = React.createContext<Space | undefined>(undefined);

const SpaceContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [space, setSpace] = React.useState<Space | undefined>(undefined);
const spaceName = getSpaceNameFromUrl();

const [spaces, ably] = React.useMemo(() => {
const clientId = nanoid();

const ably = new Realtime.Promise({
authUrl: `/api/ably-token-request?clientId=${clientId}`,
clientId,
});

return [new Spaces(ably), ably];
}, []);

React.useEffect(() => {
let ignore: boolean = false;
let ignore = false;

const init = async () => {
if (ignore) {
return;
}

const spaceInstance = await spaces.get(getSpaceNameFromUrl(), {
offlineTimeout: 10_000,
});

if (spaceInstance && !space) {
if (spaceInstance && !space && !ignore) {
setSpace(spaceInstance);
}
};
Expand All @@ -41,9 +42,13 @@ const SpaceContextProvider: React.FC<{ children: React.ReactNode }> = ({ childre
return () => {
ignore = true;
};
});
}, [spaceName, spaces]);

return <SpacesContext.Provider value={space}>{children}</SpacesContext.Provider>;
return (
<AblyProvider client={ably}>
<SpacesContext.Provider value={space}>{children}</SpacesContext.Provider>{' '}
</AblyProvider>
);
};

export { SpaceContextProvider };
11 changes: 4 additions & 7 deletions demo/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { AblyProvider } from '@ably-labs/react-hooks';
import App from './App';
import './index.css';

import { ably, SpaceContextProvider } from './components';
import { SpaceContextProvider } from './components';
import { SlidesStateContextProvider } from './components/SlidesStateContext.tsx';

const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);

root.render(
<React.StrictMode>
<SpaceContextProvider>
<AblyProvider client={ably}>
<SlidesStateContextProvider>
<App />
</SlidesStateContextProvider>
</AblyProvider>
<SlidesStateContextProvider>
<App />
</SlidesStateContextProvider>
</SpaceContextProvider>
</React.StrictMode>,
);
7 changes: 0 additions & 7 deletions src/Errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,3 @@ export const ERR_LOCK_INVALIDATED = () =>
code: 101004,
statusCode: 400,
});

export const ERR_LOCK_RELEASED = () =>
new ErrorInfo({
message: 'lock was released',
code: 101005,
statusCode: 400,
});
21 changes: 11 additions & 10 deletions src/Locks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ interface SpaceTestContext {

vi.mock('ably/promises');

describe('Locks (mockClient)', () => {
describe('Locks', () => {
beforeEach<SpaceTestContext>((context) => {
const client = new Realtime({});
const presence = client.channels.get('').presence;
Expand Down Expand Up @@ -147,15 +147,15 @@ describe('Locks (mockClient)', () => {
desc: 'assigns the lock to the other member',
otherConnId: '0',
otherTimestamp: now - 100,
expectedSelfStatus: 'unlocked',
expectedSelfStatus: undefined,
expectedOtherStatus: 'locked',
},
{
name: 'when the other member has the lock with the same timestamp but a lower connectionId',
desc: 'assigns the lock to the other member',
otherConnId: '0',
otherTimestamp: now,
expectedSelfStatus: 'unlocked',
expectedSelfStatus: undefined,
expectedOtherStatus: 'locked',
},
{
Expand All @@ -164,15 +164,15 @@ describe('Locks (mockClient)', () => {
otherConnId: '2',
otherTimestamp: now,
expectedSelfStatus: 'locked',
expectedOtherStatus: 'unlocked',
expectedOtherStatus: undefined,
},
{
name: 'when the other member has the lock with a later timestamp',
desc: 'assigns the lock to self',
otherConnId: '0',
otherTimestamp: now + 100,
expectedSelfStatus: 'locked',
expectedOtherStatus: 'unlocked',
expectedOtherStatus: undefined,
},
])('$name', ({ desc, otherConnId, otherTimestamp, expectedSelfStatus, expectedOtherStatus }) => {
it<SpaceTestContext>(desc, async ({ client, space }) => {
Expand Down Expand Up @@ -216,12 +216,12 @@ describe('Locks (mockClient)', () => {
await space.locks.processPresenceMessage(msg);
const selfMember = (await space.members.getByConnectionId(client.connection.id!))!;
const selfLock = space.locks.getLock(lockID, selfMember.connectionId)!;
expect(selfLock.status).toBe(expectedSelfStatus);
expect(selfLock?.status).toBe(expectedSelfStatus);
const otherMember = (await space.members.getByConnectionId(otherConnId))!;
const otherLock = space.locks.getLock(lockID, otherMember.connectionId)!;
expect(otherLock.status).toBe(expectedOtherStatus);
expect(otherLock?.status).toBe(expectedOtherStatus);

if (expectedSelfStatus === 'unlocked') {
if (!expectedSelfStatus) {
expect(emitSpy).toHaveBeenCalledTimes(1);
expect(emitSpy).toHaveBeenNthCalledWith(1, 'update', lockEvent(selfMember, 'unlocked'));
} else {
Expand Down Expand Up @@ -318,14 +318,15 @@ describe('Locks (mockClient)', () => {
const member = (await space.members.getSelf())!;

const lockID = 'test';
const timestamp = Date.now();
const msg = Realtime.PresenceMessage.fromValues({
connectionId: member.connectionId,
extras: {
locks: [
{
id: lockID,
status: 'pending',
timestamp: Date.now(),
timestamp,
},
],
},
Expand All @@ -339,7 +340,7 @@ describe('Locks (mockClient)', () => {

expect(presenceUpdate).toHaveBeenCalledTimes(1);
const updateMsg = presenceUpdate.mock.calls[0][0];
expect(updateMsg.extras).not.toBeDefined();
expect(updateMsg.extras).toEqual({ locks: [{ id: lockID, member, timestamp, status: 'unlocked' }] });
});
});

Expand Down
40 changes: 17 additions & 23 deletions src/Locks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,7 @@ import { Types } from 'ably';
import Space from './Space.js';
import type { Lock, SpaceMember } from './types.js';
import type { PresenceMember } from './utilities/types.js';
import {
ERR_LOCK_IS_LOCKED,
ERR_LOCK_INVALIDATED,
ERR_LOCK_REQUEST_EXISTS,
ERR_LOCK_RELEASED,
ERR_NOT_ENTERED_SPACE,
} from './Errors.js';
import { ERR_LOCK_IS_LOCKED, ERR_LOCK_INVALIDATED, ERR_LOCK_REQUEST_EXISTS, ERR_NOT_ENTERED_SPACE } from './Errors.js';
import EventEmitter, {
InvalidArgumentError,
inspect,
Expand Down Expand Up @@ -126,13 +120,20 @@ export default class Locks extends EventEmitter<LockEventMap> {

async release(id: string): Promise<void> {
const self = await this.space.members.getSelf();

if (!self) {
throw ERR_NOT_ENTERED_SPACE();
}

this.deleteLock(id, self.connectionId);
const lock = this.getLock(id, self.connectionId);
if (!lock) return;

await this.updatePresence(self);
lock.status = 'unlocked';
lock.reason = undefined;
// Send presence update with the updated lock, but delete afterwards so when the
// message is processed an update event is fired
this.updatePresence(self);
this.deleteLock(id, self.connectionId);
}

subscribe<K extends EventKey<LockEventMap>>(
Expand Down Expand Up @@ -181,9 +182,9 @@ export default class Locks extends EventEmitter<LockEventMap> {

if (lock) {
lock.status = 'unlocked';
lock.reason = ERR_LOCK_RELEASED();
locks.delete(member.connectionId);
lock.reason = undefined;
this.emit('update', lock);
locks.delete(member.connectionId);
}
}

Expand All @@ -206,19 +207,12 @@ export default class Locks extends EventEmitter<LockEventMap> {
this.setLock({ ...lock, member });
});

// handle locks which have been removed from presence extras
// handle locks which have been unlocked and longer need to be held locally
for (const locks of this.locks.values()) {
const lock = locks.get(member.connectionId);

if (!lock) {
continue;
}

if (!message.extras.locks.some((l: Lock) => l.id === lock.id)) {
lock.status = 'unlocked';
lock.reason = ERR_LOCK_RELEASED();
locks.delete(member.connectionId);
this.emit('update', lock);
for (const lock of locks.values()) {
if (lock.status === 'unlocked') {
this.deleteLock(lock.id, lock.member.connectionId);
}
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/Spaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ class Spaces {
throw ERR_SPACE_NAME_MISSING();
}

if (this.spaces[name]) return this.spaces[name];

if (this.connection.state !== 'connected') {
await this.connection.once('connected');
}

if (this.spaces[name]) return this.spaces[name];

const space = new Space(name, this.client, options);
this.spaces[name] = space;

Expand Down