#db transactions
1 messages · Page 1 of 1 (latest)
Can you explain your use-case more (and/or share code)?
Hey, so I have an application that utilises next.js, trpc and prisma. Essentially we are allowing buying of passes for a gym, and also allowing payments and refunds on our platform. The way our database is setup is that we have these few tables:
- Pass
- Payment
- Refund
- PaymentItem
So in an example of a pass purchase, we would have a db transaction on the backend, which creates a Payment with a PaymentStatus of NONE, creates a Pass with a status of PENDING, create a PaymentItem that joins the Pass and the Payment together. Then there will also be another db transaction of attempting to conduct the actual payment using stripe and further updating the statuses based on success or failure. My current plan was for the Pass, Payment to have their own state machines and then utilise an orchestrating state machine to perform the communication but im a little confused with how i would wrap it all in a transaction or if this is the best way to structure it @restive rain
Additionally, I am not sure if xstate is compatible with trpc, given that I will have to manually pass in the types for context and inputs in state machine as opposed to trpc doing the type interpreations for me.
Example Code:
context: {
paymentRef: ActorRefFrom<typeof paymentMachine> | undefined
passRef: ActorRefFrom<typeof passMachine> | undefined
db: Kysely<DB>
passDuration: PassDuration
lgr: Logger
userId: string
passActivityId: string
priceInCents: string
isPeak: boolean
description: string
}
input: {
db: Kysely<DB>
passDuration: PassDuration
lgr: Logger
userId: string
passActivityId: string
priceInCents: string
isPeak: boolean
description: string
}
},```
Thanks for the context (no pun intended) How are you wanting to use XState with trpc?
The way I'd generally do it (e.g. within a trpc route) would be similar to this:
cancelPass: t.procedure
.input(z.object({/* ... */}))
.mutation(async ({ input, ... }) => {
const actor = createActor(passMachine, { input })
.start(); // start immediately
actor.send({ type: 'pass.cancel' });
// ...
})
Yup so i have it set up in a similar way, except that we also have a context field so it looks something like this:
.input(z.object({/* ... */}))
.mutation(async ({ ctx, input }) => {
const lgr = ctx.logger.createScopedLogger({
action: 'passPayment:pay',
})
{Information fetching from db}
const actor = createActor(orchestratorMachine, {
input: {
db: ctx.db,
passDuration,
lgr,
userId: ctx.session.user.id,
passActivityId: passActivity.id,
priceInCents,
isPeak,
description,
},
})
actor.send({ type: 'initialisingPayment' });
// ...
})```
and for the orchestrator machine my initial setup(which has some typescript errors currently) is this
there are still some transactions following the createPayment method but i have not integrated that yet
@last pollen here!
This looks like a good path that you're heading down, where are you getting stuck?
not sure if passing down all the info into the context is the right way to go cause it makes the trpc typesafety kind of useless since i have to declare the types right
You need to give the types to the state machine to get type safety in the events but if you are using Zod for your schemas you can just use z.infer<typeof schema> as a starting point
and also sometimes i have to initialise the state machine with a particular state and im also unable to find that in the docs without storing the state machine snapshot
Put your schemas in a const instead of inlining them and you'll be fine
oh but some of the information that i pass into the state machine is not only from the input but after fetching data from the db, such as passDuration. do u think i should shift all the functions to be within the state machine? cause i perform all the fetching functions outside of the state machine currently
I recommend actors all the way down
Those fetches could be fromPromise actors that spawn and send back their result
If you need to initialise the state machine with a particular state that is not coming from that state machine's getPersistedSnapshot(), I think the best bet may well be to create an event to handle that situation after the machine starts.
I think there's a way to resume from a partial snapshot but I can't find where I saw that being discussed on the discord server
is it reccommended to store the snapshot actually? cause in my case i would rather initialise the state from my current db implementation
When you say initialise the state, do you mean starting a machine in a state that is not the machine's specified initial state?
yup so for instance in my payment table i have a NONE state as the initial, then PENDING and CONFIRMED
so in this pass example it will go from NONE to PENDING
but to go from PENDING to CONFIRMED it will be within a separate function which is from stripe’s webhook
so when i initialise that state machine, i want it to start at the state PENDING instead
So I think the best thing to do would be to explicitly model that circumstance, it almost sounds like a separate actor to be honest
I just spent some time playing around and there doesn't seem to be a straightforward way to resume without a snapshot to resume from
separate actor meaning separate state machine right?
Doesn't sound like it would need to be a full state machine, it's just an async function right?
Promise actors are actors that represent a promise that performs some asynchronous task. They may resolve with some output, or reject with an error.
oh so it wouldnt be part of a state machine?
Doesn't sound like it should be to me, it's an external thing happening on a different CPU
then i am a little confused on why i would need to wrap it in a promise actor to do that if i can just use the normal code
Because then you can easily spawn it from your state machine and use its result to continue
oh but to spawn it from my state machine i would have to call createActor on the state machine and then spawn it during some state transition/ on entry right? but then i run into the same problem of my state machine being in the incorrect state initially. (referencing back the stripe webhook example)
unless im misunderstanding something
Can you lay out sequentially the flow of your events here? I'm unclear on how the state machine that spawns the actor that handles the webhook situation would be in the wrong state when the webhook resolves unless you put it in a different one?
Okay so its basically a two step process.
Step 1: user buys the pass and calls the endpoint. This pay endpoint creates an orchestrator state machine that performs the db transaction of creatjng the Pass creating the Payment and creating the PaymentItem and Pass and Payment Status is set to PENDING. And a stripe payment is created.
step 2:
we handle stripe’s webhook that calls us. in this function, we are checking if the payment succeeded then setting the pass and payment status accordingly
so essentially these two steps will not be called at the same time but one by one
So what happens to the process that handled step 1? How does step 2 get the data on what step 1 was about?
i imagine the orchestrator state machine to actually spawn two state machines of payment and pass that performs its respective duties
the process that handles step 1 ends and the user gets redirected to a processing page
Why are you creating an orchestrator state machine if it doesn't live past step 1 of the process it is controlling?
step 2 gets the data because we send some of the data to the stripe event
So at this point, you have two very simple state machines that don't need to know anything about each other because they are pure functions and always get the data they need, right?
they need to know information about each other because the state transitions in payment triggers the state transitions in pass
for instance if a payment changes to success then pass also changes to success
and then it also gets more complicated when i introduce refunds cause that can change the state of passes to cancelled
Ok, so it sounds like this is basically what you need to happen right?
Unless you have a way to get Stripe's webhook to specifically get the process that created the Stripe payment, you need a little more robustness in how it gets handled, and I don't think there's going to be a way to do it wrapped in a single database transaction
(assuming you're using Postgres or similar that is, I'm sure there's some other DB out there that can suspend a transaction with an id that makes this easy)
oh my current implementation involves it being in separate db transactions for the two processes
the last step of A isnt really periodically check but more like on the frontend we will just refetch from the endpoint periodically
So it sounds like you have two machines, A and B
Neither needs to know anything about the other to function
Then you can use a couple of fromPromise actor invocations to handle the async tasks those machines need to perform and then you should be in good shape
so that means i wont require an actual state machine and just utilise promise actors?
I mean that A and B could be full state machines (and I recommend it) that each have some promise actors that they invoke to handle the machine's async behaviours
@last pollen could i also get ur help on this error, basically i want to spawn a payment state machine from my orchestrator state machine but i get this error on paymentMachine that Argument of type `StateMachine<MachineContext....> isnt assignable to parameter of type 'createPayment' im abit confused to why it is trying to assign it tot ype of createPayment, i also get an error when hovering over paymentRef in that same line. i am following the docs to spawn the machine but it does not seem to work
Put payment machine in your actors in setup
Only just, but it is 5.30am :D
11:30 PM here, also tired 💤
just paymentMachine? i still get the error though