Skip to content

Commit

Permalink
Merge pull request #1501 from rustworthy/feat/react-use-derived-channel
Browse files Browse the repository at this point in the history
Enable 'derived' options in 'useChannel' hook
  • Loading branch information
owenpearson authored Nov 16, 2023
2 parents 7220619 + 63adfc4 commit 3dbd400
Show file tree
Hide file tree
Showing 8 changed files with 582 additions and 27 deletions.
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",
"@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

0 comments on commit 3dbd400

Please sign in to comment.