Skip to content
Open
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
1 change: 1 addition & 0 deletions src/data/languages/languageData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export default {
},
aiTransport: {
javascript: '2.19',
react: '2.19',
java: '1.6',
python: '3.1',
swift: '1.2',
Expand Down
168 changes: 168 additions & 0 deletions src/pages/docs/ai-transport/token-streaming/message-per-response.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,31 @@ channel.subscribe(message -> {
}
});
```
```react
const [responses, setResponses] = useState(new Map());

// Subscribe to live messages
useChannel('ai:{{RANDOM_CHANNEL_NAME}}', (message) => {
setResponses((prev) => {
const next = new Map(prev);
switch (message.action) {
case 'message.create':
// New response started
next.set(message.serial, message.data);
break;
case 'message.append':
// Append token to existing response
next.set(message.serial, (next.get(message.serial) || '') + message.data);
break;
case 'message.update':
// Replace entire response content
next.set(message.serial, message.data);
break;
}
return next;
});
});
```
</Code>

## Client hydration <a id="hydration"/>
Expand Down Expand Up @@ -549,6 +574,31 @@ channel.subscribe(message -> {
}
});
```
```react
// Set rewind via ChannelProvider options={{ params: { rewind: '2m' } }}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WDYT about adding quick React setup context for useChannel examples?

The JS/Python/Java examples don't show full realtime client setup either, but that's fine, in those languages, channel configuration is visible inline: realtime.channels.get('ai:channel', { params: { rewind: '2m' } }) - makes it clear how the channel name, namespace prefix, and options all come together.
React splits this across a component hierarchy (AblyProvider -> ChannelProvider -> useChannel), so a reader seeing only useChannel('ai:channel', callback) doesn't see where channel options like rewind are configured or that the component must be nested inside specific providers, like ChannelProvider with channelName="ai:channel".

This rewind example addresses this with a comment, but that's easy to miss and doesn't convey the structural requirement - that your component tree needs to be shaped in a specific way for the hook to work at all.

How about adding a short <If lang="react"> block right after the "Subscribing to token streams" heading (message-per-response) and "Streaming patterns" heading (message-per-token), before the user sees the first React code example:

<If lang="react">
<Aside data-type='note'>
These examples use the `useChannel` hook from `ably/react`. Your component must be wrapped in an [`AblyProvider`](/docs/getting-started/react#prerequisites-setup-ably-provider) and a [`ChannelProvider`](/docs/getting-started/react#step-2-channel-provider) that specifies the channel name and any channel options. For example, to subscribe with rewind enabled on an
`ai:` namespaced channel:

```react
<AblyProvider client={realtimeClient}>
  <ChannelProvider channelName="ai:my-channel" options={{ params: { rewind: '2m' } }}>
    <YourComponent />
  </ChannelProvider>
</AblyProvider>
\```
</Aside>
</If>


const [responses, setResponses] = useState(new Map());

// Receive both recent historical (via rewind) and live messages
useChannel('ai:{{RANDOM_CHANNEL_NAME}}', (message) => {
setResponses((prev) => {
const next = new Map(prev);
switch (message.action) {
case 'message.create':
next.set(message.serial, message.data);
break;
case 'message.append':
const current = next.get(message.serial) || '';
next.set(message.serial, current + message.data);
break;
case 'message.update':
next.set(message.serial, message.data);
break;
}
return next;
});
});
```
</Code>

Rewind supports two formats:
Expand Down Expand Up @@ -678,6 +728,46 @@ while (page != null) {
page = page.hasNext() ? page.next() : null;
}
```
```react
const [responses, setResponses] = useState(new Map());
const hydrated = useRef(false);

// Subscribe to live messages and get the history function
const { history } = useChannel('ai:{{RANDOM_CHANNEL_NAME}}', (message) => {
setResponses((prev) => {
const next = new Map(prev);
switch (message.action) {
case 'message.create':
next.set(message.serial, message.data);
break;
case 'message.append':
next.set(message.serial, (next.get(message.serial) || '') + message.data);
break;
case 'message.update':
next.set(message.serial, message.data);
break;
}
return next;
});
});

// Fetch history on mount
useEffect(() => {
if (hydrated.current) return;
hydrated.current = true;

(async () => {
let page = await history({ untilAttach: true });
while (page) {
for (const message of page.items) {
// message.data contains the full concatenated text
setResponses((prev) => new Map(prev).set(message.serial, message.data));
}
page = page.hasNext() ? await page.next() : null;
}
})();
}, [history]);
```
</Code>

### Hydrating an in-progress response <a id="in-progress-response"/>
Expand Down Expand Up @@ -911,6 +1001,37 @@ channel.subscribe(message -> {
}
});
```
```react
// Set rewind via ChannelProvider options={{ params: { rewind: '2m' } }}

const [inProgressResponses, setInProgressResponses] = useState(new Map());

// Receive both recent historical and live messages
useChannel('ai:responses', (message) => {
const responseId = message.extras?.headers?.responseId;

if (!responseId) return;

// Skip messages for responses already loaded from database
if (completedResponses.has(responseId)) return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this react snippet is missing the example where completedResponses comes from (loaded from database)


setInProgressResponses((prev) => {
const next = new Map(prev);
switch (message.action) {
case 'message.create':
next.set(responseId, message.data);
break;
case 'message.append':
next.set(responseId, (next.get(responseId) || '') + message.data);
break;
case 'message.update':
next.set(responseId, message.data);
break;
}
return next;
});
});
```
</Code>

<Aside data-type="note">
Expand Down Expand Up @@ -1118,6 +1239,53 @@ while (page != null) {
page = page.hasNext() ? page.next() : null;
}
```
```react
const [inProgressResponses, setInProgressResponses] = useState(new Map());
const hydrated = useRef(false);

// Subscribe to live messages and get the history function
const { history } = useChannel('ai:{{RANDOM_CHANNEL_NAME}}', (message) => {
const responseId = message.extras?.headers?.responseId;

if (!responseId) return;
if (completedResponses.has(responseId)) return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same, not clear where completedResponses came from


setInProgressResponses((prev) => {
const next = new Map(prev);
switch (message.action) {
case 'message.create':
next.set(responseId, message.data);
break;
case 'message.append':
next.set(responseId, (next.get(responseId) || '') + message.data);
break;
case 'message.update':
next.set(responseId, message.data);
break;
}
return next;
});
});

// Fetch history from the last completed response until attachment
useEffect(() => {
if (hydrated.current) return;
hydrated.current = true;

(async () => {
let page = await history({ untilAttach: true, start: latestTimestamp });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not clear where latestTimestamp came from (timestamp of the latest completedResponses message)

while (page) {
for (const message of page.items) {
const responseId = message.extras?.headers?.responseId;
if (responseId) {
setInProgressResponses((prev) => new Map(prev).set(responseId, message.data));
}
}
page = page.hasNext() ? await page.next() : null;
}
})();
}, [history]);
```
</Code>

<Aside data-type="note">
Expand Down
Loading