UNPKG

@tanstack/offline-transactions

Version:

Offline-first transaction capabilities for TanStack DB

124 lines (109 loc) 3.79 kB
import { createTransaction } from '@tanstack/db' import { NonRetriableError } from '../types' import type { PendingMutation, Transaction } from '@tanstack/db' import type { CreateOfflineTransactionOptions, OfflineMutationFn, OfflineTransaction as OfflineTransactionType, } from '../types' export class OfflineTransaction { private offlineId: string private mutationFnName: string private autoCommit: boolean private idempotencyKey: string private metadata: Record<string, any> private transaction: Transaction | null = null private persistTransaction: (tx: OfflineTransactionType) => Promise<void> private executor: any // Will be typed properly - reference to OfflineExecutor constructor( options: CreateOfflineTransactionOptions, mutationFn: OfflineMutationFn, persistTransaction: (tx: OfflineTransactionType) => Promise<void>, executor: any, ) { this.offlineId = crypto.randomUUID() this.mutationFnName = options.mutationFnName this.autoCommit = options.autoCommit ?? true this.idempotencyKey = options.idempotencyKey ?? crypto.randomUUID() this.metadata = options.metadata ?? {} this.persistTransaction = persistTransaction this.executor = executor } mutate(callback: () => void): Transaction { this.transaction = createTransaction({ id: this.offlineId, autoCommit: false, mutationFn: async () => { // This is the blocking mutationFn that waits for the executor // First persist the transaction to the outbox const offlineTransaction: OfflineTransactionType = { id: this.offlineId, mutationFnName: this.mutationFnName, mutations: this.transaction!.mutations, keys: this.extractKeys(this.transaction!.mutations), idempotencyKey: this.idempotencyKey, createdAt: new Date(), retryCount: 0, nextAttemptAt: Date.now(), metadata: this.metadata, spanContext: undefined, version: 1, } const completionPromise = this.executor.waitForTransactionCompletion( this.offlineId, ) try { await this.persistTransaction(offlineTransaction) // Now block and wait for the executor to complete the real mutation await completionPromise } catch (error) { const normalizedError = error instanceof Error ? error : new Error(String(error)) this.executor.rejectTransaction(this.offlineId, normalizedError) throw error } return }, metadata: this.metadata, }) this.transaction.mutate(() => { callback() }) if (this.autoCommit) { // Auto-commit for direct OfflineTransaction usage this.commit().catch((error) => { console.error(`Auto-commit failed:`, error) throw error }) } return this.transaction } async commit(): Promise<Transaction> { if (!this.transaction) { throw new Error(`No mutations to commit. Call mutate() first.`) } try { // Commit the TanStack DB transaction // This will trigger the mutationFn which handles persistence and waiting await this.transaction.commit() return this.transaction } catch (error) { // Only rollback for NonRetriableError - other errors should allow retry if (error instanceof NonRetriableError) { this.transaction.rollback() } throw error } } rollback(): void { if (this.transaction) { this.transaction.rollback() } } private extractKeys(mutations: Array<PendingMutation>): Array<string> { return mutations.map((mutation) => mutation.globalKey) } get id(): string { return this.offlineId } }