Progressive response
Progressive Response example
Demonstrates an experimental feature called “Progressive Response” that implemented with JSONLinesResponse
on the server and handled with progressive
function on the client. For demonstational purposes users and tasks resolve in random order, using setTimeout
with a random delay of 0-10 seconds. Press “Load” button to load users and tasks. The response is streamed progressively, so you can see the results as they come in, rather than waiting for the entire response to be ready. Open devtools to see the network requests.
Result
Code
1import { JSONLinesResponse, type VovkIteration } from 'vovk';
2import type ProgressiveController from './ProgressiveController';
3
4export default class ProgressiveService {
5 static async getUsers() {
6 await new Promise((resolve) => setTimeout(resolve, Math.random() * 10_000));
7 return [
8 { id: 1, name: 'John Doe' },
9 { id: 2, name: 'Jane Smith' },
10 { id: 3, name: 'Alice Johnson' },
11 { id: 4, name: 'Bob Brown' },
12 { id: 5, name: 'Charlie White' },
13 ];
14 }
15
16 static async getTasks() {
17 await new Promise((resolve) => setTimeout(resolve, Math.random() * 10_000));
18 return [
19 { id: 1, title: 'Task One', completed: false },
20 { id: 2, title: 'Task Two', completed: true },
21 { id: 3, title: 'Task Three', completed: false },
22 { id: 4, title: 'Task Four', completed: true },
23 { id: 5, title: 'Task Five', completed: false },
24 ];
25 }
26
27 static streamProgressiveResponse(
28 resp: JSONLinesResponse<VovkIteration<typeof ProgressiveController.streamProgressiveResponse>>
29 ) {
30 Promise.all([
31 this.getUsers().then((users) => resp.send({ users })),
32 this.getTasks().then((tasks) => resp.send({ tasks })),
33 ])
34 .then(resp.close)
35 .catch(resp.throw);
36 }
37}
1import { JSONLinesResponse, type VovkIteration } from 'vovk';
2import type ProgressiveController from './ProgressiveController';
3
4export default class ProgressiveService {
5 static async getUsers() {
6 await new Promise((resolve) => setTimeout(resolve, Math.random() * 10_000));
7 return [
8 { id: 1, name: 'John Doe' },
9 { id: 2, name: 'Jane Smith' },
10 { id: 3, name: 'Alice Johnson' },
11 { id: 4, name: 'Bob Brown' },
12 { id: 5, name: 'Charlie White' },
13 ];
14 }
15
16 static async getTasks() {
17 await new Promise((resolve) => setTimeout(resolve, Math.random() * 10_000));
18 return [
19 { id: 1, title: 'Task One', completed: false },
20 { id: 2, title: 'Task Two', completed: true },
21 { id: 3, title: 'Task Three', completed: false },
22 { id: 4, title: 'Task Four', completed: true },
23 { id: 5, title: 'Task Five', completed: false },
24 ];
25 }
26
27 static streamProgressiveResponse(
28 resp: JSONLinesResponse<VovkIteration<typeof ProgressiveController.streamProgressiveResponse>>
29 ) {
30 Promise.all([
31 this.getUsers().then((users) => resp.send({ users })),
32 this.getTasks().then((tasks) => resp.send({ tasks })),
33 ])
34 .then(resp.close)
35 .catch(resp.throw);
36 }
37}
1import { JSONLinesResponse, type VovkIteration } from 'vovk';
2import type ProgressiveController from './ProgressiveController';
3
4export default class ProgressiveService {
5 static async getUsers() {
6 await new Promise((resolve) => setTimeout(resolve, Math.random() * 10_000));
7 return [
8 { id: 1, name: 'John Doe' },
9 { id: 2, name: 'Jane Smith' },
10 { id: 3, name: 'Alice Johnson' },
11 { id: 4, name: 'Bob Brown' },
12 { id: 5, name: 'Charlie White' },
13 ];
14 }
15
16 static async getTasks() {
17 await new Promise((resolve) => setTimeout(resolve, Math.random() * 10_000));
18 return [
19 { id: 1, title: 'Task One', completed: false },
20 { id: 2, title: 'Task Two', completed: true },
21 { id: 3, title: 'Task Three', completed: false },
22 { id: 4, title: 'Task Four', completed: true },
23 { id: 5, title: 'Task Five', completed: false },
24 ];
25 }
26
27 static streamProgressiveResponse(
28 resp: JSONLinesResponse<VovkIteration<typeof ProgressiveController.streamProgressiveResponse>>
29 ) {
30 Promise.all([
31 this.getUsers().then((users) => resp.send({ users })),
32 this.getTasks().then((tasks) => resp.send({ tasks })),
33 ])
34 .then(resp.close)
35 .catch(resp.throw);
36 }
37}
1import { JSONLinesResponse, type VovkIteration } from 'vovk';
2import type ProgressiveController from './ProgressiveController';
3
4export default class ProgressiveService {
5 static async getUsers() {
6 await new Promise((resolve) => setTimeout(resolve, Math.random() * 10_000));
7 return [
8 { id: 1, name: 'John Doe' },
9 { id: 2, name: 'Jane Smith' },
10 { id: 3, name: 'Alice Johnson' },
11 { id: 4, name: 'Bob Brown' },
12 { id: 5, name: 'Charlie White' },
13 ];
14 }
15
16 static async getTasks() {
17 await new Promise((resolve) => setTimeout(resolve, Math.random() * 10_000));
18 return [
19 { id: 1, title: 'Task One', completed: false },
20 { id: 2, title: 'Task Two', completed: true },
21 { id: 3, title: 'Task Three', completed: false },
22 { id: 4, title: 'Task Four', completed: true },
23 { id: 5, title: 'Task Five', completed: false },
24 ];
25 }
26
27 static streamProgressiveResponse(
28 resp: JSONLinesResponse<VovkIteration<typeof ProgressiveController.streamProgressiveResponse>>
29 ) {
30 Promise.all([
31 this.getUsers().then((users) => resp.send({ users })),
32 this.getTasks().then((tasks) => resp.send({ tasks })),
33 ])
34 .then(resp.close)
35 .catch(resp.throw);
36 }
37}
1import { get, JSONLinesResponse, prefix, type VovkIteration } from 'vovk';
2import { withZod } from 'vovk-zod';
3import { z } from 'zod/v4';
4import ProgressiveService from './ProgressiveService';
5
6@prefix('progressive')
7export default class ProgressiveController {
8 @get()
9 static streamProgressiveResponse = withZod({
10 validateEachIteration: true,
11 iteration: z.union([
12 z.strictObject({
13 users: z.array(
14 z.strictObject({
15 id: z.number(),
16 name: z.string(),
17 })
18 ),
19 }),
20 z.strictObject({
21 tasks: z.array(
22 z.strictObject({
23 id: z.number(),
24 title: z.string(),
25 completed: z.boolean(),
26 })
27 ),
28 }),
29 ]),
30 async handle(req) {
31 const response = new JSONLinesResponse<VovkIteration<typeof ProgressiveController.streamProgressiveResponse>>(
32 req
33 );
34
35 void ProgressiveService.streamProgressiveResponse(response);
36
37 return response;
38 },
39 });
40}
1import { get, JSONLinesResponse, prefix, type VovkIteration } from 'vovk';
2import { withZod } from 'vovk-zod';
3import { z } from 'zod/v4';
4import ProgressiveService from './ProgressiveService';
5
6@prefix('progressive')
7export default class ProgressiveController {
8 @get()
9 static streamProgressiveResponse = withZod({
10 validateEachIteration: true,
11 iteration: z.union([
12 z.strictObject({
13 users: z.array(
14 z.strictObject({
15 id: z.number(),
16 name: z.string(),
17 })
18 ),
19 }),
20 z.strictObject({
21 tasks: z.array(
22 z.strictObject({
23 id: z.number(),
24 title: z.string(),
25 completed: z.boolean(),
26 })
27 ),
28 }),
29 ]),
30 async handle(req) {
31 const response = new JSONLinesResponse<VovkIteration<typeof ProgressiveController.streamProgressiveResponse>>(
32 req
33 );
34
35 void ProgressiveService.streamProgressiveResponse(response);
36
37 return response;
38 },
39 });
40}
1import { get, JSONLinesResponse, prefix, type VovkIteration } from 'vovk';
2import { withZod } from 'vovk-zod';
3import { z } from 'zod/v4';
4import ProgressiveService from './ProgressiveService';
5
6@prefix('progressive')
7export default class ProgressiveController {
8 @get()
9 static streamProgressiveResponse = withZod({
10 validateEachIteration: true,
11 iteration: z.union([
12 z.strictObject({
13 users: z.array(
14 z.strictObject({
15 id: z.number(),
16 name: z.string(),
17 })
18 ),
19 }),
20 z.strictObject({
21 tasks: z.array(
22 z.strictObject({
23 id: z.number(),
24 title: z.string(),
25 completed: z.boolean(),
26 })
27 ),
28 }),
29 ]),
30 async handle(req) {
31 const response = new JSONLinesResponse<VovkIteration<typeof ProgressiveController.streamProgressiveResponse>>(
32 req
33 );
34
35 void ProgressiveService.streamProgressiveResponse(response);
36
37 return response;
38 },
39 });
40}
1import { get, JSONLinesResponse, prefix, type VovkIteration } from 'vovk';
2import { withZod } from 'vovk-zod';
3import { z } from 'zod/v4';
4import ProgressiveService from './ProgressiveService';
5
6@prefix('progressive')
7export default class ProgressiveController {
8 @get()
9 static streamProgressiveResponse = withZod({
10 validateEachIteration: true,
11 iteration: z.union([
12 z.strictObject({
13 users: z.array(
14 z.strictObject({
15 id: z.number(),
16 name: z.string(),
17 })
18 ),
19 }),
20 z.strictObject({
21 tasks: z.array(
22 z.strictObject({
23 id: z.number(),
24 title: z.string(),
25 completed: z.boolean(),
26 })
27 ),
28 }),
29 ]),
30 async handle(req) {
31 const response = new JSONLinesResponse<VovkIteration<typeof ProgressiveController.streamProgressiveResponse>>(
32 req
33 );
34
35 void ProgressiveService.streamProgressiveResponse(response);
36
37 return response;
38 },
39 });
40}
1'use client';
2import { useState } from 'react';
3import { progressive, VovkIteration } from 'vovk';
4import { ProgressiveRPC } from 'vovk-client';
5import Skeleton from 'react-loading-skeleton';
6import 'react-loading-skeleton/dist/skeleton.css';
7
8export default function ProgressiveExample() {
9 type Iteration = VovkIteration<typeof ProgressiveRPC.streamProgressiveResponse>;
10 const [users, setUsers] = useState<Extract<Iteration, { users: unknown }>['users'] | null>(null);
11 const [tasks, setTasks] = useState<Extract<Iteration, { tasks: unknown }>['tasks'] | null>(null);
12 const [isLoaded, setIsLoaded] = useState<boolean | null>(null);
13
14 const load = () => {
15 setUsers(null);
16 setTasks(null);
17 const { users: usersPromise, tasks: tasksPromise } = progressive(ProgressiveRPC.streamProgressiveResponse);
18 setIsLoaded(false);
19 Promise.all([usersPromise.then(setUsers), tasksPromise.then(setTasks)]).finally(() => setIsLoaded(true));
20 };
21
22 return (
23 <>
24 <button onClick={load} disabled={isLoaded === false}>
25 Load
26 </button>
27 {isLoaded !== null && <h2>Users</h2>}
28 {isLoaded !== null && !users && <ListSkeleton />}
29 {!!users?.length &&
30 users?.map((user) => (
31 <ul key={user.id}>
32 <li>{user.name}</li>
33 </ul>
34 ))}
35
36 {isLoaded !== null && <h2>Tasks</h2>}
37 {isLoaded !== null && !tasks && <ListSkeleton />}
38 {!!tasks?.length &&
39 tasks?.map((task) => (
40 <ul key={task.id}>
41 <li>
42 {task.title} {task.completed ? '✅' : ''}
43 </li>
44 </ul>
45 ))}
46 </>
47 );
48}
49
50const ListSkeleton = () => (
51 <ul>
52 {Array(5)
53 .fill(0)
54 .map((_, index) => (
55 <li key={index}>
56 <Skeleton width={150} />
57 </li>
58 ))}
59 </ul>
60);
1'use client';
2import { useState } from 'react';
3import { progressive, VovkIteration } from 'vovk';
4import { ProgressiveRPC } from 'vovk-client';
5import Skeleton from 'react-loading-skeleton';
6import 'react-loading-skeleton/dist/skeleton.css';
7
8export default function ProgressiveExample() {
9 type Iteration = VovkIteration<typeof ProgressiveRPC.streamProgressiveResponse>;
10 const [users, setUsers] = useState<Extract<Iteration, { users: unknown }>['users'] | null>(null);
11 const [tasks, setTasks] = useState<Extract<Iteration, { tasks: unknown }>['tasks'] | null>(null);
12 const [isLoaded, setIsLoaded] = useState<boolean | null>(null);
13
14 const load = () => {
15 setUsers(null);
16 setTasks(null);
17 const { users: usersPromise, tasks: tasksPromise } = progressive(ProgressiveRPC.streamProgressiveResponse);
18 setIsLoaded(false);
19 Promise.all([usersPromise.then(setUsers), tasksPromise.then(setTasks)]).finally(() => setIsLoaded(true));
20 };
21
22 return (
23 <>
24 <button onClick={load} disabled={isLoaded === false}>
25 Load
26 </button>
27 {isLoaded !== null && <h2>Users</h2>}
28 {isLoaded !== null && !users && <ListSkeleton />}
29 {!!users?.length &&
30 users?.map((user) => (
31 <ul key={user.id}>
32 <li>{user.name}</li>
33 </ul>
34 ))}
35
36 {isLoaded !== null && <h2>Tasks</h2>}
37 {isLoaded !== null && !tasks && <ListSkeleton />}
38 {!!tasks?.length &&
39 tasks?.map((task) => (
40 <ul key={task.id}>
41 <li>
42 {task.title} {task.completed ? '✅' : ''}
43 </li>
44 </ul>
45 ))}
46 </>
47 );
48}
49
50const ListSkeleton = () => (
51 <ul>
52 {Array(5)
53 .fill(0)
54 .map((_, index) => (
55 <li key={index}>
56 <Skeleton width={150} />
57 </li>
58 ))}
59 </ul>
60);
1'use client';
2import { useState } from 'react';
3import { progressive, VovkIteration } from 'vovk';
4import { ProgressiveRPC } from 'vovk-client';
5import Skeleton from 'react-loading-skeleton';
6import 'react-loading-skeleton/dist/skeleton.css';
7
8export default function ProgressiveExample() {
9 type Iteration = VovkIteration<typeof ProgressiveRPC.streamProgressiveResponse>;
10 const [users, setUsers] = useState<Extract<Iteration, { users: unknown }>['users'] | null>(null);
11 const [tasks, setTasks] = useState<Extract<Iteration, { tasks: unknown }>['tasks'] | null>(null);
12 const [isLoaded, setIsLoaded] = useState<boolean | null>(null);
13
14 const load = () => {
15 setUsers(null);
16 setTasks(null);
17 const { users: usersPromise, tasks: tasksPromise } = progressive(ProgressiveRPC.streamProgressiveResponse);
18 setIsLoaded(false);
19 Promise.all([usersPromise.then(setUsers), tasksPromise.then(setTasks)]).finally(() => setIsLoaded(true));
20 };
21
22 return (
23 <>
24 <button onClick={load} disabled={isLoaded === false}>
25 Load
26 </button>
27 {isLoaded !== null && <h2>Users</h2>}
28 {isLoaded !== null && !users && <ListSkeleton />}
29 {!!users?.length &&
30 users?.map((user) => (
31 <ul key={user.id}>
32 <li>{user.name}</li>
33 </ul>
34 ))}
35
36 {isLoaded !== null && <h2>Tasks</h2>}
37 {isLoaded !== null && !tasks && <ListSkeleton />}
38 {!!tasks?.length &&
39 tasks?.map((task) => (
40 <ul key={task.id}>
41 <li>
42 {task.title} {task.completed ? '✅' : ''}
43 </li>
44 </ul>
45 ))}
46 </>
47 );
48}
49
50const ListSkeleton = () => (
51 <ul>
52 {Array(5)
53 .fill(0)
54 .map((_, index) => (
55 <li key={index}>
56 <Skeleton width={150} />
57 </li>
58 ))}
59 </ul>
60);
1'use client';
2import { useState } from 'react';
3import { progressive, VovkIteration } from 'vovk';
4import { ProgressiveRPC } from 'vovk-client';
5import Skeleton from 'react-loading-skeleton';
6import 'react-loading-skeleton/dist/skeleton.css';
7
8export default function ProgressiveExample() {
9 type Iteration = VovkIteration<typeof ProgressiveRPC.streamProgressiveResponse>;
10 const [users, setUsers] = useState<Extract<Iteration, { users: unknown }>['users'] | null>(null);
11 const [tasks, setTasks] = useState<Extract<Iteration, { tasks: unknown }>['tasks'] | null>(null);
12 const [isLoaded, setIsLoaded] = useState<boolean | null>(null);
13
14 const load = () => {
15 setUsers(null);
16 setTasks(null);
17 const { users: usersPromise, tasks: tasksPromise } = progressive(ProgressiveRPC.streamProgressiveResponse);
18 setIsLoaded(false);
19 Promise.all([usersPromise.then(setUsers), tasksPromise.then(setTasks)]).finally(() => setIsLoaded(true));
20 };
21
22 return (
23 <>
24 <button onClick={load} disabled={isLoaded === false}>
25 Load
26 </button>
27 {isLoaded !== null && <h2>Users</h2>}
28 {isLoaded !== null && !users && <ListSkeleton />}
29 {!!users?.length &&
30 users?.map((user) => (
31 <ul key={user.id}>
32 <li>{user.name}</li>
33 </ul>
34 ))}
35
36 {isLoaded !== null && <h2>Tasks</h2>}
37 {isLoaded !== null && !tasks && <ListSkeleton />}
38 {!!tasks?.length &&
39 tasks?.map((task) => (
40 <ul key={task.id}>
41 <li>
42 {task.title} {task.completed ? '✅' : ''}
43 </li>
44 </ul>
45 ))}
46 </>
47 );
48}
49
50const ListSkeleton = () => (
51 <ul>
52 {Array(5)
53 .fill(0)
54 .map((_, index) => (
55 <li key={index}>
56 <Skeleton width={150} />
57 </li>
58 ))}
59 </ul>
60);
Last updated on