Skip to content

Refactor ServiceContext as generic#322

Merged
daweifeng-replit merged 12 commits into
mainfrom
dawei/refactor-global-declare
Jun 16, 2025
Merged

Refactor ServiceContext as generic#322
daweifeng-replit merged 12 commits into
mainfrom
dawei/refactor-global-declare

Conversation

@daweifeng-replit

@daweifeng-replit daweifeng-replit commented Jun 9, 2025

Copy link
Copy Markdown
Contributor

Why

When we are creating a server using river, we have to declare the ServiceContext interface globally so TypeScript can infer the type for ctx within a procedure.

 declare module '@replit/river' {
   interface ServiceContext {
       db: Database;
   }
 }

However, it will be an issue in a mono repo setup where we want to create multiple servers with different ServiceContext.

What changed

Use builder pattern for ServiceSchema so we can create a ServiceSchema with Context like

const ServiceSchema = createServiceSchema<Context>();

Example:

const extendedContext = {
  testctx: Math.random().toString(),
};

const ServiceSchema = createServiceSchema<typeof extendedContext>();

const services = {
  testservice: ServiceSchema.define({
    testrpc: Procedure.rpc({
      requestInit: Type.Object({}),
      responseData: Type.String(),
      handler: async ({ ctx }) => {
        return Ok(ctx.testctx);
      },
    }),
  }),
};

When we create a server, it will perform type checking to make sure that all the services are expecting the same ServiceContext as the type of extendedContext in the config.

createServer(serverTransport, services, {
  extendedContext,
});

There is no type-checking for Context in createClient(). We are using any to skip it behind the scene because it is a implementation detail on the server side.

const client = createClient<typeof services>(
  clientTransport,
  serverTransport.clientId,
); 

Versioning

  • Breaking protocol change
  • Breaking ts/js API change

@jackyzha0

Copy link
Copy Markdown
Member

hm I feel pretty strongly we shouldn't change the default ServiceSchema.define into ServiceSchema.defineWithContext() for this to work, we might be able to get away with a slightly less breaking change

my other main nit is that in theory, we shouldn't need to change the generics on createClient either as ServiceContext is purely a server-side concept so it makes no sense for a client to reason about ServiceContext

let's talk about this more tomorrow in the office but perhaps we can subclass ServiceSchema to be ServiceSchemaWithContext (metaclasses/class factories :))

@Monkatraz

Copy link
Copy Markdown
Contributor

Another idea is to do something like:

import { Procedure as BaseProcedure } from '@replit/river'

export const Procedure = BaseProcedure.extend<ExtendedContext>()

@Monkatraz

Copy link
Copy Markdown
Contributor

Ah yeah Jacky's idea is better, we basically want a server builder scaffold

@daweifeng-replit daweifeng-replit marked this pull request as ready for review June 10, 2025 01:30
@daweifeng-replit daweifeng-replit requested a review from a team as a code owner June 10, 2025 01:30
@daweifeng-replit daweifeng-replit requested review from Monkatraz, jackyzha0 and masad-frost and removed request for a team June 10, 2025 01:30
Comment thread __tests__/context.test.ts Outdated
Comment thread __tests__/context.test.ts
Comment thread __tests__/typescript-stress.test.ts Outdated
Comment thread router/services.ts
Comment thread router/client.ts
Comment thread router/services.ts
Comment thread __tests__/context.test.ts
Comment thread router/services.ts
@Monkatraz

Monkatraz commented Jun 10, 2025

Copy link
Copy Markdown
Contributor

Do we have to pass the extended context as a parameter? That goes against how e.g. Chort defines its services and will require a significant refactor

I think it goes against the spirit of River's service schema if you have to instantiate a bunch of potential side effects just to construct the schema

@daweifeng-replit

Copy link
Copy Markdown
Contributor Author

Do we have to pass the extended context as a parameter? That goes against how e.g. Chort defines its services and will require a significant refactor

I think it goes against the spirit of River's service schema if you have to instantiate a bunch of potential side effects just to construct the schema

There is another option which is

const extendedContext = {
    testctx: Math.random().toString(),
};
const ServiceSchema = createServiceSchema<typeof extendedContext>();

Would this work for Chort?

@jackyzha0

Copy link
Copy Markdown
Member

yeah now that i think of it just having the type and not constructing the whole object is probably better, lets go with the second approach

@daweifeng-replit

Copy link
Copy Markdown
Contributor Author

yeah now that i think of it just having the type and not constructing the whole object is probably better, lets go with the second approach

Yup, it is updated to const ServiceSchema = createServiceSchema<typeof extendedContext>();

@jackyzha0

Copy link
Copy Markdown
Member

id like to see if we can spend a bit more time avoiding the any before we merge, it scares me and tells us the types are flowing wrong somewhere

@daweifeng-replit

Copy link
Copy Markdown
Contributor Author

id like to see if we can spend a bit more time avoiding the any before we merge, it scares me and tells us the types are flowing wrong somewhere

Looking into the code. We might not be able to do it without any. Here is an example

type Handler<T> = (ctx: T) => void;

const specificHandler: Handler<{ db: string }> = (ctx) => {
  console.log(ctx.db); // Expects 'db' to exist
};

type AnyHandlerMap<Context extends object = object> = Record<string, Handler<Context>>

const services = {
  specificHandler
}

const generalServices: AnyHandlerMap<object> = services

TypeScript would complain about

Type '{ specificHandler: Handler<{ db: string; }>; }' is not assignable to type 'AnyHandlerMap<object>'.
  Property 'specificHandler' is incompatible with index signature.
    Type 'Handler<{ db: string; }>' is not assignable to type 'Handler<object>'.
      Property 'db' is missing in type '{}' but required in type '{ db: string; }'. (typescript)

Because AnyHandlerMap is not actual accepting "Any" procedures unless we set it to AnyHandlerMap<any>

@daweifeng-replit

Copy link
Copy Markdown
Contributor Author

As we discussed offline, we are going to keep any in the Context param of createClient() and Client generic. One potential idea to fix that is to create a new ClientService type that does not require Context so we can map ServiceSchema to ClientService. We can try it in the future.

@daweifeng-replit daweifeng-replit force-pushed the dawei/refactor-global-declare branch from 41423f7 to b25fee8 Compare June 13, 2025 00:08

daweifeng-replit commented Jun 13, 2025

Copy link
Copy Markdown
Contributor Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@daweifeng-replit daweifeng-replit added minor New features or functionality that are backwards compatible (e.g., 1.2.3 → 1.3.0) enhancement New feature or request labels Jun 16, 2025

daweifeng-replit commented Jun 16, 2025

Copy link
Copy Markdown
Contributor Author

Merge activity

  • Jun 16, 6:51 PM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Jun 16, 6:51 PM UTC: @daweifeng-replit merged this pull request with Graphite.

@daweifeng-replit daweifeng-replit merged commit de38cdf into main Jun 16, 2025
9 checks passed
@daweifeng-replit daweifeng-replit deleted the dawei/refactor-global-declare branch June 16, 2025 18:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request minor New features or functionality that are backwards compatible (e.g., 1.2.3 → 1.3.0)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants