Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(server): lazy routers #5489

Draft
wants to merge 20 commits into
base: next
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release-tmp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ on:
branches:
# Replace this with the branch you want to release from
# 👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇
- 'issues/5557-types-node-wut'
- 'issues/4129-lazy-routers'
# 👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆
paths:
- '.github/workflows/release-tmp.yml'
Expand Down
19 changes: 19 additions & 0 deletions examples/lazy-load/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# A minimal working tRPC example

Requires node 18 (for global fetch).

## Playing around

```
npm i
npm run dev
```

Try editing the ts files to see the type checking in action :)

## Building

```
npm run build
npm run start
```
26 changes: 26 additions & 0 deletions examples/lazy-load/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "examples-lazy-load",
"private": true,
"type": "module",
"scripts": {
"build": "tsc",
"dev:server": "tsx watch src/server",
"dev:client": "wait-port 3000 && tsx watch src/client",
"dev": "run-p dev:* --print-label",
"test-dev": "start-server-and-test 'tsx src/server' 3000 'tsx src/client'",
"test-start": "start-server-and-test 'node dist/server' 3000 'node dist/client'"
},
"dependencies": {
"@trpc/client": "npm:@trpc/client@next",
"@trpc/server": "npm:@trpc/server@next",
"zod": "^3.0.0"
},
"devDependencies": {
"@types/node": "^20.10.0",
"npm-run-all": "^4.1.5",
"start-server-and-test": "^1.12.0",
"tsx": "^4.0.0",
"typescript": "^5.4.0",
"wait-port": "^1.0.1"
}
}
36 changes: 36 additions & 0 deletions examples/lazy-load/src/client/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* This is the client-side code that uses the inferred types from the server
*/
import { createTRPCClient, httpBatchLink } from '@trpc/client';
/**
* We only import the `AppRouter` type from the server - this is not available at runtime
*/
import type { AppRouter } from '../server/index.js';

// Initialize the tRPC client
const trpc = createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: 'http://localhost:3000',
}),
],
});

// Call procedure functions

// 💡 Tip, try to:
// - hover any types below to see the inferred types
// - Cmd/Ctrl+click on any function to jump to the definition
// - Rename any variable and see it reflected across both frontend and backend

const users = await trpc.user.list.query();
// ^?
console.log('Users:', users);

const createdUser = await trpc.user.create.mutate({ name: 'sachinraja' });
// ^?
console.log('Created user:', createdUser);

const user = await trpc.user.byId.query('1');
// ^?
console.log('User 1:', user);
15 changes: 15 additions & 0 deletions examples/lazy-load/src/server/db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
type User = { id: string; name: string };

// Imaginary database
const users: User[] = [];
export const db = {
user: {
findMany: async () => users,
findById: async (id: string) => users.find((user) => user.id === id),
create: async (data: { name: string }) => {
const user = { id: String(users.length + 1), ...data };
users.push(user);
return user;
},
},
};
25 changes: 25 additions & 0 deletions examples/lazy-load/src/server/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* This a minimal tRPC server
*/
import { createHTTPServer } from '@trpc/server/adapters/standalone';
import { lazy } from '@trpc/server/unstable-core-do-not-import';
import { router } from './trpc.js';

const user = lazy(() =>
import('./routers/user.js').then((m) => {
console.log('💤 lazy loaded user router');
return m.userRouter;
}),
);
Comment on lines +5 to +13
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @zomars here's an example on how to use it

const appRouter = router({
user,
});

// Export type router type signature, this is used by the client.
export type AppRouter = typeof appRouter;

const server = createHTTPServer({
router: appRouter,
});

server.listen(3000);
32 changes: 32 additions & 0 deletions examples/lazy-load/src/server/routers/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* This a minimal tRPC server
*/
import { z } from 'zod';
import { db } from '../db.js';
import { publicProcedure, router } from '../trpc.js';

