{"id":18562466,"url":"https://github.com/replit/river","last_synced_at":"2026-03-09T01:01:08.764Z","repository":{"id":214558649,"uuid":"689102279","full_name":"replit/river","owner":"replit","description":"🌊 long-lived streaming RPC framework","archived":false,"fork":false,"pushed_at":"2026-03-03T03:24:11.000Z","size":2126,"stargazers_count":92,"open_issues_count":8,"forks_count":10,"subscribers_count":21,"default_branch":"main","last_synced_at":"2026-03-03T04:45:41.045Z","etag":null,"topics":["api","rpc-framework"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/@replit/river","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/replit.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2023-09-08T20:04:13.000Z","updated_at":"2026-02-25T02:00:20.000Z","dependencies_parsed_at":"2024-01-26T21:46:53.335Z","dependency_job_id":"1ceb9122-4c86-4d3f-8ae5-99f21061ccd2","html_url":"https://github.com/replit/river","commit_stats":null,"previous_names":["replit/river"],"tags_count":188,"template":false,"template_full_name":null,"purl":"pkg:github/replit/river","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/replit%2Friver","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/replit%2Friver/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/replit%2Friver/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/replit%2Friver/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/replit","download_url":"https://codeload.github.com/replit/river/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/replit%2Friver/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30279764,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-08T20:45:49.896Z","status":"ssl_error","status_checked_at":"2026-03-08T20:45:49.525Z","response_time":56,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api","rpc-framework"],"created_at":"2024-11-06T22:09:50.628Z","updated_at":"2026-03-09T01:01:08.749Z","avatar_url":"https://github.com/replit.png","language":"TypeScript","readme":"# River\n\n## Long-lived streaming remote procedure calls\n\nRiver provides a framework for long-lived streaming Remote Procedure Calls (RPCs) in modern web applications, featuring advanced error handling and customizable retry policies to ensure seamless communication between clients and servers.\n\nRiver provides a framework similar to [tRPC](https://trpc.io/) and [gRPC](https://grpc.io/) but with additional features:\n\n- JSON Schema Support + run-time schema validation\n- full-duplex streaming\n- service multiplexing\n- result types and error handling\n- snappy DX (no code generation)\n- transparent reconnect support for long-lived sessions\n- over any transport (WebSockets out of the box)\n- full OpenTelemetry integration (distributed tracing for connections, sessions, procedure calls)\n\nSee [PROTOCOL.md](./PROTOCOL.md) for more information on the protocol.\n\n### Prerequisites\n\nBefore proceeding, ensure you have TypeScript 5 installed and configured appropriately:\n\n1. **Ensure your `tsconfig.json` is configured correctly**:\n\n   You must verify that:\n\n   - `compilerOptions.moduleResolution` is set to `\"bundler\"`\n   - `compilerOptions.strict` is set to true (or at least `compilerOptions.strictFunctionTypes` and `compilerOptions.strictNullChecks`)\n\n   Like so:\n\n   ```jsonc\n   {\n     \"compilerOptions\": {\n       \"moduleResolution\": \"bundler\",\n       \"strict\": true\n       // Other compiler options...\n     }\n   }\n   ```\n\n   If these options already exist in your `tsconfig.json` and don't match what is shown above, modify them. Failing to set these will cause unresolvable type errors when defining services.\n\n2. Install River and Dependencies:\n\n   To use River, install the required packages using npm:\n\n   ```bash\n   npm i @replit/river @sinclair/typebox\n   ```\n\n## Writing services\n\n### Concepts\n\n- Router: a collection of services, namespaced by service name.\n- Service: a collection of procedures with a shared state.\n- Procedure: a single procedure. A procedure declares its type, a request data type, a response data type, optionally a response error type, and the associated handler. Valid types are:\n  - `rpc`, single request, single response\n  - `upload`, multiple requests, single response\n  - `subscription`, single request, multiple responses\n  - `stream`, multiple requests, multiple response\n- Transport: manages the lifecycle (creation/deletion) of connections and multiplexing read/writes from clients. Both the client and the server must be passed in a subclass of `Transport` to work.\n  - Connection: the actual raw underlying transport connection\n  - Session: a higher-level abstraction that operates over the span of potentially multiple transport-level connections\n- Codec: encodes messages between clients/servers before the transport sends it across the wire.\n\n### A basic router\n\nFirst, we create a service:\n\n```ts\nimport { createServiceSchema, Procedure, Ok } from '@replit/river';\nimport { Type } from '@sinclair/typebox';\n\nconst ServiceSchema = createServiceSchema();\nexport const ExampleService = ServiceSchema.define(\n  // optional configuration parameter\n  {\n    // initializer for shared state\n    initializeState: () =\u003e ({ count: 0 }),\n  },\n  // procedures\n  {\n    add: Procedure.rpc({\n      // input type\n      requestInit: Type.Object({ n: Type.Number() }),\n      // response data type\n      responseData: Type.Object({ result: Type.Number() }),\n      // any error results (other than the uncaught) that this procedure can return\n      responseError: Type.Never(),\n      // note that a handler is unique per user\n      async handler({ ctx, reqInit: { n } }) {\n        // access and mutate shared state\n        ctx.state.count += n;\n        return Ok({ result: ctx.state.count });\n      },\n    }),\n  },\n);\n```\n\nThen, we create the server:\n\n```ts\nimport http from 'http';\nimport { WebSocketServer } from 'ws';\nimport { WebSocketServerTransport } from '@replit/river/transport/ws/server';\nimport { createServer } from '@replit/river';\n\n// start websocket server on port 3000\nconst httpServer = http.createServer();\nconst port = 3000;\nconst wss = new WebSocketServer({ server: httpServer });\nconst transport = new WebSocketServerTransport(wss, 'SERVER');\n\nconst services = {\n  example: ExampleService,\n};\n\nexport type ServiceSurface = typeof services;\n\nconst server = createServer(transport, services);\n\nhttpServer.listen(port);\n```\n\nIn another file for the client (to create a separate entrypoint),\n\n```ts\nimport { WebSocketClientTransport } from '@replit/river/transport/ws/client';\nimport { createClient } from '@replit/river';\nimport { WebSocket } from 'ws';\nimport type { ServiceSurface } from './server';\n//     ^ type only import to avoid bundling the server!\n\nconst transport = new WebSocketClientTransport(\n  async () =\u003e new WebSocket('ws://localhost:3000'),\n  'my-client-id',\n);\n\nconst client = createClient\u003cServiceSurface\u003e(\n  transport,\n  'SERVER', // transport id of the server in the previous step\n  { eagerlyConnect: true }, // whether to eagerly connect to the server on creation (optional argument)\n);\n\n// we get full type safety on `client`\n// client.\u003cservice name\u003e.\u003cprocedure name\u003e.\u003cprocedure type\u003e()\n// e.g.\nconst result = await client.example.add.rpc({ n: 3 });\nif (result.ok) {\n  const msg = result.payload;\n  console.log(msg.result); // 0 + 3 = 3\n}\n```\n\n### Error Handling\n\nRiver uses a Result pattern for error handling. All procedure responses are wrapped in `Ok()` for success or `Err()` for errors:\n\n```ts\nimport { Ok, Err } from '@replit/river';\n\n// success\nreturn Ok({ result: 42 });\n\n// error\nreturn Err({ code: 'INVALID_INPUT', message: 'Value must be positive' });\n```\n\n#### Custom Error Types\n\nYou can define custom error schemas for your procedures:\n\n```ts\nconst MathService = ServiceSchema.define({\n  divide: Procedure.rpc({\n    requestInit: Type.Object({ a: Type.Number(), b: Type.Number() }),\n    responseData: Type.Object({ result: Type.Number() }),\n    responseError: Type.Union([\n      Type.Object({\n        code: Type.Literal('DIVISION_BY_ZERO'),\n        message: Type.String(),\n        extras: Type.Object({ dividend: Type.Number() }),\n      }),\n      Type.Object({\n        code: Type.Literal('INVALID_INPUT'),\n        message: Type.String(),\n      }),\n    ]),\n    async handler({ reqInit: { a, b } }) {\n      if (b === 0) {\n        return Err({\n          code: 'DIVISION_BY_ZERO',\n          message: 'Cannot divide by zero',\n          extras: { dividend: a },\n        });\n      }\n\n      if (!Number.isFinite(a) || !Number.isFinite(b)) {\n        return Err({\n          code: 'INVALID_INPUT',\n          message: 'Inputs must be finite numbers',\n        });\n      }\n\n      return Ok({ result: a / b });\n    },\n  }),\n});\n```\n\n#### Uncaught Errors\n\nWhen a procedure handler throws an uncaught error, River automatically handles it:\n\n```ts\nconst ExampleService = ServiceSchema.define({\n  maybeThrow: Procedure.rpc({\n    requestInit: Type.Object({ shouldThrow: Type.Boolean() }),\n    responseData: Type.Object({ result: Type.String() }),\n    async handler({ reqInit: { shouldThrow } }) {\n      if (shouldThrow) {\n        throw new Error('Something went wrong!');\n      }\n\n      return Ok({ result: 'success' });\n    },\n  }),\n});\n\n// client will receive an error with code 'UNCAUGHT_ERROR'\nconst result = await client.example.maybeThrow.rpc({ shouldThrow: true });\nif (!result.ok \u0026\u0026 result.payload.code === 'UNCAUGHT_ERROR') {\n  console.log('Handler threw an error:', result.payload.message);\n}\n```\n\n### Logging\n\nTo add logging, you can bind a logging function to a transport.\n\n```ts\nimport { coloredStringLogger } from '@replit/river/logging';\n\nconst transport = new WebSocketClientTransport(\n  async () =\u003e new WebSocket('ws://localhost:3000'),\n  'my-client-id',\n);\n\ntransport.bindLogger(console.log);\n// or\ntransport.bindLogger(coloredStringLogger);\n```\n\nYou can define your own logging functions that satisfy the `LogFn` type.\n\n### Connection status\n\nRiver defines two types of reconnects:\n\n1. **Transparent reconnects:** These occur when the connection is temporarily lost and reestablished without losing any messages. From the application's perspective, this process is seamless and does not disrupt ongoing operations.\n2. **Hard reconnect:** This occurs when all server state is lost, requiring the client to reinitialize anything stateful (e.g. subscriptions).\n\nHard reconnects are signaled via `sessionStatus` events.\n\nIf your application is stateful on either the server or the client, the service consumer _should_ wrap all the client-side setup with `transport.addEventListener('sessionStatus', (evt) =\u003e ...)` to do appropriate setup and teardown.\n\n```ts\ntransport.addEventListener('sessionStatus', (evt) =\u003e {\n  if (evt.status === 'created') {\n    // do something\n  } else if (evt.status === 'closing') {\n    // do other things\n  } else if (evt.status === 'closed') {\n    // note that evt.session only has id + to\n    // this is useful for doing things like creating a new session if\n    // a session just got yanked\n  }\n});\n\n// or, listen for specific session states\ntransport.addEventListener('sessionTransition', (evt) =\u003e {\n  if (evt.state === SessionState.Connected) {\n    // switch on various transition states\n  } else if (evt.state === SessionState.NoConnection) {\n    // do something\n  }\n});\n```\n\n### Advanced Patterns\n\n#### All Procedure Types\n\nRiver supports four types of procedures, each with different message patterns:\n\n##### Unary RPC Procedures (1:1)\n\nSingle request, single response:\n\n```ts\nconst ExampleService = ServiceSchema.define({\n  add: Procedure.rpc({\n    requestInit: Type.Object({ a: Type.Number(), b: Type.Number() }),\n    responseData: Type.Object({ result: Type.Number() }),\n    async handler({ reqInit: { a, b } }) {\n      return Ok({ result: a + b });\n    },\n  }),\n});\n\n// client usage\nconst result = await client.example.add.rpc({ a: 1, b: 2 });\nif (result.ok) {\n  console.log(result.payload.result); // 3\n}\n```\n\n##### Upload Procedures (n:1)\n\nMultiple requests, single response:\n\n```ts\nconst ExampleService = ServiceSchema.define({\n  sum: Procedure.upload({\n    requestInit: Type.Object({ multiplier: Type.Number() }),\n    requestData: Type.Object({ value: Type.Number() }),\n    responseData: Type.Object({ total: Type.Number() }),\n    responseError: Type.Object({\n      code: Type.Literal('INVALID_INPUT'),\n      message: Type.String(),\n    }),\n    async handler({ ctx, reqInit, reqReadable }) {\n      let sum = 0;\n      for await (const msg of reqReadable) {\n        if (!msg.ok) {\n          return ctx.cancel('client disconnected');\n        }\n\n        sum += msg.payload.value;\n      }\n      return Ok({ total: sum * reqInit.multiplier });\n    },\n  }),\n});\n\n// client usage\nconst { reqWritable, finalize } = client.example.sum.upload({ multiplier: 2 });\nreqWritable.write({ value: 1 });\nreqWritable.write({ value: 2 });\nreqWritable.write({ value: 3 });\n\nconst result = await finalize();\nif (result.ok) {\n  console.log(result.payload.total); // 12 (6 * 2)\n} else {\n  console.error('Upload failed:', result.payload.message);\n}\n```\n\n##### Subscription Procedures (1:n)\n\nSingle request, multiple responses:\n\n```ts\nconst ExampleService = ServiceSchema.define(\n  { initializeState: () =\u003e ({ count: 0 }) },\n  {\n    counter: Procedure.subscription({\n      requestInit: Type.Object({ interval: Type.Number() }),\n      responseData: Type.Object({ count: Type.Number() }),\n      async handler({ ctx, reqInit, resWritable }) {\n        const intervalId = setInterval(() =\u003e {\n          ctx.state.count++;\n          resWritable.write(Ok({ count: ctx.state.count }));\n        }, reqInit.interval);\n\n        ctx.signal.addEventListener('abort', () =\u003e {\n          clearInterval(intervalId);\n        });\n      },\n    }),\n  },\n);\n\n// client usage\nconst { resReadable } = client.example.counter.subscribe({ interval: 1000 });\nfor await (const msg of resReadable) {\n  if (msg.ok) {\n    console.log('Count:', msg.payload.count);\n  } else {\n    console.error('Subscription error:', msg.payload.message);\n    break; // exit on error for subscriptions\n  }\n}\n```\n\n##### Stream Procedures (n:n)\n\nMultiple requests, multiple responses:\n\n```ts\nconst ExampleService = ServiceSchema.define({\n  echo: Procedure.stream({\n    requestInit: Type.Object({ prefix: Type.String() }),\n    requestData: Type.Object({ message: Type.String() }),\n    responseData: Type.Object({ echo: Type.String() }),\n    async handler({ reqInit, reqReadable, resWritable, ctx }) {\n      for await (const msg of reqReadable) {\n        if (!msg.ok) {\n          return;\n        }\n\n        const { message } = msg.payload;\n        resWritable.write(\n          Ok({\n            echo: `${reqInit.prefix}: ${message}`,\n          }),\n        );\n      }\n\n      // client ended their side, we can close ours\n      resWritable.close();\n    },\n  }),\n});\n\n// client usage\nconst { reqWritable, resReadable } = client.example.echo.stream({\n  prefix: 'Server',\n});\n\n// send messages\nreqWritable.write({ message: 'Hello' });\nreqWritable.write({ message: 'World' });\nreqWritable.close();\n\n// read responses\nfor await (const msg of resReadable) {\n  if (msg.ok) {\n    console.log(msg.payload.echo); // \"Server: Hello\", \"Server: World\"\n  } else {\n    console.error('Stream error:', msg.payload.message);\n  }\n}\n```\n\n#### Client Cancellation\n\nRiver supports client-side cancellation using AbortController. All procedure calls accept an optional `signal` parameter:\n\n```ts\nconst controller = new AbortController();\nconst rpcResult = client.example.longRunning.rpc(\n  { data: 'hello world' },\n  { signal: controller.signal },\n);\n\n// cancel the operation\ncontroller.abort();\n\n// all cancelled operations will receive an error with CANCEL_CODE\nconst result = await rpcResult;\nif (!result.ok \u0026\u0026 result.payload.code === 'CANCEL_CODE') {\n  console.log('Operation was cancelled');\n}\n```\n\nWhen a client cancels an operation, the server handler receives the cancellation via the `ctx.signal`:\n\n```ts\nconst ExampleService = ServiceSchema.define({\n  longRunning: Procedure.rpc({\n    requestInit: Type.Object({}),\n    responseData: Type.Object({ result: Type.String() }),\n    async handler({ ctx }) {\n      ctx.signal.addEventListener('abort', () =\u003e {\n        // do something\n      });\n\n      // long running operation\n      await new Promise((resolve) =\u003e setTimeout(resolve, 10000));\n      return Ok({ result: 'completed' });\n    },\n  }),\n\n  streamingExample: Procedure.stream({\n    requestInit: Type.Object({}),\n    requestData: Type.Object({ message: Type.String() }),\n    responseData: Type.Object({ echo: Type.String() }),\n    async handler({ ctx, reqReadable, resWritable }) {\n      // for streams, cancellation closes both readable and writable\n      // in addition to triggering the abort signal.\n      for await (const msg of reqReadable) {\n        if (!msg.ok) {\n          // msg.payload.code === CANCEL_CODE error if client cancelled\n          break;\n        }\n\n        resWritable.write(Ok({ echo: msg.payload.message }));\n      }\n\n      resWritable.close();\n    },\n  }),\n});\n```\n\nWorth noting that the `ctx.signal` is triggered regardless of the reason the procedure has ended.\n\n#### Codecs\n\nRiver provides two built-in codecs:\n\n- `NaiveJsonCodec`: Simple JSON serialization\n- `BinaryCodec`: Efficient msgpack serialization (recommended for production)\n\n```ts\nimport { BinaryCodec, NaiveJsonCodec } from '@replit/river/codec';\n\n// use binary codec for better performance\nconst transport = new WebSocketClientTransport(\n  async () =\u003e new WebSocket('ws://localhost:3000'),\n  'my-client-id',\n  { codec: BinaryCodec },\n);\n```\n\nYou can also create custom codecs for message serialization:\n\n```ts\nimport { Codec } from '@replit/river/codec';\n\nclass CustomCodec implements Codec {\n  toBuffer(obj: object): Uint8Array {\n    // custom serialization logic\n  }\n\n  fromBuffer(buf: Uint8Array): object {\n    // custom deserialization logic\n  }\n}\n\n// use with transports\nconst transport = new WebSocketClientTransport(\n  async () =\u003e new WebSocket('ws://localhost:3000'),\n  'my-client-id',\n  { codec: new CustomCodec() },\n);\n```\n\n#### Custom Transports\n\nYou can implement custom transports by extending the base Transport classes:\n\n```ts\nimport { ClientTransport, ServerTransport } from '@replit/river/transport';\nimport { Connection } from '@replit/river/transport';\n\n// custom connection implementation\nclass MyCustomConnection extends Connection {\n  private socket: MyCustomSocket;\n\n  constructor(socket: MyCustomSocket) {\n    super();\n    this.socket = socket;\n\n    this.socket.onMessage = (data: Uint8Array) =\u003e {\n      this.dataListener?.(data);\n    };\n\n    this.socket.onClose = () =\u003e {\n      this.closeListener?.();\n    };\n\n    this.socket.onError = (err: Error) =\u003e {\n      this.errorListener?.(err);\n    };\n  }\n\n  send(msg: Uint8Array): boolean {\n    return this.socket.send(msg);\n  }\n\n  close(): void {\n    this.socket.close();\n  }\n}\n\n// custom client transport\nclass MyCustomClientTransport extends ClientTransport\u003cMyCustomConnection\u003e {\n  constructor(\n    private connectFn: () =\u003e Promise\u003cMyCustomSocket\u003e,\n    clientId: string,\n  ) {\n    super(clientId);\n  }\n\n  async createNewOutgoingConnection(): Promise\u003cMyCustomConnection\u003e {\n    const socket = await this.connectFn();\n    return new MyCustomConnection(socket);\n  }\n}\n\n// custom server transport\nclass MyCustomServerTransport extends ServerTransport\u003cMyCustomConnection\u003e {\n  constructor(\n    private server: MyCustomServer,\n    clientId: string,\n  ) {\n    super(clientId);\n\n    server.onConnection = (socket: MyCustomSocket) =\u003e {\n      const connection = new MyCustomConnection(socket);\n      this.handleConnection(connection);\n    };\n  }\n}\n\n// usage\nconst clientTransport = new MyCustomClientTransport(\n  () =\u003e connectToMyCustomServer(),\n  'client-id',\n);\n\nconst client = createClient\u003cServiceSurface\u003e(clientTransport, 'SERVER');\n```\n\n#### Testing\n\nRiver provides utilities for testing your services:\n\n```ts\nimport { createMockTransportNetwork } from '@replit/river/testUtil';\n\ndescribe('My Service', () =\u003e {\n  // create mock transport network\n  const { getClientTransport, getServerTransport, cleanup } =\n    createMockTransportNetwork();\n  afterEach(cleanup);\n\n  test('should add numbers correctly', async () =\u003e {\n    // setup server\n    const serverTransport = getServerTransport('SERVER');\n    const services = {\n      math: MathService,\n    };\n    const server = createServer(serverTransport, services);\n\n    // setup client\n    const clientTransport = getClientTransport('client');\n    const client = createClient\u003ctypeof services\u003e(clientTransport, 'SERVER');\n\n    // test the service\n    const result = await client.math.add.rpc({ a: 1, b: 2 });\n    expect(result.ok).toBe(true);\n    if (result.ok) {\n      expect(result.payload.result).toBe(3);\n    }\n  });\n});\n```\n\n#### Custom Handshake\n\nRiver allows you to extend the protocol-level handshake so you can add additional logic to\nvalidate incoming connections.\n\nYou can do this by passing extra options to `createClient` and `createServer` and extending the `ParsedMetadata` interface:\n\n```ts\ntype ContextType = { ... }; // has to extend object\ntype ParsedMetadata = { parsedToken: string };\nconst ServiceSchema = createServiceSchema\u003cContextType, ParsedMetadata\u003e();\n\nconst services = { ... }; // use custom ServiceSchema builder here\n\nconst handshakeSchema = Type.Object({ token: Type.String() });\ncreateClient\u003ctypeof services\u003e(new MockClientTransport('client'), 'SERVER', {\n  eagerlyConnect: false,\n  handshakeOptions: createClientHandshakeOptions(handshakeSchema, async () =\u003e ({\n    // the type of this function is\n    // () =\u003e Static\u003ctypeof handshakeSchema\u003e | Promise\u003cStatic\u003ctypeof handshakeSchema\u003e\u003e\n    token: '123',\n  })),\n});\n\ncreateServer(new MockServerTransport('SERVER'), services, {\n  handshakeOptions: createServerHandshakeOptions(\n    handshakeSchema,\n    (metadata, previousMetadata) =\u003e {\n      // the type of this function is\n      // (metadata: Static\u003ctypeof\u003chandshakeSchema\u003e, previousMetadata?: ParsedMetadata) =\u003e\n      //   | false | Promise\u003cfalse\u003e (if you reject it)\n      //   | ParsedMetadata | Promise\u003cParsedMetadata\u003e (if you allow it)\n      // next time a connection happens on the same session, previousMetadata will\n      // be populated with the last returned value\n      return { parsedToken: metadata.token };\n    },\n  ),\n});\n```\n\nYou can then access the `ParsedMetadata` in your procedure handlers:\n\n```ts\nasync handler(ctx, ...args) {\n  // this contains the parsed metadata\n  console.log(ctx.metadata)\n}\n```\n\n### Further examples\n\nWe've also provided an end-to-end testing environment using `Next.js`, and a simple backend connected with the WebSocket transport that you can [play with on Replit](https://replit.com/@jzhao-replit/riverbed).\n\nYou can find more service examples in the [E2E test fixtures](https://github.com/replit/river/blob/main/__tests__/fixtures/services.ts)\n\n## Developing\n\n[![Run on Repl.it](https://replit.com/badge/github/replit/river)](https://replit.com/new/github/replit/river)\n\n- `npm i` -- install dependencies\n- `npm run check` -- lint\n- `npm run format` -- format\n- `npm run test` -- run tests\n- `npm run release` -- cut a new release (should bump version in package.json first)\n\n## Releasing\n\nRiver uses an automated release process with [Release Drafter](https://github.com/release-drafter/release-drafter) for version management and NPM publishing.\n\n### Automated Release Process (Recommended)\n\n1. **Merge PRs to main** - Release Drafter automatically:\n\n   - Updates the draft release notes with PR titles\n   - You can view the draft at [GitHub Releases](../../releases)\n\n2. **When ready to release, create a version bump PR**:\n\n   - Create a PR that bumps the version in `package.json` and `package-lock.json`. You can run `pnpm version --no-git-tag-version \u003cversion\u003e` to bump the version.\n   - Use semantic versioning:\n     - `patch` - Bug fixes, small improvements (e.g., 0.208.4 → 0.208.5)\n     - `minor` - New features, backwards compatible (e.g., 0.208.4 → 0.209.0)\n     - `major` - Breaking changes (e.g., 0.208.4 → 1.0.0)\n   - Merge the PR to main\n\n3. **Publish the GitHub release**:\n\n   - Go to [GitHub Releases](../../releases)\n   - Find the draft release and click \"Edit\"\n   - Update the tag to match your new version (e.g., `v0.209.0`)\n   - Click \"Publish release\"\n\n4. **Automation takes over**:\n\n   - Publishing the release automatically triggers the \"Build and Publish\" workflow\n   - The `river` package is published to NPM\n\n5. **Manual npm release**:\n   - If the auto-publish workflow failed, you can run `npm run release` locally\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Freplit%2Friver","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Freplit%2Friver","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Freplit%2Friver/lists"}