Skip to content

Commit 79fac78

Browse files
committed
add input streams example
1 parent f0f7315 commit 79fac78

File tree

11 files changed

+413
-11
lines changed

11 files changed

+413
-11
lines changed

docker/config/s2-spec.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"$schema": "https://raw.githubusercontent.com/s2-streamstore/s2/main/cli/schema.json",
3+
"basins": [
4+
{
5+
"name": "trigger-local",
6+
"config": {
7+
"create_stream_on_append": true,
8+
"create_stream_on_read": true
9+
}
10+
}
11+
]
12+
}

docker/docker-compose.yml

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,9 @@ services:
145145

146146
s2:
147147
image: ghcr.io/s2-streamstore/s2
148-
command: lite
148+
command: ["lite", "--init-file", "/s2-spec.json"]
149+
volumes:
150+
- ./config/s2-spec.json:/s2-spec.json:ro
149151
ports:
150152
- "4566:80"
151153
networks:
@@ -157,16 +159,6 @@ services:
157159
retries: 5
158160
start_period: 3s
159161

160-
s2-init:
161-
image: curlimages/curl:latest
162-
depends_on:
163-
s2:
164-
condition: service_healthy
165-
networks:
166-
- app_network
167-
restart: "no"
168-
entrypoint: ["sh", "-c", "curl -sf -X PUT http://s2:80/v1/basins/trigger-local -H 'Content-Type: application/json' -d '{\"config\":{\"create_stream_on_append\":true,\"create_stream_on_read\":true}}' && echo ' Basin trigger-local ready' || echo ' Basin trigger-local already exists'"]
169-
170162
toxiproxy:
171163
container_name: toxiproxy
172164
image: ghcr.io/shopify/toxiproxy:latest

references/realtime-streams/src/app/actions.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import { tasks, auth } from "@trigger.dev/sdk";
44
import type { streamsTask } from "@/trigger/streams";
55
import type { aiChatTask } from "@/trigger/ai-chat";
6+
import type { approvalTask } from "@/trigger/approval";
7+
import type { messagesTask } from "@/trigger/messages";
68
import { redirect } from "next/navigation";
79
import type { UIMessage } from "ai";
810

@@ -43,6 +45,16 @@ export async function triggerStreamTask(
4345
redirect(path);
4446
}
4547

