This tutorial will guide you through the creation of another real-time full-stack application using Livestack. We will dive into the details of the server, client, and common components to understand how they work together to provide a seamless real-time experience.
As always, you can run the following commands to see how the end result is going to look like
npx create-livestack my-livestack-counter --template typescript-live-counter
cd my-livestack-counter
npm install
npm run dev
Here's a brief overview of our project's structure:
my-livestack-counter/
├── src/
│ ├── server/
│ │ └── index.ts
│ ├── client/
│ │ ├── index.tsx
│ │ └── App.tsx
│ └── common/
│ └── defs.ts
├── package.json
├── tsconfig.json
├── index.html
├── .gitignore
└── vite.config.ts
src/common/defs.ts
We define the structure or schema of the data stream used in our application here. This helps in maintaining consistency between the client and server.
import { z } from "zod";
// Define the input schema for the increment action
export const incrementInput = z.object({ action: z.literal("increment") });
// Define the output schema for the increment result
export const incrementOutput = z.object({ count: z.number() });
// Define a constant for the incrementer job name
export const INCREMENTER = "incrementer";
src/server/index.ts
This is where we set up the backend of our application using Vite-Express and Livestack.
import { LiveEnv, JobSpec } from "@livestack/core";
import express from "express";
import ViteExpress from "vite-express";
import { INCREMENTER, incrementInput, incrementOutput } from "../common/defs";
import { initJobBinding } from "@livestack/gateway";
import bodyParser from "body-parser";
// Create a LiveEnv environment with a specified project ID
const liveEnvP = LiveEnv.create({
projectId: "MY_LIVE_SPEECH_APP",
});
// Define the job specification for the incrementer
const incrementSpec = JobSpec.define({
name: INCREMENTER,
input: incrementInput,
output: incrementOutput,
});
// Define the worker that will process the increment job
const incrementWorker = incrementSpec.defineWorker({
processor: async ({ input, output }) => {
let counter = 0;
for await (const _ of input) {
counter += 1;
await output.emit({
count: counter,
});
}
},
});
async function main() {
// Set the global LiveEnv instance
LiveEnv.setGlobal(liveEnvP);
// Initialize an Express application
const app = express();
app.use(bodyParser.json());
// Define the server port
const PORT = 3000;
// Start the ViteExpress server
const server = ViteExpress.listen(app, PORT, () =>
console.log(`Live counter server listening on http://localhost:${PORT}.`)
);
// Initialize job binding with the server and allowed specs
initJobBinding({
httpServer: server,
allowedSpecsForBinding: [incrementSpec],
});
}
// Start the server if this file is run directly
if (require.main === module) {
main();
}
src/client/index.tsx
This file initializes the React application.
import React from "react";
import ReactDOM from "react-dom/client";
import { App } from "./App";
// Render the App component into the root element
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
src/client/App.tsx
This is where the client-side logic resides. It uses Livestack hooks to interact with the server.
"use client";
import React from "react";
import { useInput, useJobBinding, useOutput } from "@livestack/client";
import { INCREMENTER, incrementInput, incrementOutput } from "../common/defs";
export function App() {
// Bind to the incrementer job
const job = useJobBinding({
specName: INCREMENTER,
});
// Get the latest count from the output
const { last: currCount } = useOutput({
tag: "default",
def: incrementOutput,
job,
});
// Feed increment actions to the input
const { feed } = useInput({ tag: "default", def: incrementInput, job });
return (
<div className="App">
<button onClick={() => feed && feed({ action: "increment" })}>
Click me
</button>
<div>{currCount?.data.count || `No count, click the button!`}</div>
</div>
);
}
In this tutorial, we have built a real-time full-stack application using Livestack. Let's summarize the key components of Livestack that we used and how they fit into our application:
LiveEnv:
JobSpec:
LiveWorker:
initJobBinding:
useJobBinding (Client-side):
useInput (Client-side):
useOutput (Client-side):
By leveraging these Livestack components, we were able to build a robust real-time counter application. Livestack provided the infrastructure for defining, processing, and binding jobs, while ensuring seamless communication between the client and server. This tutorial demonstrated how to:
With these building blocks, you can extend this application to more complex real-time scenarios, ensuring a scalable and maintainable codebase. Happy coding with Livestack!