@@ -99,14 +99,22 @@ async function readDatabaseClock(executor: DbOrTx): Promise<number> {
9999 * an older value for the webhook reconcile to restore. The caller must hold the subscription row
100100 * lock (`FOR UPDATE`, or the `UPDATE` itself), per the lock order on
101101 * {@link lockSubscriptionForSyncRetry}.
102+ *
103+ * A Sim commit is stamped with the clock under that lock. A value taken from Stripe passes
104+ * `observedAt`, the clock read just before Stripe was read, so it orders by when it was observed:
105+ * a slower reconcile of an earlier Stripe read cannot outrank a later one.
102106 */
103107async function commitIntent < T extends SyncIntentFields > (
104108 tx : DbOrTx ,
105109 eventType : SubscriptionSyncEventType ,
106110 subscriptionId : string ,
107- fields : T
111+ fields : T ,
112+ observedAt ?: number
108113) : Promise < T & { committedAt : number } > {
109- const committed = await withCommittedAt ( tx , fields )
114+ const committed =
115+ observedAt === undefined
116+ ? await withCommittedAt ( tx , fields )
117+ : { ...fields , committedAt : observedAt }
110118 await patchRetryableOutboxEvents ( tx , eventType , subscriptionSubject ( subscriptionId ) , committed )
111119 return committed
112120}
@@ -446,7 +454,7 @@ export async function reconcileSubscriptionSyncFromStripe(event: Stripe.Event):
446454 if ( liveCancelAtPeriodEnd === undefined ) return false
447455 cancelAtPeriodEnd = liveCancelAtPeriodEnd
448456 if ( intents . cancelSyncCarriesOtherThan ( cancelAtPeriodEnd ) ) {
449- await commitIntent ( tx , CANCEL_SYNC , row . id , { cancelAtPeriodEnd } )
457+ await commitIntent ( tx , CANCEL_SYNC , row . id , { cancelAtPeriodEnd } , liveReadAt )
450458 }
451459 }
452460 const seats = intents . seats . status === 'value' ? intents . seats . value : current . seats
0 commit comments