Skip to content

Commit

Permalink
Merge pull request #1620 from ably/SDK-4083/channel-provider
Browse files Browse the repository at this point in the history
`ChannelProvider` implementation
  • Loading branch information
ttypic authored Feb 27, 2024
2 parents f33d4c0 + 8996ff5 commit ec95b59
Show file tree
Hide file tree
Showing 16 changed files with 247 additions and 151 deletions.
8 changes: 5 additions & 3 deletions src/platform/react-hooks/sample-app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function App() {
useChannel(
{
channelName: 'your-derived-channel-name',
deriveOptions: { filter: 'headers.email == `"rob.pike@domain.com"` || headers.company == `"domain"`' },
id: 'rob',
},
(message) => {
updateDerivedChannelMessages((prev) => [...prev, message]);
Expand All @@ -32,14 +32,16 @@ function App() {
useChannel(
{
channelName: 'your-derived-channel-name',
deriveOptions: { filter: 'headers.role == `"front-office"` || headers.company == `"domain"`' },
id: 'frontOffice',
},
(message) => {
updateFrontOfficeOnlyMessages((prev) => [...prev, message]);
}
);

const { channel: anotherChannelPublisher } = useChannel({ channelName: 'your-derived-channel-name' });
const { channel: anotherChannelPublisher } = useChannel({
channelName: 'your-derived-channel-name',
});

const { presenceData, updateStatus } = usePresence(
{ channelName: 'your-channel-name', skip },
Expand Down
25 changes: 22 additions & 3 deletions src/platform/react-hooks/sample-app/src/script.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ import { createRoot } from 'react-dom/client';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import * as Ably from 'ably';

import App from './App.js';
import { AblyProvider } from '../../src/index.js';
import { AblyProvider, ChannelProvider } from '../../src/index.js';

const container = document.getElementById('root')!;

Check warning on line 9 in src/platform/react-hooks/sample-app/src/script.tsx

View workflow job for this annotation

GitHub Actions / lint

Forbidden non-null assertion

Expand All @@ -22,7 +21,27 @@ const root = createRoot(container);
root.render(
<React.StrictMode>
<AblyProvider client={client}>
<App />
<AblyProvider id="rob" client={client}>
<AblyProvider id="frontOffice" client={client}>
<ChannelProvider channelName="your-channel-name" options={{ modes: ['PRESENCE', 'PUBLISH', 'SUBSCRIBE'] }}>
<ChannelProvider channelName="your-derived-channel-name">
<ChannelProvider
id="rob"
channelName="your-derived-channel-name"
deriveOptions={{ filter: 'headers.email == `"rob.pike@domain.com"` || headers.company == `"domain"`' }}
>
<ChannelProvider
id="frontOffice"
channelName="your-derived-channel-name"
deriveOptions={{ filter: 'headers.role == `"front-office"` || headers.company == `"domain"`' }}
>
<App />
</ChannelProvider>
</ChannelProvider>
</ChannelProvider>
</ChannelProvider>
</AblyProvider>
</AblyProvider>
</AblyProvider>
</React.StrictMode>
);
21 changes: 16 additions & 5 deletions src/platform/react-hooks/src/AblyProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ interface AblyProviderProps {
id?: string;
}

type AblyContextType = React.Context<Ably.RealtimeClient>;
export interface AblyContextProps {
client: Ably.RealtimeClient;
_channelNameToInstance: Record<string, Ably.RealtimeChannel>;
}

type AblyContextType = React.Context<AblyContextProps>;

// An object is appended to `React.createContext` which stores all contexts
// indexed by id, which is used by useAbly to find the correct context when an
Expand All @@ -24,16 +29,22 @@ export function getContext(ctxId = 'default'): AblyContextType {
}

export const AblyProvider = ({ client, children, id = 'default' }: AblyProviderProps) => {
const value: AblyContextProps = useMemo(
() => ({
client,
_channelNameToInstance: {},
}),
[client]
);

if (!client) {
throw new Error('AblyProvider: the `client` prop is required');
}

const realtime = useMemo(() => client, [client]);

let context = getContext(id);
if (!context) {
context = ctxMap[id] = React.createContext(realtime);
context = ctxMap[id] = React.createContext({ client, _channelNameToInstance: {} });
}

return <context.Provider value={realtime}>{children}</context.Provider>;
return <context.Provider value={value}>{children}</context.Provider>;
};
2 changes: 0 additions & 2 deletions src/platform/react-hooks/src/AblyReactHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import * as Ably from 'ably';

export type ChannelNameAndOptions = {
channelName: string;
options?: Ably.ChannelOptions;
deriveOptions?: Ably.DeriveOptions;
id?: string;
subscribeOnly?: boolean;
skip?: boolean;
Expand Down
47 changes: 47 additions & 0 deletions src/platform/react-hooks/src/ChannelProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React, { useEffect, useMemo } from 'react';
import * as Ably from 'ably';
import { type AblyContextProps, getContext } from './AblyProvider.js';
import { channelOptionsWithAgent } from './AblyReactHooks.js';

interface ChannelProviderProps {
id?: string;
channelName: string;
options?: Ably.ChannelOptions;
deriveOptions?: Ably.DeriveOptions;
children?: React.ReactNode | React.ReactNode[] | null;
}

export const ChannelProvider = ({
id = 'default',
channelName,
options,
deriveOptions,
children,
}: ChannelProviderProps) => {
const context = getContext(id);
const { client, _channelNameToInstance } = React.useContext(context);

if (_channelNameToInstance[channelName]) {
throw new Error('You can not use more than one `ChannelProvider` with the same channel name');
}

const channel = deriveOptions
? client.channels.getDerived(channelName, deriveOptions)
: client.channels.get(channelName);

const value: AblyContextProps = useMemo(() => {
return {
client,
_channelNameToInstance: {
..._channelNameToInstance,
[channelName]: channel,
},
};
}, [client, channel, channelName, _channelNameToInstance]);

useEffect(() => {
channel.setOptions(channelOptionsWithAgent(options));
}, [channel, options]);

return <context.Provider value={value}>{children}</context.Provider>;
};
24 changes: 20 additions & 4 deletions src/platform/react-hooks/src/fakes/ably.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export class ClientChannelsCollection {
if (channelConnection) {
return channelConnection;
} else {
channelConnection = new ClientSingleChannelConnection(this.client, this.channels.get(name));
channelConnection = new ClientSingleChannelConnection(this.client, this.channels.get(name), name);
this._channelConnections.set(name, channelConnection);
return channelConnection;
}
Expand All @@ -115,7 +115,7 @@ export class ClientChannelsCollection {
if (channelConnection) return channelConnection as ClientSingleDerivedChannelConnection;

const channel = this.channels.get(name);
channelConnection = new ClientSingleDerivedChannelConnection(this.client, channel, options);
channelConnection = new ClientSingleDerivedChannelConnection(this.client, channel, options, name);
this._channelConnections.set(name, channelConnection);
return channelConnection;
}
Expand All @@ -127,13 +127,15 @@ export class ClientSingleChannelConnection extends EventEmitter {

public presence: any;
public state: string;
public name: string;

constructor(client: FakeAblySdk, channel: Channel) {
constructor(client: FakeAblySdk, channel: Channel, name: string) {
super();
this.client = client;
this.channel = channel;
this.presence = new ClientPresenceConnection(this.client, this.channel.presence);
this.state = 'attached';
this.name = name;
}

publish(messages: any, callback?: Ably.errorCallback): void;
Expand All @@ -157,18 +159,24 @@ export class ClientSingleChannelConnection extends EventEmitter {
public detach() {
this.channel.subscriptionsPerClient.delete(this.client.clientId);
}

public async setOptions() {
// do nothing
}
}

export class ClientSingleDerivedChannelConnection extends EventEmitter {
private client: FakeAblySdk;
private channel: Channel;
private deriveOpts: Ably.DeriveOptions;
public name?: string;

constructor(client: FakeAblySdk, channel: Channel, deriveOptions?: Ably.DeriveOptions) {
constructor(client: FakeAblySdk, channel: Channel, deriveOptions?: Ably.DeriveOptions, name?: string) {
super();
this.client = client;
this.channel = channel;
this.deriveOpts = deriveOptions;
this.name = name;
}

public async subscribe(
Expand All @@ -183,6 +191,10 @@ export class ClientSingleDerivedChannelConnection extends EventEmitter {
public unsubscribe() {
this.channel.subscriptionsPerClient.delete(this.client.clientId);
}

public async setOptions() {
// do nothing
}
}

export class ClientPresenceConnection {
Expand Down Expand Up @@ -328,6 +340,10 @@ export class Channel {
subs.push(callback);
}
}

public async setOptions() {
// do nothing
}
}

export class ChannelPresence {
Expand Down
2 changes: 1 addition & 1 deletion src/platform/react-hooks/src/hooks/useAbly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { getContext } from '../AblyProvider.js';
import * as API from 'ably';

export function useAbly(id = 'default'): API.RealtimeClient {
const client = React.useContext(getContext(id)) as API.RealtimeClient;
const client = React.useContext(getContext(id)).client;

if (!client) {
throw new Error(
Expand Down
Loading

0 comments on commit ec95b59

Please sign in to comment.