48+
export async function triggerApprovalTask() {
49+
const handle = await tasks.trigger<typeof approvalTask>("approval-flow", {});
50+
redirect(`/approval/${handle.id}?accessToken=${handle.publicAccessToken}`);
51+
}
52+
53+
export async function triggerMessagesTask() {
54+
const handle = await tasks.trigger<typeof messagesTask>("messages-flow", { messageCount: 5 });
55+
redirect(`/messages/${handle.id}?accessToken=${handle.publicAccessToken}`);
56+
}
57+
4658
export async function triggerAIChatTask(messages: UIMessage[]) {
4759
// Trigger the AI chat task
4860
const handle = await tasks.trigger<typeof aiChatTask>("ai-chat", {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { ApprovalFlow } from "@/components/approval-flow";
2+
import Link from "next/link";
3+
4+
export default function ApprovalPage({
5+
params,
6+
searchParams,
7+
}: {
8+
params: { runId: string };
9+
searchParams: { accessToken?: string };
10+
}) {
11+
const { runId } = params;
12+
const accessToken = searchParams.accessToken;
13+
14+
if (!accessToken) {
15+
return (
16+
<div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
17+
<main className="flex flex-col gap-8 row-start-2 items-center">
18+
<h1 className="text-2xl font-bold text-red-600">Missing Access Token</h1>
19+
<p className="text-gray-600">This page requires an access token.</p>
20+
<Link href="/" className="text-blue-600 hover:underline">
21+
Go back home
22+
</Link>
23+
</main>
24+
</div>
25+
);
26+
}
27+
28+
return (
29+
<div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
30+
<main className="flex flex-col gap-8 row-start-2 items-start w-full max-w-4xl">
31+
<div className="flex items-center justify-between w-full">
32+
<h1 className="text-2xl font-bold">Approval Flow: {runId}</h1>
33+
<Link
34+
href="/"
35+
className="px-4 py-2 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300 transition-colors"
36+
>
37+
&larr; Back to Home
38+
</Link>
39+
</div>
40+
41+
<div className="w-full bg-gray-50 p-4 rounded-lg">
42+
<p className="text-sm text-gray-600">
43+
This task is waiting for your approval. Click Approve or Reject to send input to the
44+
running task via <code className="bg-gray-200 px-1 rounded">useInputStreamSend</code>.
45+
</p>
46+
</div>
47+
48+
<div className="w-full border border-gray-200 rounded-lg p-6 bg-white">
49+
<ApprovalFlow accessToken={accessToken} runId={runId} />
50+
</div>
51+
</main>
52+
</div>
53+
);
54+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { MessagesFlow } from "@/components/messages-flow";
2+
import Link from "next/link";
3+
4+
export default function MessagesPage({
5+
params,
6+
searchParams,
7+
}: {
8+
params: { runId: string };
9+
searchParams: { accessToken?: string };
10+
}) {
11+
const { runId } = params;
12+
const accessToken = searchParams.accessToken;
13+
14+
if (!accessToken) {
15+
return (
16+
<div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
17+
<main className="flex flex-col gap-8 row-start-2 items-center">
18+
<h1 className="text-2xl font-bold text-red-600">Missing Access Token</h1>
19+
<p className="text-gray-600">This page requires an access token.</p>
20+
<Link href="/" className="text-blue-600 hover:underline">
21+
Go back home
22+
</Link>
23+
</main>
24+
</div>
25+
);
26+
}
27+
28+
return (
29+
<div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
30+
<main className="flex flex-col gap-8 row-start-2 items-start w-full max-w-4xl">
31+
<div className="flex items-center justify-between w-full">
32+
<h1 className="text-2xl font-bold">Messages Flow: {runId}</h1>
33+
<Link
34+
href="/"
35+
className="px-4 py-2 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300 transition-colors"
36+
>
37+
&larr; Back to Home
38+
</Link>
39+
</div>
40+
41+
<div className="w-full bg-gray-50 p-4 rounded-lg">
42+
<p className="text-sm text-gray-600">
43+
Send messages to the running task using{" "}
44+
<code className="bg-gray-200 px-1 rounded">useInputStreamSend</code>. The task
45+
subscribes via <code className="bg-gray-200 px-1 rounded">.on()</code> and logs each
46+
message as it arrives. Send 5 messages to complete the task.
47+
</p>
48+
</div>
49+
50+
<div className="w-full border border-gray-200 rounded-lg p-6 bg-white">
51+
<MessagesFlow accessToken={accessToken} runId={runId} />
52+
</div>
53+
</main>
54+
</div>
55+
);
56+
}

references/realtime-streams/src/app/page.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { TriggerButton } from "@/components/trigger-button";
22
import { AIChatButton } from "@/components/ai-chat-button";
3+
import { triggerApprovalTask, triggerMessagesTask } from "./actions";
34

45
export default function Home() {
56
return (
@@ -45,6 +46,31 @@ export default function Home() {
4546
</TriggerButton>
4647
</div>
4748

49+
<div className="mt-8 pt-8 border-t border-gray-300 w-full">
50+
<h2 className="text-xl font-semibold mb-4">Input Streams</h2>
51+
<p className="text-sm text-gray-600 mb-4">
52+
Test sending data from the UI to a running task via useInputStreamSend
53+
</p>
54+
<div className="flex gap-3">
55+
<form action={triggerApprovalTask}>
56+
<button
57+
type="submit"
58+
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
59+
>
60+
Approval Flow (.wait)
61+
</button>
62+
</form>
63+
<form action={triggerMessagesTask}>
64+
<button
65+
type="submit"
66+
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
67+
>
68+
Messages Flow (.on)
69+
</button>
70+
</form>
71+
</div>
72+
</div>
73+
4874
<div className="mt-8 pt-8 border-t border-gray-300">
4975
<h2 className="text-xl font-semibold mb-4">Performance Testing</h2>
5076
<TriggerButton scenario="performance" redirect="/performance">

references/realtime-streams/src/app/streams.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,11 @@ export type DemoStreamPart = InferStreamType<typeof demoStream>;
1010
export const aiStream = streams.define<UIMessageChunk>({
1111
id: "ai",
1212
});
13+
14+
export const approvalInputStream = streams.input<{ approved: boolean; reviewer: string }>({
15+
id: "approval",
16+
});
17+
18+
export const messageInputStream = streams.input<{ text: string }>({
19+
id: "messages",
20+
});
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
"use client";
2+
3+
import { useRealtimeRun, useInputStreamSend } from "@trigger.dev/react-hooks";
4+
import type { approvalTask } from "@/trigger/approval";
5+
6+
export function ApprovalFlow({
7+
runId,
8+
accessToken,
9+
}: {
10+
runId: string;
11+
accessToken: string;
12+
}) {
13+
const { run, error: runError } = useRealtimeRun<typeof approvalTask>(runId, {
14+
accessToken,
15+
baseURL: process.env.NEXT_PUBLIC_TRIGGER_API_URL,
16+
});
17+
const {
18+
send,
19+
isLoading: isSending,
20+
error: sendError,
21+
} = useInputStreamSend<{ approved: boolean; reviewer: string }>("approval", runId, {
22+
accessToken,
23+
baseURL: process.env.NEXT_PUBLIC_TRIGGER_API_URL,
24+
});
25+
26+
const status = run?.metadata?.status as string | undefined;
27+
const reviewer = run?.metadata?.reviewer as string | undefined;
28+
const isWaiting = status === "waiting-for-approval";
29+
const isCompleted = run?.status === "COMPLETED";
30+
const isFailed = run?.status === "FAILED" || run?.status === "CANCELED";
31+
32+
return (
33+
<div className="flex flex-col gap-6">
34+
<div className="flex items-center gap-3">
35+
<div
36+
className={`w-3 h-3 rounded-full ${
37+
isWaiting
38+
? "bg-yellow-400 animate-pulse"
39+
: status === "approved"
40+
? "bg-green-400"
41+
: status === "rejected"
42+
? "bg-red-400"
43+
: status === "timed-out"
44+
? "bg-gray-400"
45+
: "bg-blue-400 animate-pulse"
46+
}`}
47+
/>
48+
<span className="text-sm text-gray-600">
49+
{!run
50+
? "Loading..."
51+
: isWaiting
52+
? "Waiting for approval"
53+
: status === "approved"
54+
? `Approved by ${reviewer}`
55+
: status === "rejected"
56+
? `Rejected by ${reviewer}`
57+
: status === "timed-out"
58+
? "Timed out"
59+
: `Status: ${run?.status ?? "unknown"}`}
60+
</span>
61+
</div>
62+
63+
{isWaiting && (
64+
<div className="flex gap-3">
65+
<button
66+
onClick={() => send({ approved: true, reviewer: "You" })}
67+
disabled={isSending}
68+
className="px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 transition-colors font-medium"
69+
>
70+
{isSending ? "Sending..." : "Approve"}
71+
</button>
72+
<button
73+
onClick={() => send({ approved: false, reviewer: "You" })}
74+
disabled={isSending}
75+
className="px-6 py-3 bg-red-600 text-white rounded-lg hover:bg-red-700 disabled:opacity-50 transition-colors font-medium"
76+
>
77+
{isSending ? "Sending..." : "Reject"}
78+
</button>
79+
</div>
80+
)}
81+
82+
{runError && <p className="text-red-500 text-sm">Run error: {runError.message}</p>}
83+
{sendError && <p className="text-red-500 text-sm">Send error: {sendError.message}</p>}
84+
85+
{(isCompleted || isFailed) && run?.output && (
86+
<pre className="bg-gray-100 p-4 rounded-lg text-sm overflow-auto">
87+
{JSON.stringify(run.output, null, 2)}
88+
</pre>
89+
)}
90+
</div>
91+
);
92+
}

0 commit comments

Comments
 (0)