Feature: Add support to transactions - #2
Conversation
|
It was faster than I thought. I will get hands on this ASAP. But for the code itself I was reading so far seems GREAT but I need to make a deep dive and test it. |
Uanela
left a comment
There was a problem hiding this comment.
Great job @luanupe you went deeper into this codebase (even thought not that large, but somehow dealing with a kind complex scenario) and the job you did was exactly what was blind to me on the benchmark repo (I will migrate to this as monorepo instead) see https://github.com/Uanela/smart-cache-benchmark
| }); | ||
|
|
||
| test("should intercept write operations and call handleWrite", async () => { | ||
| mockHandlerInstance.isReadOperation.mockReturnValue(false); |
There was a problem hiding this comment.
- This is not needed and does nothing you can remove
| data: { name: "Ada" }, | ||
| }); | ||
| }, | ||
| { smartCache: { enabled: false } } |
There was a problem hiding this comment.
See https://github.com/Uanela/prisma-smart-cache/pull/2/changes#r3144714942 before tackling those 2
- Let's rename
smartCache->cache - Allow pass
cache: falseas shorthand and keep the current behavior also because it allows expansion
|
|
||
| ```ts | ||
| await prisma.$transaction([ | ||
| prisma.$raw.user.findMany({ where: { active: true } }), |
| ? ((...args: A) => R) & | ||
| (<TResult>( | ||
| fn: (tx: PrismaWithCache<any>) => Promise<TResult>, | ||
| options?: SmartCacheTransactionOptions & Record<string, any> |
There was a problem hiding this comment.
-
optionsmust be strictly typed
Regarding SmartCacheTransactionOptions I think we should stick to CacheQueryOptions it will stay consistent with the current API, and using it would be a clear sign of meaning apply those CacheQueryOptions to the global transaction. e.g:
await prisma.$transaction(
async (tx) => {
await tx.user.update({
where: { id: 1 },
data: { name: "Ada" },
});
},
{ cache: ... } // CacheQueryOptions | false| } | ||
|
|
||
| interface ProxyRuntimeOptions { | ||
| deferInvalidation?: (mutation: CacheMutation) => void; |
There was a problem hiding this comment.
- What you think about renaming
deferInvalidationto simplydefer
|
|
||
| // wrap interactive transactions so tx can use smart cache and defer invalidation | ||
| if (modelName === "$transaction" && typeof modelDelegate === "function") { | ||
| return (firstArg: any, transactionOptions?: any) => { |
There was a problem hiding this comment.
- Let's add the types that we already have here I guess will make it much clear for future readers
| smartCache: _smartCache, | ||
| ...prismaTransactionOptions | ||
| } = (transactionOptions ?? {}) as SmartCacheTransactionOptions & | ||
| Record<string, any>; |
There was a problem hiding this comment.
- As we already casted to
SmartCacheTransactionOptionsmaybeRecord<string, any>is unneeded what you think?
| ) | ||
| .then(async (result: unknown) => { | ||
| // flush transaction invalidations only after commit succeeds | ||
| if (useSmartCache && mutations?.length) { |
| ); | ||
| ``` | ||
|
|
||
| For array transactions, Prisma requires every array element to be a Prisma Client promise. Cached operations return regular promises, so use `$raw` when building the array: |
There was a problem hiding this comment.
- The correct name is
sequential transactionsI just saw now on their website you could putsequential transactions (array transactions). - I think also for this one would be great if we added a https://www.prisma.io/docs/orm/prisma-client/queries/transactions#sequential-operations so that users can quickly grasp why the
$raw. I am trying to check if would could go without $raw but up until now shows impossible, and for now the solution you brought is the GREATEST
| mockPrisma.user.findMany.mockReturnValue(prismaPromise); | ||
|
|
||
| const proxy = smartCache(mockPrisma, mockBento); | ||
| const result = proxy.$raw.user.findMany({ cache: { ttl: 100 } } as any); |
There was a problem hiding this comment.
- is the cache object need on this situation, I think not at all. what are you thoughts on this one?
|
Hey @Uanela , I believe your points are indeed valid. 🙂 |
|
Glad you got those, in Mozambique May 1st is also an holiday. |
|
Any update? |
Hey @Uanela I haven't forgotten about this PR, but I ended up finding a huge bug in one of my projects, requiring data migration and some other major changes that are taking up a lot of my time. But I'll get back to fixing it ASAP. |
Summary
This PR improves transaction support in
prisma-smart-cache, specifically addressing cache consistency issues when using Prisma transactions and compatibility issues with array-based transactions.Problems addressed
Interactive transactions bypassed smart cache
Previously, when using:
the
txobject was Prisma’s raw transaction client. Because of that:Array transactions could fail with PrismaPromise validation
When using:
proxied operations returned regular JavaScript promises instead of Prisma Client promises. Prisma requires all array transaction entries to be Prisma Client promises, which could lead to:
What changed
Interactive transaction support
$transactionis now handled explicitly by the proxy.For interactive transactions, the transaction client passed to the callback is wrapped with smart cache by default:
This allows transactional reads and writes to go through the same cache-aware behavior as the root client.
Deferred invalidation after transaction commit
Writes inside interactive transactions no longer invalidate cache immediately.
Instead, mutations are collected while the transaction callback runs, and invalidation is flushed only after the transaction commits successfully.
If the transaction rolls back or throws, no invalidation is performed.
This avoids invalidating cache before the database changes are actually committed.
Opt-out for raw transaction client
A transaction can explicitly opt out of smart cache wrapping:
In this case, the callback receives Prisma’s original transaction client.
The custom
smartCacheoption is stripped before forwarding transaction options to Prisma.$rawpassthrough clientA
$rawproperty was added to expose the original Prisma client behavior for cases where preserving Prisma Client promises is required.This is especially useful for array transactions:
$rawbypasses caching but strips the customcacheoption from arguments before forwarding calls to Prisma.Public invalidation method
The internal invalidation method was made public so transaction invalidation can call it directly after commit.
When multiple transactional mutations are collected, invalidations are flushed with
Promise.allafter first checking that there are mutations to process.Types moved to shared type file
Transaction-related cache mutation types were moved into
src/types.tsand exported from the package entrypoint.This keeps shared proxy/handler types in the same place as the rest of the public/internal type definitions.
Documentation
The README now includes a section explaining:
$rawfor array transactions;Tests
Regression coverage was added for:
smartCache.enabled: false;$rawwithout smart cache interception;Implementation note
Some logic was intentionally kept inline and slightly duplicated in the proxy.
The goal was to minimize unnecessary abstraction in areas where small behavior differences matter, such as:
Over-reusing the same helper paths here could introduce unnecessary risk, since these flows may need slightly different rules over time.
To avoid duplicating even more code, a small
wrapClientfunction is still used internally to wrap both the root Prisma client and transaction clients consistently.How to test
Run the test suite:
npm testRun the package build:
Manual scenarios worth validating in a consuming app:
Interactive transaction invalidation
The final read should not return stale cached data.
Opt out of smart cache inside transaction
The transaction callback should receive the raw Prisma transaction client.
Array transaction with
$rawThis should preserve Prisma Client promises and avoid the array transaction validation error.