export const userRouter = router({
list: publicProcedure.query(async () => {
// Retrieve users from a datasource, this is an imaginary database
const users = await db.user.findMany();
// ^?
return users;
}),
byId: publicProcedure.input(z.string()).query(async (opts) => {
const { input } = opts;
// ^?
// Retrieve the user with the given ID
const user = await db.user.findById(input);
return user;
}),
create: publicProcedure
.input(z.object({ name: z.string() }))
.mutation(async (opts) => {
const { input } = opts;
// ^?
// Create a new user in the database
const user = await db.user.create(input);
// ^?
return user;
}),
});
14 changes: 14 additions & 0 deletions examples/lazy-load/src/server/trpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { initTRPC } from '@trpc/server';

/**
* Initialization of tRPC backend
* Should be done only once per backend!
*/
const t = initTRPC.create();

/**
* Export reusable router and procedure helpers
* that can be used throughout the router
*/
export const router = t.router;
export const publicProcedure = t.procedure;
12 changes: 12 additions & 0 deletions examples/lazy-load/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "esnext",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"strict": true,
"outDir": "dist",
"skipLibCheck": true
},
"include": ["src"]
}
2 changes: 1 addition & 1 deletion packages/next/src/app-dir/links/nextCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export function experimental_nextCacheLink<TRouter extends AnyRouter>(
// // that calls with different tags are properly separated
// // @link https://github.com/trpc/trpc/issues/4622
const procedureResult = await callProcedure({
procedures: opts.router._def.procedures,
_def: opts.router._def,
path,
getRawInput: async () => input,
ctx: ctx,
Expand Down
2 changes: 1 addition & 1 deletion packages/react-query/src/server/ssgProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ export function createServerSideHelpers<TRouter extends AnyRouter>(
serialize: transformer.output.serialize,
query: (queryOpts) => {
return callProcedure({
procedures: router._def.procedures,
_def: router._def,
path: queryOpts.path,
getRawInput: async () => queryOpts.input,
ctx,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type { QueryLike } from './queryLike';
*/
export type RouterLike<TRouter extends AnyRouter> = RouterLikeInner<
TRouter['_def']['_config']['$types'],
TRouter['_def']['procedures']
TRouter['_def']['record']
>;
export type RouterLikeInner<
TRoot extends AnyRootTypes,
Expand Down
2 changes: 1 addition & 1 deletion packages/server/src/adapters/ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export function getWSConnectionHandler<TRouter extends AnyRouter>(
await ctxPromise; // asserts context has been set

const result = await callProcedure({
procedures: router._def.procedures,
_def: router._def,
path,
getRawInput: async () => input,
ctx,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ export async function resolveHTTPResponse<
const input = inputs[index];
try {
const data = await callProcedure({
procedures: opts.router._def.procedures,
_def: router._def,
path,
getRawInput: async () => input,
ctx,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,7 @@ function createProcedureCaller(_def: AnyProcedureBuilderDef): AnyProcedure {
}

procedure._def = _def;
procedure.procedure = true;

// FIXME typecast shouldn't be needed - fixittt
return procedure as unknown as AnyProcedure;
Expand Down
52 changes: 52 additions & 0 deletions packages/server/src/unstable-core-do-not-import/router.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { initTRPC } from '..';
import { lazy } from './router';

const t = initTRPC.create();

Expand Down Expand Up @@ -34,3 +35,54 @@ describe('router', () => {
).toThrow('Duplicate key: foo..bar');
});
});

describe('lazy loading routers', () => {
test('lazy child', async () => {
const t = initTRPC.create();

const child = lazy(async () =>
t.router({
foo: t.procedure.query(() => 'bar'),
}),
);
const router = t.router({
child,
});

const caller = router.createCaller({});

expect(await caller.child.foo()).toBe('bar');
});

test('lazy grandchild', async () => {
const t = initTRPC.create();

const router = t.router({
child: lazy(async () =>
t.router({
grandchild: lazy(async () =>
t.router({
foo: t.procedure.query(() => 'bar'),
}),
),
}),
),
});

const caller = router.createCaller({});

expect(router._def.record).toMatchInlineSnapshot(`Object {}`);
expect(await caller.child.grandchild.foo()).toBe('bar');

// (Maybe we should just delete `_.def.record`)
expect(router._def.record).toMatchInlineSnapshot(`
Object {
"child": Object {
"grandchild": Object {
"foo": [Function],
},
},
}
`);
});
});