an experiment by ben davis that went WAY too far...
Warning
As I said, this is alpha software that's gonna change. This is a new version from the original with almost entirely new api's. Expect this to happen again in 0.4.0...
<script lang="ts">
import { myRiverClient } from '$lib/river/client';
// ALL of this is type safe, feels just like TRPC
const { start, stop, status } = myRiverClient.basicExample({
onStart: () => {
console.log('starting basic example');
},
onChunk: (chunk) => {
// full type safety on the chunks
console.log(chunk);
},
onError: (error) => {
console.error(error);
},
onSuccess: () => {
console.log('Success');
},
onCancel: () => {
console.log('Canceled');
},
onStreamInfo: (streamInfo) => {
console.log(streamInfo);
}
});
</script>bun i @davis7dotsh/river-alphathis is alpha software, use it at your own risk. api's will change, bugs will be fixed, features will be added, etc...
- full type safety
- rpc-like function calling
- trpc mutation like interface for consuming the streams
- ai sdk streaming support with full stack type safety
- custom stream support with zod validation on chunks
this project does actually work right now, but it is very early in development and NOT recommended for production use. it is in alpha, the apis will change a lot...
if you want to try this out, it's now available on npm!
here are a couple of examples, they're both are fully type safe, are pleasant to work in, and work great: check them out
- create a new sveltekit project (if you don't have one already)
bunx sv create river-demo- install dependencies
bun i @davis7dotsh/river-alpha zod- setup your first stream
// src/lib/river/streams.ts
import { RIVER_STREAMS } from '@davis7dotsh/river-alpha';
import { z } from 'zod';
export const myFirstNewRiverStream = RIVER_STREAMS.createRiverStream()
.input(
z.object({
yourName: z.string()
})
)
.runner(async (stuff) => {
const { input, initStream, abortSignal } = stuff;
const activeStream = await initStream(
// this is where the type safety happens, the generic type is the chunk type
RIVER_PROVIDERS.defaultRiverStorageProvider<{
isVowel: boolean;
letter: string;
}>()
);
const { yourName } = input;
activeStream.sendData(async ({ appendChunk, close }) => {
const letters = yourName.split('');
const onlyLetters = letters.filter((letter) => letter.match(/[a-zA-Z]/));
for await (const letter of onlyLetters) {
if (abortSignal.aborted) {
break;
}
appendChunk({ isVowel: !!letter.match(/[aeiou]/i), letter });
await new Promise((resolve) => setTimeout(resolve, 100));
}
close();
});
return activeStream;
});- setup your router
// src/lib/river/router.ts
import { RIVER_STREAMS } from '$lib/river/streams.js';
import { myFirstNewRiverStream } from './streams.js';
export const myFirstRiverRouter = RIVER_STREAMS.createRiverRouter({
vowelCounter: myFirstNewRiverStream
});
export type MyFirstRiverRouter = typeof myFirstRiverRouter;- setup the endpoint
// src/routes/api/river/+server.ts
import { RIVER_SERVERS } from '$lib/river/server.js';
import { myFirstRiverRouter } from './router.js';
export const { POST } = RIVER_SERVERS.createSvelteKitEndpointHandler(myFirstRiverRouter);- setup the client
// src/lib/river/client.ts
import { RIVER_CLIENT_SVELTEKIT } from '$lib/index.js';
import type { MyFirstRiverRouter } from './router.js';
export const myFirstRiverClient =
RIVER_CLIENT_SVELTEKIT.createSvelteKitRiverClient<MyFirstRiverRouter>('/examples');- use your agent on the client with a client side caller
<!-- src/routes/+page.svelte -->
<script lang="ts">
import { myFirstRiverClient } from '$lib/river/client.js';
// this works just like mutations in trpc, it will not actually run until you call start
// the callbacks are optional, and will fire when they are defined and the agent starts
const { start, stop, status } = myFirstRiverClient.vowelCounter({
onStart: () => {
console.log('Starting');
},
onChunk: (chunk) => {
console.log(chunk);
},
onError: (error) => {
console.error(error);
},
onSuccess: () => {
console.log('Success');
},
onCancel: () => {
console.log('Canceled');
},
onStreamInfo: (streamInfo) => {
console.log(streamInfo);
}
});
</script>
<!-- some UI to to consume and start the stream -->- streams went from something you touch every once and a while, to something we're using all the time
- i want typesafety
- mutations are awesome in tanstack query, i want them for streams
- rpc >>>>>>
- streams are a pain to consume out of the box (readers and encoders and raw fetch and type casting and more annoying shit)
these are a few helper types that really help with getting good type safety in your clients. the names are a bit verbose, but at least they're descriptive...
// AI SDK SPECIFIC HELPERS (for agents using Vercel AI SDK)
// gets the "tool set" type (a record of tool names to their tool types) for an ai-sdk agent
type AiSdkAgentToolSet = RiverAiSdkToolSet<typeof riverClient.exampleAiSdkAgent>;
// gets the input type for a tool call for an ai-sdk agent. pass in the tool set type and the tool name
type ImposterToolCallInputType = RiverAiSdkToolInputType<AiSdkAgentToolSet, 'imposterCheck'>;
// gets the output type for a tool call for an ai-sdk agent. pass in the tool set type and the tool name
type ImposterToolCallOutputType = RiverAiSdkToolOutputType<AiSdkAgentToolSet, 'imposterCheck'>;
// GENERAL HELPERS (for any agent)
// gets the chunk type for an agent (the thing passed to the onChunk callback)
type AgentChunkType = RiverStreamChunkType<typeof riverClient.exampleAgent>;
// gets the input type for an agent (the thing passed to the start function)
type AgentInputType = RiverStreamInputType<typeof riverClient.exampleAgent>;
// SERVER SIDE HELPERS (for use in your agent definitions)
// infers the chunk type from an AI SDK stream (useful for typing your storage provider)
type AiSdkChunkType = InferAiSdkChunkType<typeof fullStream>;if you have feedback or want to contribute, don't hesitate. best place to reach out is on my twitter @bmdavis419