Skip to content
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

Enable 'derived' options in 'useChannel' hook #1501

Merged
merged 9 commits into from
Nov 16, 2023
23 changes: 23 additions & 0 deletions docs/react.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,29 @@ const { channel } = useChannel({ channelName: "your-channel-name", options: { ..
});
```

[Subscription filters](https://ably.com/docs/channels#filter-subscribe) are also supported:

```javascript
const deriveOptions = { filter: 'headers.email == `"[email protected]"` || headers.company == `"domain"`' }
const { channel } = useChannel({ channelName: "your-derived-channel-name", options: { ... }, deriveOptions }, (message) => {
...
});
```

Please note that attempts to publish to a derived channel (the one created or retrieved with a filter expression) will fail. In order to send messages to the channel called _"your-derived-channel-name"_ from the example above, you will need to create another channel instance without a filter expression.

```javascript
const channelName = "your-derived-channel-name";
const options = { ... };
const deriveOptions = { filter: 'headers.email == `"[email protected]"` || headers.company == `"domain"`' }
const callback = (message) => { ... };

const { channel: readOnlyChannelInstance } = useChannel({ channelName, options, deriveOptions }, callback);
const { channel: readWriteChannelInstance } = useChannel({ channelName, options }, callback); // NB! No 'deriveOptions' passed here

readWriteChannelInstance.publish("test-message", { text: "message text" });
```

---

### usePresence
Expand Down
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"@ably/vcdiff-decoder": "1.0.4",
"@testing-library/react": "^13.3.0",
"@types/crypto-js": "^4.0.1",
"@types/jmespath": "^0.15.2",
owenpearson marked this conversation as resolved.
Show resolved Hide resolved
"@types/node": "^15.0.0",
"@types/request": "^2.48.7",
"@types/ws": "^8.2.0",
Expand Down Expand Up @@ -76,6 +77,7 @@
"grunt-shell": "~1.1",
"grunt-webpack": "^4.0.2",
"hexy": "~0.2",
"jmespath": "^0.16.0",
"jsdom": "^20.0.0",
"kexec": "ably-forks/node-kexec#update-for-node-12",
"minimist": "^1.2.5",
Expand Down
97 changes: 95 additions & 2 deletions src/platform/react-hooks/sample-app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,36 @@ import './App.css';

function App() {
const [messages, updateMessages] = useState<Types.Message[]>([]);
const [derivedChannelMessages, updateDerivedChannelMessages] = useState<Types.Message[]>([]);
const [frontOficeOnlyMessages, updateFrontOfficeOnlyMessages] = useState<Types.Message[]>([]);

const [skip, setSkip] = useState(false);
const { channel, ably } = useChannel({ channelName: 'your-channel-name', skip }, (message) => {
updateMessages((prev) => [...prev, message]);
});

useChannel(
{
channelName: 'your-derived-channel-name',
deriveOptions: { filter: 'headers.email == `"[email protected]"` || headers.company == `"domain"`' },
},
(message) => {
updateDerivedChannelMessages((prev) => [...prev, message]);
}
);

useChannel(
{
channelName: 'your-derived-channel-name',
deriveOptions: { filter: 'headers.role == `"front-office"` || headers.company == `"domain"`' },
},
(message) => {
updateFrontOfficeOnlyMessages((prev) => [...prev, message]);
}
);

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

const { presenceData, updateStatus } = usePresence(
{ channelName: 'your-channel-name', skip },
{ foo: 'bar' },
Expand All @@ -42,7 +67,14 @@ function App() {
setChannelStateReason(stateChange.reason ?? undefined);
});

const messagePreviews = messages.map((msg, index) => <li key={index}>{msg.data.text}</li>);
const messagePreviews = messages.map((message, idx) => <MessagePreview key={idx} message={message} />);
const derivedChannelMessagePreviews = derivedChannelMessages.map((message, idx) => (
<MessagePreview key={idx} message={message} />
));
const frontOfficeMessagePreviews = frontOficeOnlyMessages.map((message, idx) => (
<MessagePreview key={idx} message={message} />
));

const presentClients = presenceData.map((msg, index) => (
<li key={index}>
{msg.clientId}: {JSON.stringify(msg.data)}
Expand All @@ -69,6 +101,57 @@ function App() {
>
Update status to hello
</button>
<button
onClick={() => {
anotherChannelPublisher.publish({
name: 'test-message',
data: {
text: 'This is a message for Rob',
},
extras: {
headers: {
email: '[email protected]',
},
},
});
}}
>
Send Message to Rob Only
</button>
<button
onClick={() => {
anotherChannelPublisher.publish({
name: 'test-message',
data: {
text: 'This is a company-wide message',
},
extras: {
headers: {
company: 'domain',
},
},
});
}}
>
Send Company-wide message
</button>
<button
onClick={() => {
anotherChannelPublisher.publish({
name: 'test-message',
data: {
text: 'This is a message for front office employees only',
},
extras: {
headers: {
role: 'front-office',
},
},
});
}}
>
Send message to Front Office
</button>
</div>

<div style={{ position: 'fixed', width: '250px' }}>
Expand All @@ -94,7 +177,13 @@ function App() {
<div>{ablyErr}</div>

<h2>Messages</h2>
<ul>{messagePreviews}</ul>
{<ul>{messagePreviews}</ul>}

<h2>Derived Channel Messages</h2>
<ul>{derivedChannelMessagePreviews}</ul>

<h2>Front Office Messages</h2>
<ul>{frontOfficeMessagePreviews}</ul>

<h2>Present Clients</h2>
<ul>{presentClients}</ul>
Expand All @@ -103,6 +192,10 @@ function App() {
);
}

function MessagePreview({ message }: { message: Types.Message }) {
return <li>{message.data.text}</li>;
}

function ConnectionState() {
const ably = useAbly();
const [connectionState, setConnectionState] = useState(ably.connection.state);
Expand Down
1 change: 1 addition & 0 deletions src/platform/react-hooks/src/AblyReactHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Types } from '../../../../ably.js';
export type ChannelNameAndOptions = {
channelName: string;
options?: Types.ChannelOptions;
deriveOptions?: Types.DeriveOptions;
id?: string;
subscribeOnly?: boolean;
skip?: boolean;
Expand Down
45 changes: 43 additions & 2 deletions src/platform/react-hooks/src/fakes/ably.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Types } from 'ably';
import { search } from 'jmespath';

export class FakeAblySdk {
public clientId: string;
Expand Down Expand Up @@ -89,7 +90,7 @@ class Connection extends EventEmitter {
export class ClientChannelsCollection {
private client: FakeAblySdk;
private channels: FakeAblyChannels;
private _channelConnections: Map<string, ClientSingleChannelConnection>;
private _channelConnections: Map<string, ClientSingleChannelConnection | ClientSingleDerivedChannelConnection>;

constructor(client: FakeAblySdk, channels: FakeAblyChannels) {
this.client = client;
Expand All @@ -107,6 +108,16 @@ export class ClientChannelsCollection {
return channelConnection;
}
}

public getDerived(name: string, options: Types.DeriveOptions): ClientSingleDerivedChannelConnection {
let channelConnection = this._channelConnections.get(name);
if (channelConnection) return channelConnection as ClientSingleDerivedChannelConnection;

const channel = this.channels.get(name);
channelConnection = new ClientSingleDerivedChannelConnection(this.client, channel, options);
this._channelConnections.set(name, channelConnection);
return channelConnection;
}
}

export class ClientSingleChannelConnection extends EventEmitter {
Expand Down Expand Up @@ -147,6 +158,32 @@ export class ClientSingleChannelConnection extends EventEmitter {
}
}

export class ClientSingleDerivedChannelConnection extends EventEmitter {
private client: FakeAblySdk;
private channel: Channel;
private deriveOpts: Types.DeriveOptions;

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

public async subscribe(
eventOrCallback: Types.messageCallback<Types.Message> | string | Array<string>,
listener?: Types.messageCallback<Types.Message>
) {
if (typeof eventOrCallback === 'function') eventOrCallback.deriveOptions = this.deriveOpts;
if (typeof listener === 'function') listener.deriveOpts = this.deriveOpts;
this.channel.subscribe(this.client.clientId, eventOrCallback, listener);
}

public unsubscribe() {
this.channel.subscriptionsPerClient.delete(this.client.clientId);
}
}

export class ClientPresenceConnection {
private client: FakeAblySdk;
private presence: ChannelPresence;
Expand Down Expand Up @@ -246,7 +283,11 @@ export class Channel {
}

for (const subscription of subs) {
subscription(messageEnvelope);
const filter = subscription.deriveOptions?.filter;
if (!filter) return subscription(messageEnvelope);
const headers = messageEnvelope.data?.extras?.headers;
const found = search({ headers }, filter);
if (found) subscription(messageEnvelope);
}
}

Expand Down
Loading
Loading