-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
new way to integrate AsyncLocalStorage #6881
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
base: main
Are you sure you want to change the base?
Conversation
…d cross-platform valid absoulte path string with fileURLToPath, and path.resolve, running tests fails on windows without this modification
…ed to work cross-platforms
…can enable async context with one line as optional run time configuration option
|
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
WalkthroughThis update adds native support for Node.js AsyncLocalStorage to the tRPC server core. It introduces an optional asyncStorage property in configuration interfaces and propagates it through the server initialization and procedure builder, enabling context propagation across asynchronous calls. New tests and documentation demonstrate and explain the feature. Changes
Sequence Diagram(s)sequenceDiagram
participant App as Application
participant tRPC as tRPC Server
participant ALS as AsyncLocalStorage
participant Proc as Procedure
App->>tRPC: Initialize with asyncStorage
tRPC->>ALS: Store context (run)
ALS->>Proc: Execute procedure with context
Proc->>ALS: Access context anywhere in async call chain
Proc-->>tRPC: Return result
tRPC-->>App: Respond with result
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~15–25 minutes Possibly related issues
Suggested reviewers
Poem
Note ⚡️ Unit Test Generation is now available in beta!Learn more here, or try it out under "Finishing Touches" below. 📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (1)
🧰 Additional context used🧠 Learnings (3)📓 Common learnings📚 Learning: in trpc codebase, both `input` and `errors` properties consistently use `void` as their default type...Applied to files:
📚 Learning: the cli options `trpcfile` and `trpcimportname` in `packages/upgrade/src/bin/cli.ts` are temporary a...Applied to files:
🧬 Code Graph Analysis (1)packages/server/src/unstable-core-do-not-import/procedureBuilder.ts (1)
🪛 ESLintpackages/server/src/unstable-core-do-not-import/procedureBuilder.ts[error] 647-647: Forbidden non-null assertion. (@typescript-eslint/no-non-null-assertion) ⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
🔇 Additional comments (2)
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
|
@ARAldhafeeri is attempting to deploy a commit to the trpc Team on Vercel. A member of the Team first needs to authorize it. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (4)
packages/server/src/unstable-core-do-not-import/rootConfig.ts (1)
1-1: Fix import type to comply with ESLint rules.The
AsyncLocalStorageimport is only used as a type in this file, so it should be imported as a type.Apply this diff to fix the ESLint issue:
-import { AsyncLocalStorage } from 'async_hooks'; +import type { AsyncLocalStorage } from 'async_hooks';packages/server/src/unstable-core-do-not-import/initTRPC.ts (1)
1-1: Fix import type to comply with ESLint rules.Similar to the rootConfig file,
AsyncLocalStorageis only used as a type here.Apply this diff to fix the ESLint issue:
-import { AsyncLocalStorage } from 'async_hooks'; +import type { AsyncLocalStorage } from 'async_hooks';packages/server/src/unstable-core-do-not-import/procedureBuilder.ts (1)
1-1: Fix import type to comply with ESLint rules.Consistent with other files,
AsyncLocalStorageshould be imported as a type.Apply this diff to fix the ESLint issue:
-import { AsyncLocalStorage } from 'async_hooks'; +import type { AsyncLocalStorage } from 'async_hooks';packages/tests/server/validators.test.ts (1)
719-727: Consider adding more descriptive error message.The error handling is correct, but the error message could be more specific to help with debugging.
- throw new Error('No context found'); + throw new Error('AsyncLocalStorage context not found - ensure procedure is called within async storage scope');
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
packages/server/src/unstable-core-do-not-import/initTRPC.ts(5 hunks)packages/server/src/unstable-core-do-not-import/procedureBuilder.ts(3 hunks)packages/server/src/unstable-core-do-not-import/rootConfig.ts(2 hunks)packages/tests/server/adapters/standalone.http2.test.ts(1 hunks)packages/tests/server/validators.test.ts(1 hunks)www/docs/server/async-context-management.md(1 hunks)
🧰 Additional context used
🧠 Learnings (7)
📓 Common learnings
Learnt from: barelyhuman
PR: trpc/trpc#6530
File: packages/upgrade/src/bin/cli.ts:218-222
Timestamp: 2025-02-19T20:31:52.088Z
Learning: The CLI options `trpcFile` and `trpcImportName` in `packages/upgrade/src/bin/cli.ts` are temporary and planned to be removed in favor of using automatically discovered references from the TypeScript program.
📚 Learning: the cli options `trpcfile` and `trpcimportname` in `packages/upgrade/src/bin/cli.ts` are temporary a...
Learnt from: barelyhuman
PR: trpc/trpc#6530
File: packages/upgrade/src/bin/cli.ts:218-222
Timestamp: 2025-02-19T20:31:52.088Z
Learning: The CLI options `trpcFile` and `trpcImportName` in `packages/upgrade/src/bin/cli.ts` are temporary and planned to be removed in favor of using automatically discovered references from the TypeScript program.
Applied to files:
packages/server/src/unstable-core-do-not-import/initTRPC.tswww/docs/server/async-context-management.mdpackages/server/src/unstable-core-do-not-import/procedureBuilder.tspackages/server/src/unstable-core-do-not-import/rootConfig.ts
📚 Learning: in the trpc monorepo, packages/tests and packages/upgrade don't need main/module/types fields in the...
Learnt from: juliusmarminge
PR: trpc/trpc#6789
File: packages/tanstack-react-query/tsdown.config.ts:1-24
Timestamp: 2025-06-09T14:01:20.033Z
Learning: In the tRPC monorepo, packages/tests and packages/upgrade don't need main/module/types fields in their package.json because tests is not distributed and upgrade just exposes a binary.
Applied to files:
packages/server/src/unstable-core-do-not-import/initTRPC.ts
📚 Learning: in trpc codebase, both `input` and `errors` properties consistently use `void` as their default type...
Learnt from: heitorlisboa
PR: trpc/trpc#6849
File: packages/tests/server/websockets.test.ts:1962-1962
Timestamp: 2025-06-29T21:21:01.631Z
Learning: In tRPC codebase, both `input` and `errors` properties consistently use `void` as their default type value in procedure definitions, following the pattern `DefaultValue<TInputIn, void>` and `DefaultValue<TErrors, void>`. This is an established architectural decision that maintains consistency across the type system, even though it diverges from general TypeScript conventions that prefer `undefined` for property types.
Applied to files:
packages/server/src/unstable-core-do-not-import/initTRPC.tspackages/server/src/unstable-core-do-not-import/procedureBuilder.ts
📚 Learning: in the minimal-react-typesafe-errors example, the database mock in db.ts should throw generic error ...
Learnt from: heitorlisboa
PR: trpc/trpc#6849
File: examples/minimal-react-typesafe-errors/server/db.ts:18-20
Timestamp: 2025-06-29T21:25:46.435Z
Learning: In the minimal-react-typesafe-errors example, the database mock in db.ts should throw generic Error objects rather than typed errors. The typed errors are handled at the tRPC procedure layer, which catches database errors and transforms them into API-specific typed errors. This maintains proper architectural separation between the database layer and API layer.
Applied to files:
packages/tests/server/validators.test.ts
📚 Learning: in the minimal-typesafe-errors example, the database mock in db.ts should throw generic error object...
Learnt from: heitorlisboa
PR: trpc/trpc#6849
File: examples/minimal-typesafe-errors/src/server/db.ts:18-20
Timestamp: 2025-06-29T21:26:35.189Z
Learning: In the minimal-typesafe-errors example, the database mock in db.ts should throw generic Error objects rather than typed errors. The typed errors are handled at the tRPC procedure layer, which catches database errors and transforms them into API-specific typed errors. This maintains proper architectural separation between the database layer and API layer.
Applied to files:
packages/tests/server/validators.test.ts
📚 Learning: in the minimal-react-typesafe-errors example, the `trpc.user.list` query is designed to never throw ...
Learnt from: heitorlisboa
PR: trpc/trpc#6849
File: examples/minimal-react-typesafe-errors/client/src/Users.tsx:6-6
Timestamp: 2025-06-29T21:21:07.306Z
Learning: In the minimal-react-typesafe-errors example, the `trpc.user.list` query is designed to never throw errors since it's a minimal example focused on demonstrating typed error handling in mutations rather than queries.
Applied to files:
packages/tests/server/validators.test.ts
🧬 Code Graph Analysis (2)
packages/server/src/unstable-core-do-not-import/procedureBuilder.ts (1)
packages/server/src/unstable-core-do-not-import/middleware.ts (1)
MiddlewareResult(36-38)
packages/tests/server/validators.test.ts (4)
packages/server/src/unstable-core-do-not-import/initTRPC.ts (1)
initTRPC(233-233)packages/server/src/unstable-core-do-not-import/procedureBuilder.ts (1)
input(503-509)packages/tests/server/___testHelpers.ts (1)
routerToServerAndClientNew(36-74)packages/server/src/__tests__/waitError.ts (1)
waitError(3-40)
🪛 ESLint
packages/server/src/unstable-core-do-not-import/initTRPC.ts
[error] 1-1: All imports in the declaration are only used as types. Use import type.
(@typescript-eslint/consistent-type-imports)
packages/server/src/unstable-core-do-not-import/procedureBuilder.ts
[error] 1-1: All imports in the declaration are only used as types. Use import type.
(@typescript-eslint/consistent-type-imports)
[error] 647-647: Forbidden non-null assertion.
(@typescript-eslint/no-non-null-assertion)
packages/server/src/unstable-core-do-not-import/rootConfig.ts
[error] 1-1: All imports in the declaration are only used as types. Use import type.
(@typescript-eslint/consistent-type-imports)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
- GitHub Check: Lint and auto-fix
- GitHub Check: build
- GitHub Check: test
- GitHub Check: Analyze (typescript)
🔇 Additional comments (11)
packages/tests/server/adapters/standalone.http2.test.ts (1)
26-49: Excellent security and cross-platform improvement!The refactoring from
execSyncwith concatenated string toexecFileSyncwith structured arguments prevents shell injection vulnerabilities and improves cross-platform compatibility. ThewindowsHide: trueoption is a thoughtful addition for Windows users.packages/server/src/unstable-core-do-not-import/rootConfig.ts (1)
91-94: LGTM! Clean integration of AsyncLocalStorage into root config.The optional
asyncStorageproperty is properly typed and documented. UsingAsyncLocalStorage<any>provides the necessary flexibility for different context types while maintaining type safety.packages/server/src/unstable-core-do-not-import/initTRPC.ts (3)
52-55: Well-documented configuration option.The
asyncStorageproperty is properly integrated into the runtime configuration with clear documentation.
174-177: LGTM! Proper propagation to internal config.The
asyncStorageoption is correctly passed through to the internalRootConfigwith appropriate comments.
203-203: Correct forwarding to procedure builder.The
asyncStorageoption is properly passed to the procedure builder, completing the configuration propagation chain.packages/server/src/unstable-core-do-not-import/procedureBuilder.ts (2)
91-94: LGTM! Proper integration into procedure builder definition.The
asyncStorageproperty is correctly added to theProcedureBuilderDeftype with appropriate documentation.
632-648: Excellent AsyncLocalStorage implementation pattern!The implementation correctly:
- Wraps execution only at the first middleware call (index === 0)
- Prevents infinite recursion by clearing asyncStorage in the recursive call
- Uses the proper
asyncStorage.run(context, callback)pattern- Stores the result outside the run callback to return it properly
The non-null assertion on Line 647 is safe because the result is guaranteed to be set within the async callback.
However, you can address the ESLint warning by initializing the variable:
- let result: MiddlewareResult<any>; + let result: MiddlewareResult<any> | undefined; await _def.asyncStorage.run(opts.ctx, async () => { // Process the middleware chain without asyncStorage to prevent infinite recursion result = await callRecursive( 0, { ..._def, asyncStorage: undefined }, opts, ); }); // Return the result from the async context - return result!; + return result as MiddlewareResult<any>;packages/tests/server/validators.test.ts (4)
708-717: LGTM! Clean demonstration of the new AsyncLocalStorage feature.The test effectively demonstrates the new
asyncStorageconfiguration option that reduces boilerplate compared to manual middleware setup. The type definition and tRPC initialization are correct.
733-739: Excellent demonstration of context usage in input parser.This shows the key benefit of the feature - the input parser can directly access the context via AsyncLocalStorage without requiring additional middleware setup. The assertion validates that the context is properly propagated.
746-752: Good test setup with context provider.The server configuration correctly provides the expected context that will be injected into AsyncLocalStorage.
753-776: Comprehensive test coverage with proper error validation.The test covers both the success case and validation error case with appropriate snapshots. The error message format matches the expected zod v3 validation error structure.
| ## Async Context Management | ||
|
|
||
| tRPC's `AsyncLocalStorage` integration automatically propagates context through async calls. | ||
|
|
||
| ### Setup | ||
|
|
||
| ```ts | ||
| import { AsyncLocalStorage } from 'async_hooks'; | ||
|
|
||
| const t = initTRPC.context<{ user: { id: string } }>().create({ | ||
| asyncStorage: new AsyncLocalStorage(), // Enable auto-context | ||
| }); | ||
| ``` | ||
|
|
||
| ### Key Features | ||
|
|
||
| 1. **Access context anywhere** | ||
|
|
||
| ```ts | ||
| const router = t.router({ | ||
| getUser: t.procedure.query(() => { | ||
| const ctx = t.asyncStorage.getStore(); // No prop drilling | ||
| return ctx?.user; | ||
| }) | ||
| }); | ||
| ``` | ||
|
|
||
| 2. **Works with nested calls** | ||
|
|
||
| ```ts | ||
|
|
||
| const router = t.router({ | ||
| getUser: t.procedure.query(() => { | ||
| const users = await getUserFromDB(); | ||
| }) | ||
| }); | ||
|
|
||
| async function getUserFromDB() { | ||
| // Still has access to original context | ||
| const { user } = t.asyncStorage.getStore()!; | ||
| return db.users.find(user.id); | ||
| } | ||
| ``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Verify the API usage in documentation examples.
The documentation is well-structured and covers the key features, but I need to verify that the API usage (t.asyncStorage.getStore()) matches the actual implementation.
Let me check if the t.asyncStorage property exists and how users should access the AsyncLocalStorage instance:
🏁 Script executed:
#!/bin/bash
# Search for asyncStorage usage patterns in the codebase
echo "Searching for asyncStorage access patterns..."
rg -A 3 -B 3 "asyncStorage\.getStore|\.asyncStorage" --type ts
echo "Searching for AsyncLocalStorage property definitions..."
rg -A 5 -B 2 "asyncStorage.*:" --type ts
echo "Searching for procedure builder usage with asyncStorage..."
rg -A 10 -B 5 "createBuilder.*asyncStorage" --type tsLength of output: 7644
I couldn’t find how the public t object exposes the asyncStorage instance. Let me pull the relevant creation logic to confirm whether t.asyncStorage exists or if a different accessor is required:
🏁 Script executed:
#!/bin/bash
# Show the context builder and create() return structure
rg -n "export function context" -A 20 -B 5 packages/server/src/unstable-core-do-not-import/initTRPC.ts
# Show where create() is defined and what it returns
rg -n ".create(" -A 10 -B 10 packages/server/src/unstable-core-do-not-import/initTRPC.tsLength of output: 262
I didn’t catch the initTRPC builder or its .create() signature. Let’s re-run with escaped regexes to pull in the full definition and return shape:
🏁 Script executed:
#!/bin/bash
echo "Finding initTRPC definition and options..."
rg -n "function initTRPC" -A 20 -B 5 packages/server/src/unstable-core-do-not-import/initTRPC.ts
echo "Inspecting context builder call site..."
rg -n "\.context<" -A 10 -B 5 packages/server/src/unstable-core-do-not-import/initTRPC.ts
echo "Locating the .create\(" -A 15 -B 5 packages/server/src/unstable-core-do-not-import/initTRPC.tsLength of output: 571
I still can’t locate the public t shape or the .create() return type to confirm if asyncStorage is exposed. Let me dump the top of the initTRPC.ts file to inspect how the context builder and .create() are implemented:
🏁 Script executed:
#!/bin/bash
# Dump the first 200 lines of the initTRPC implementation
sed -n '1,200p' packages/server/src/unstable-core-do-not-import/initTRPC.tsLength of output: 5799
Documentation: Expose and use the AsyncLocalStorage instance, not t.asyncStorage
The public TRPCRootObject does not include an asyncStorage property—asyncStorage is only kept internally on the builder’s config. Users must keep a reference to the AsyncLocalStorage they pass into .create() and call its methods directly.
Suggested updates:
-
In “Setup,” capture the storage instance:
import { AsyncLocalStorage } from 'async_hooks'; const asyncStorage = new AsyncLocalStorage<{ user: { id: string } }>(); const t = initTRPC .context<{ user: { id: string } }>() .create({ asyncStorage });
-
In examples, replace
t.asyncStorage.getStore()withasyncStorage.getStore()(and similarly for.run()):- const ctx = t.asyncStorage.getStore(); + const ctx = asyncStorage.getStore();
- await t.asyncStorage.run(opts.ctx, async () => { … }); + await asyncStorage.run(opts.ctx, async () => { … });
Please update all occurrences accordingly so code samples compile against the public API.
🤖 Prompt for AI Agents
In www/docs/server/async-context-management.md lines 1 to 43, the code examples
incorrectly use t.asyncStorage to access AsyncLocalStorage, but the public
TRPCRootObject does not expose asyncStorage. To fix this, declare and export the
AsyncLocalStorage instance separately during setup, assign it to a variable
(e.g., asyncStorage), pass it into t.create(), and update all code samples to
use asyncStorage.getStore() and asyncStorage.run() instead of t.asyncStorage
methods to align with the public API.
@trpc/client
@trpc/next
@trpc/react-query
@trpc/server
@trpc/tanstack-react-query
@trpc/upgrade
commit: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
packages/server/src/unstable-core-do-not-import/initTRPC.ts(5 hunks)packages/server/src/unstable-core-do-not-import/procedureBuilder.ts(3 hunks)packages/server/src/unstable-core-do-not-import/rootConfig.ts(2 hunks)www/docs/server/async-context-management.md(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- www/docs/server/async-context-management.md
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/server/src/unstable-core-do-not-import/rootConfig.ts
- packages/server/src/unstable-core-do-not-import/initTRPC.ts
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: barelyhuman
PR: trpc/trpc#6530
File: packages/upgrade/src/bin/cli.ts:218-222
Timestamp: 2025-02-19T20:31:52.088Z
Learning: The CLI options `trpcFile` and `trpcImportName` in `packages/upgrade/src/bin/cli.ts` are temporary and planned to be removed in favor of using automatically discovered references from the TypeScript program.
📚 Learning: in trpc codebase, both `input` and `errors` properties consistently use `void` as their default type...
Learnt from: heitorlisboa
PR: trpc/trpc#6849
File: packages/tests/server/websockets.test.ts:1962-1962
Timestamp: 2025-06-29T21:21:01.631Z
Learning: In tRPC codebase, both `input` and `errors` properties consistently use `void` as their default type value in procedure definitions, following the pattern `DefaultValue<TInputIn, void>` and `DefaultValue<TErrors, void>`. This is an established architectural decision that maintains consistency across the type system, even though it diverges from general TypeScript conventions that prefer `undefined` for property types.
Applied to files:
packages/server/src/unstable-core-do-not-import/procedureBuilder.ts
📚 Learning: the cli options `trpcfile` and `trpcimportname` in `packages/upgrade/src/bin/cli.ts` are temporary a...
Learnt from: barelyhuman
PR: trpc/trpc#6530
File: packages/upgrade/src/bin/cli.ts:218-222
Timestamp: 2025-02-19T20:31:52.088Z
Learning: The CLI options `trpcFile` and `trpcImportName` in `packages/upgrade/src/bin/cli.ts` are temporary and planned to be removed in favor of using automatically discovered references from the TypeScript program.
Applied to files:
packages/server/src/unstable-core-do-not-import/procedureBuilder.ts
🧬 Code Graph Analysis (1)
packages/server/src/unstable-core-do-not-import/procedureBuilder.ts (1)
packages/server/src/unstable-core-do-not-import/middleware.ts (1)
MiddlewareResult(36-38)
🪛 ESLint
packages/server/src/unstable-core-do-not-import/procedureBuilder.ts
[error] 647-647: Forbidden non-null assertion.
(@typescript-eslint/no-non-null-assertion)
🪛 GitHub Check: Lint and auto-fix
packages/server/src/unstable-core-do-not-import/procedureBuilder.ts
[failure] 647-647:
Forbidden non-null assertion
🪛 GitHub Actions: autofix.ci
packages/server/src/unstable-core-do-not-import/procedureBuilder.ts
[error] 647-647: ESLint: Forbidden non-null assertion (@typescript-eslint/no-non-null-assertion)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
- GitHub Check: E2E-tests (next-prisma-websockets-starter)
- GitHub Check: E2E-tests (express-minimal)
- GitHub Check: E2E-tests (next-minimal-starter)
- GitHub Check: E2E-tests (minimal-react)
- GitHub Check: E2E-tests (.test/ssg)
- GitHub Check: E2E-tests (express-server)
- GitHub Check: E2E-tests (cloudflare-workers)
- GitHub Check: E2E-tests (fastify-server)
- GitHub Check: E2E-tests (.test/internal-types-export)
- GitHub Check: E2E-tests (.experimental/next-app-dir)
- GitHub Check: e2e-legacy-node (next-prisma-websockets-starter, 18.x)
- GitHub Check: e2e-legacy-node (next-prisma-starter, 18.x)
- GitHub Check: e2e-legacy-node (next-prisma-websockets-starter, 20.x)
- GitHub Check: e2e-legacy-node (next-prisma-todomvc, 18.x)
- GitHub Check: e2e-legacy-node (next-prisma-starter, 20.x)
- GitHub Check: Release using pkg.pr.new
- GitHub Check: E2E-tests (Bun) (bun, ubuntu-latest)
- GitHub Check: E2E-tests (Deno) (deno-deploy)
- GitHub Check: typecheck
- GitHub Check: test
🔇 Additional comments (2)
packages/server/src/unstable-core-do-not-import/procedureBuilder.ts (2)
1-1: LGTM!Correct type-only import of AsyncLocalStorage from Node.js async_hooks module.
91-94: LGTM!Well-documented optional property that maintains backward compatibility. The
AsyncLocalStorage<any>typing is appropriate for flexible context storage.
| // If this is the first call (index 0) and asyncStorage is configured, wrap the entire execution | ||
| if (index === 0 && _def.asyncStorage) { | ||
| // Store the result outside the run() callback | ||
| let result: MiddlewareResult<any>; | ||
|
|
||
| await _def.asyncStorage.run(opts.ctx, async () => { | ||
| // Process the middleware chain without asyncStorage to prevent هnfinite recursion | ||
| result = await callRecursive( | ||
| 0, | ||
| { ..._def, asyncStorage: undefined }, | ||
| opts, | ||
| ); | ||
| }); | ||
|
|
||
| // Return the result from the async context | ||
| return result!; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix non-null assertion and typo in AsyncLocalStorage implementation.
The AsyncLocalStorage integration logic is sound, but there are a few issues to address:
- ESLint Error: The non-null assertion on line 647 is flagged by linting rules and could be unsafe
- Typo: Line 638 contains an Arabic character ("هnfinite") that should be "infinite"
- Potential Safety Issue: While the logic should always set
result, there's no explicit guarantee
Apply this diff to fix these issues:
// If this is the first call (index 0) and asyncStorage is configured, wrap the entire execution
if (index === 0 && _def.asyncStorage) {
// Store the result outside the run() callback
let result: MiddlewareResult<any>;
await _def.asyncStorage.run(opts.ctx, async () => {
- // Process the middleware chain without asyncStorage to prevent هnfinite recursion
+ // Process the middleware chain without asyncStorage to prevent infinite recursion
result = await callRecursive(
0,
{ ..._def, asyncStorage: undefined },
opts,
);
});
// Return the result from the async context
- return result!;
+ return result;
}The non-null assertion can be safely removed because the await ensures the callback completes before returning, and the recursive call will always produce a result (either success or error).
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // If this is the first call (index 0) and asyncStorage is configured, wrap the entire execution | |
| if (index === 0 && _def.asyncStorage) { | |
| // Store the result outside the run() callback | |
| let result: MiddlewareResult<any>; | |
| await _def.asyncStorage.run(opts.ctx, async () => { | |
| // Process the middleware chain without asyncStorage to prevent هnfinite recursion | |
| result = await callRecursive( | |
| 0, | |
| { ..._def, asyncStorage: undefined }, | |
| opts, | |
| ); | |
| }); | |
| // Return the result from the async context | |
| return result!; | |
| } | |
| // If this is the first call (index 0) and asyncStorage is configured, wrap the entire execution | |
| if (index === 0 && _def.asyncStorage) { | |
| // Store the result outside the run() callback | |
| let result: MiddlewareResult<any>; | |
| await _def.asyncStorage.run(opts.ctx, async () => { | |
| // Process the middleware chain without asyncStorage to prevent infinite recursion | |
| result = await callRecursive( | |
| 0, | |
| { ..._def, asyncStorage: undefined }, | |
| opts, | |
| ); | |
| }); | |
| // Return the result from the async context | |
| return result; | |
| } |
🧰 Tools
🪛 ESLint
[error] 647-647: Forbidden non-null assertion.
(@typescript-eslint/no-non-null-assertion)
🪛 GitHub Check: Lint and auto-fix
[failure] 647-647:
Forbidden non-null assertion
🪛 GitHub Actions: autofix.ci
[error] 647-647: ESLint: Forbidden non-null assertion (@typescript-eslint/no-non-null-assertion)
🤖 Prompt for AI Agents
In packages/server/src/unstable-core-do-not-import/procedureBuilder.ts around
lines 632 to 648, remove the non-null assertion on the returned result variable
to fix the ESLint error and improve safety, correct the typo from the Arabic
character "ه" to "i" in the comment to fix the word "infinite," and ensure the
code logic clearly guarantees that the result is always assigned before
returning by relying on the await to complete the asyncStorage.run callback.
|
Hello, any updates on this ? |
| @@ -1,3 +1,4 @@ | |||
| import type { AsyncLocalStorage } from 'async_hooks'; | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We shouldn't import from Node, instead we should use a duck type that looks like AsyncLocalStorage
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace with an interface like
interface AsyncStorageLike<T> {
run<TResult>(store: T, fn: (store: T) => Promise<TResult>): Promise<TResult>;
}| // If this is the first call (index 0) and asyncStorage is configured, wrap the entire execution | ||
| if (index === 0 && _def.asyncStorage) { | ||
| // Store the result outside the run() callback | ||
| let result: MiddlewareResult<any>; | ||
|
|
||
| await _def.asyncStorage.run(opts.ctx, async () => { | ||
| // Process the middleware chain without asyncStorage to prevent infinite recursion | ||
| result = await callRecursive( | ||
| 0, | ||
| { ..._def, asyncStorage: undefined }, | ||
| opts, | ||
| ); | ||
| }); | ||
|
|
||
| // Return the result from the async context | ||
| return result!; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| // If this is the first call (index 0) and asyncStorage is configured, wrap the entire execution | |
| if (index === 0 && _def.asyncStorage) { | |
| // Store the result outside the run() callback | |
| let result: MiddlewareResult<any>; | |
| await _def.asyncStorage.run(opts.ctx, async () => { | |
| // Process the middleware chain without asyncStorage to prevent infinite recursion | |
| result = await callRecursive( | |
| 0, | |
| { ..._def, asyncStorage: undefined }, | |
| opts, | |
| ); | |
| }); | |
| // Return the result from the async context | |
| return result!; | |
| } | |
| // If this is the first call (index 0) and asyncStorage is configured, wrap the entire execution | |
| if (index === 0 && _def.asyncStorage) { | |
| return await _def.asyncStorage.run(opts.ctx, async () => { | |
| // Process the middleware chain without asyncStorage to prevent infinite recursion | |
| return await callRecursive( | |
| 0, | |
| { ..._def, asyncStorage: undefined }, | |
| opts, | |
| ); | |
| }); | |
| } |
|
|
||
| const t = initTRPC.context<Context>().create({ | ||
| asyncStorage: contextStorage, | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm, with the types you've put in, it always has to be a context storage - I think it'd be nice to separate the context from the data in the store so your async storage can have a type that doesn't match the context..
type Context = {
foo: string;
};
const contextStorage = new AsyncLocalStorage<{
something: string;
ctx: Context;
}>();
const t = initTRPC.context<Context>().create({
asyncStorage: {
storage: contextStorage,
// called whenever a procedure is called
getStore: async (opts) => {
return {
ctx: opts.ctx,
something: 'else',
};
},
},
});There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hello 👋,
What do you think about simply using:
AsyncLocalStorage<unknown>That way developers can cast to their own type when needed. Since ALS is strictly server-side, type safety across the client–server boundary doesn’t add much practical value, it just introduces extra boilerplate.
On the breaking getStore change: I see the biggest win in keeping AsyncLocalStorage integration a one-line config. Developers can define their async local storage (with any helper methods they want) in one place and export only the storage. Inside procedures, they can call getStore when needed.
Not every procedure needs async local storage, but to make one-line config possible, we currently run storage on all procedures. The overhead of running storage each time is minimal, and developers can still wrap their own helpers around it if desired.
Your call though, if you’d prefer me to implement this differently as you instructed, let me know. You’re the creator and know best.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hello Katt,
Any update on this ?
This PR addresses the feature request here #6870
Problem Statement
Currently, integrating AsyncLocalStorage with tRPC requires:
This creates unnecessary boilerplate for a common need.
Solution
Add optional
asyncStorageto runtime config that automatically wraps all procedures to improve developer experience.Why Runtime Configuration?
Implementation
Propagation Path
Key Considerations
Performance
Advanced Use Cases
.use()wrapping with not passing asyncStorage as runtime configurationUsage Example
Alternatives Considered
Final note :
✅ Checklist
Summary by CodeRabbit
Summary by CodeRabbit