Skip to content

[wip] redis support for multiplayer prompting #665

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

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
Draft
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
5 changes: 4 additions & 1 deletion apps/app/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,7 @@ NEXT_PUBLIC_ORIGINAL_PLAYBACK_ID=""
NEXT_PUBLIC_MULTIPLAYER_STREAM_KEY=""

GCP_BUCKET_NAME=livepeer-clips-staging
GCP_CREDENTIALS="
GCP_CREDENTIALS=""

UPSTASH_REDIS_REST_URL=<YOUR_URL>
UPSTASH_REDIS_REST_TOKEN=<YOUR_TOKEN>
49 changes: 8 additions & 41 deletions apps/app/app/(main)/MultiplayerHomepage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ export default function MultiplayerHomepage({
const { setIsGuestUser } = useGuestUserStore();
const [animationStarted, setAnimationStarted] = useState(false);
const [showContent, setShowContent] = useState(false);
const [isTutorialModalOpen, setIsTutorialModalOpen] = useState(false);
const [isMounted, setIsMounted] = useState(false);
const [showFooter, setShowFooter] = useState(false);
const promptFormRef = useRef<HTMLFormElement>(null);
Expand All @@ -53,21 +52,7 @@ export default function MultiplayerHomepage({
throttleTimeLeft,
} = useThrottledInput();

const {
promptState,
loading,
error,
userAvatarSeed,
addToPromptQueue,
addRandomPrompt,
} = usePromptsApi();

useRandomPromptApiTimer({
authenticated,
ready,
showContent,
addRandomPrompt,
});
const { prompts, activeIndex, submitPrompt } = usePromptsApi();

const redirectToCreate = () => {
if (!authenticated) {
Expand Down Expand Up @@ -122,17 +107,7 @@ export default function MultiplayerHomepage({
}, [containerRef]);

const handlePromptSubmit = getHandleSubmit(async value => {
const sessionId = "optimistic-" + Date.now();
const optimisticPrompt: PromptItem = {
text: value,
seed: userAvatarSeed || "optimistic",
isUser: true,
timestamp: Date.now(),
sessionId,
};
setOptimisticPrompts(prev => [...prev, optimisticPrompt]);
const result = await addToPromptQueue(value, userAvatarSeed, true);
setOptimisticPrompts(prev => prev.filter(p => p.sessionId !== sessionId));
const result = await submitPrompt(value);

track("daydream_landing_page_prompt_submitted", {
is_authenticated: authenticated,
Expand All @@ -149,11 +124,13 @@ export default function MultiplayerHomepage({
promptFormRef.current?.requestSubmit();
};

if (!ready || loading) {
if (!ready) {
console.log("Not ready");
return <div className="flex items-center justify-center h-screen"></div>;
}

if (!promptState) {
if (!prompts || !activeIndex) {
console.log("No prompts or activeIndex");
return (
<div className="flex items-center justify-center h-screen">
Loading prompt state...
Expand All @@ -179,8 +156,6 @@ export default function MultiplayerHomepage({
}`}
>
<HeroSection
handlePromptSubmit={handlePromptSubmit}
promptValue={prompt}
setPromptValue={setPrompt}
submitPromptForm={submitPromptForm}
isAuthenticated={authenticated}
Expand All @@ -197,10 +172,8 @@ export default function MultiplayerHomepage({
/>

<PromptPanel
promptQueue={[...optimisticPrompts, ...promptState.promptQueue]}
displayedPrompts={promptState.displayedPrompts}
promptAvatarSeeds={promptState.promptAvatarSeeds}
userPromptIndices={promptState.userPromptIndices}
prompts={prompts}
activeIndex={activeIndex}
onSubmit={handlePromptSubmit}
promptValue={prompt}
onPromptChange={handlePromptChange}
Expand All @@ -209,7 +182,6 @@ export default function MultiplayerHomepage({
throttleTimeLeft={throttleTimeLeft}
onTryCameraClick={handleButtonClick}
buttonText={authenticated ? "Create" : "Use your camera"}
isAuthenticated={authenticated}
promptFormRef={promptFormRef}
isMobile={isMobile}
/>
Expand All @@ -227,11 +199,6 @@ export default function MultiplayerHomepage({
<div className="md:hidden fixed bottom-0 left-0 w-full z-20">
<Footer showFooter={true} isMobile={true} />
</div>

<TutorialModal
isOpen={isTutorialModalOpen}
onClose={() => setIsTutorialModalOpen(false)}
/>
</div>
{children}
</>
Expand Down
72 changes: 38 additions & 34 deletions apps/app/app/api/cron/process-queue/route.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,46 @@
import type { MultiplayerPrompt } from "@/hooks/usePromptsApi";
import { redis } from "@/lib/redis";
import { NextResponse } from "next/server";
import { checkAndProcessQueue } from "../../prompts/store";
import { getPromptState as dbGetPromptState } from "../../../../lib/db/services/prompt-queue";
import { applyPromptToStream } from "../../prompts/store";

export async function GET() {
try {
const stateBefore = await dbGetPromptState();
const queueLengthBefore = stateBefore.promptQueue.length;

await checkAndProcessQueue();

const stateAfter = await dbGetPromptState();
// Get the last 20 prompts
const prompts = await redis.lrange<MultiplayerPrompt>("prompt:stream", 0, -1);

if (prompts.length === 0) {
return NextResponse.json({
success: true,
message: "Queue processed successfully",
queueStats: {
before: queueLengthBefore,
after: stateAfter.promptQueue.length,
processed: Math.max(
0,
queueLengthBefore - stateAfter.promptQueue.length,
),
},
timestamp: new Date().toISOString(),
message: "No prompts found, active prompt cleared.",
});
} catch (error) {
console.error("Error processing queue:", error);

return NextResponse.json(
{
success: false,
message: "Error processing queue",
error: error instanceof Error ? error.message : String(error),
timestamp: new Date().toISOString(),
},
{ status: 500 },
);
}
}

export const dynamic = "force-dynamic";
const currentActiveId = (await redis.get("prompt:active")) as string;

let newActiveId: string;
let newActivePrompt: string;

const currentIndex = prompts.findIndex(p => p.id === currentActiveId);

console.log("currentIndex", currentIndex);

if (currentIndex === -1) {
// Active prompt is NOT in top 20 → reset to latest (index 0)
newActiveId = prompts[0].id;
newActivePrompt = prompts[0].content;
} else if (currentIndex === 0) {
newActiveId = currentActiveId;
newActivePrompt = prompts[currentIndex].content;
} else {
// Rotate to next in the top 20 which is the previous index, add modulo correctly
const nextIndex = (currentIndex - 1) % prompts.length;
newActiveId = prompts[nextIndex].id;
newActivePrompt = prompts[nextIndex].content;
}

console.log("newActiveId", newActiveId);

await applyPromptToStream(newActivePrompt);

await redis.set("prompt:active", newActiveId);

return NextResponse.json({ activePromptId: newActiveId });
}
54 changes: 54 additions & 0 deletions apps/app/app/api/prompts/poll/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { NextResponse } from "next/server";
import { redis } from "@/lib/redis";
import { safePrompts } from "@/lib/nsfwCheck";
import { v4 as uuidv4 } from "uuid";
import type { MultiplayerPrompt } from "@/hooks/usePromptsApi";

async function initializeFewPromptsIfEmpty() {
const randomPrompts = safePrompts.map(prompt => ({
id: uuidv4(),
content: prompt,
created_at: Date.now().toString(),
}));

// Clear existing prompts to ensure clean initialization
await redis.del("prompt:stream");

// Add each prompt to Redis
for (const prompt of randomPrompts) {
await redis.lpush("prompt:stream", JSON.stringify(prompt));
}

await redis.set("prompt:active", randomPrompts[randomPrompts.length - 1].id);

return {
prompts: randomPrompts,
activeIndex: randomPrompts[randomPrompts.length - 1].id,
};
}

export async function GET() {
try {
const prompts = await redis.lrange<MultiplayerPrompt>(
"prompt:stream",
0,
-1,
);

const activeId = await redis.get("prompt:active");

if (prompts.length === 0) {
console.log("Initializing few prompts");
const data = await initializeFewPromptsIfEmpty();
return NextResponse.json(data, { status: 200 });
}

return NextResponse.json({ prompts, activeIndex: activeId ?? -1 });
} catch (error) {
console.error("Error fetching prompts:", error);
return NextResponse.json(
{ error: "Failed to fetch prompts" },
{ status: 500 },
);
}
}
Loading
Loading