#db transactions

1 messages · Page 1 of 1 (latest)

cosmic igloo
#

How can i handle database transactions with xstate? I have a database transaction that involves multiple state machines that i would like to update the state atomically

restive rain
#

Can you explain your use-case more (and/or share code)?

cosmic igloo
#

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:

  1. Pass
  2. Payment
  3. Refund
  4. 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
    }
  },```
restive rain
#

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' });

    // ...
  })
cosmic igloo
#

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

cosmic igloo
#

@last pollen here!

last pollen
#

This looks like a good path that you're heading down, where are you getting stuck?

cosmic igloo
#

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

last pollen
#

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

cosmic igloo
#

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

last pollen
#

Put your schemas in a const instead of inlining them and you'll be fine

cosmic igloo
#

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

last pollen
#

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

cosmic igloo
#

is it reccommended to store the snapshot actually? cause in my case i would rather initialise the state from my current db implementation

last pollen
#

When you say initialise the state, do you mean starting a machine in a state that is not the machine's specified initial state?

cosmic igloo
#

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

last pollen
#

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

cosmic igloo
#

separate actor meaning separate state machine right?

last pollen
#

Doesn't sound like it would need to be a full state machine, it's just an async function right?

cosmic igloo
#

oh so it wouldnt be part of a state machine?

last pollen
#

Doesn't sound like it should be to me, it's an external thing happening on a different CPU

cosmic igloo
#

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

last pollen
#

Because then you can easily spawn it from your state machine and use its result to continue

cosmic igloo
#

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

last pollen
#

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?

cosmic igloo
#

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

last pollen
#

So what happens to the process that handled step 1? How does step 2 get the data on what step 1 was about?

cosmic igloo
#

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

last pollen
#

Why are you creating an orchestrator state machine if it doesn't live past step 1 of the process it is controlling?

cosmic igloo
#

step 2 gets the data because we send some of the data to the stripe event

last pollen
#

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?

cosmic igloo
#

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

last pollen
#

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)

cosmic igloo
#

oh my current implementation involves it being in separate db transactions for the two processes

cosmic igloo
#

the last step of A isnt really periodically check but more like on the frontend we will just refetch from the endpoint periodically

last pollen
#

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

cosmic igloo
#

so that means i wont require an actual state machine and just utilise promise actors?

last pollen
cosmic igloo
#

@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

last pollen
#

Put payment machine in your actors in setup

restive rain
#

If you use setup(), all your actors have to be named

#

Beat me to it!

last pollen
#

Only just, but it is 5.30am :D

restive rain
#

11:30 PM here, also tired 💤

cosmic igloo
#

just paymentMachine? i still get the error though

last pollen
#

Then use a string to ~~invoke ~~ spawn it rather than the object

#

Like you do with createPayment

cosmic igloo
#

ahhh okay great

#

also realised i can use initial field to initialise my machine at a different state