UNPKG

@tanstack/offline-transactions

Version:

Offline-first transaction capabilities for TanStack DB

143 lines (120 loc) 4.23 kB
import { withSpan } from '../telemetry/tracer' import { TransactionSerializer } from './TransactionSerializer' import type { OfflineTransaction, StorageAdapter } from '../types' import type { Collection } from '@tanstack/db' export class OutboxManager { private storage: StorageAdapter private serializer: TransactionSerializer private keyPrefix = `tx:` constructor( storage: StorageAdapter, collections: Record<string, Collection<any, any, any, any, any>>, ) { this.storage = storage this.serializer = new TransactionSerializer(collections) } private getStorageKey(id: string): string { return `${this.keyPrefix}${id}` } async add(transaction: OfflineTransaction): Promise<void> { return withSpan( `outbox.add`, { 'transaction.id': transaction.id, 'transaction.mutationFnName': transaction.mutationFnName, 'transaction.keyCount': transaction.keys.length, }, async () => { const key = this.getStorageKey(transaction.id) const serialized = this.serializer.serialize(transaction) await this.storage.set(key, serialized) }, ) } async get(id: string): Promise<OfflineTransaction | null> { return withSpan(`outbox.get`, { 'transaction.id': id }, async (span) => { const key = this.getStorageKey(id) const data = await this.storage.get(key) if (!data) { span.setAttribute(`result`, `not_found`) return null } try { const transaction = this.serializer.deserialize(data) span.setAttribute(`result`, `found`) return transaction } catch (error) { console.warn(`Failed to deserialize transaction ${id}:`, error) span.setAttribute(`result`, `deserialize_error`) return null } }) } async getAll(): Promise<Array<OfflineTransaction>> { return withSpan(`outbox.getAll`, {}, async (span) => { const keys = await this.storage.keys() const transactionKeys = keys.filter((key) => key.startsWith(this.keyPrefix), ) span.setAttribute(`transactionCount`, transactionKeys.length) const transactions: Array<OfflineTransaction> = [] for (const key of transactionKeys) { const data = await this.storage.get(key) if (data) { try { const transaction = this.serializer.deserialize(data) transactions.push(transaction) } catch (error) { console.warn( `Failed to deserialize transaction from key ${key}:`, error, ) } } } return transactions.sort( (a, b) => a.createdAt.getTime() - b.createdAt.getTime(), ) }) } async getByKeys(keys: Array<string>): Promise<Array<OfflineTransaction>> { const allTransactions = await this.getAll() const keySet = new Set(keys) return allTransactions.filter((transaction) => transaction.keys.some((key) => keySet.has(key)), ) } async update( id: string, updates: Partial<OfflineTransaction>, ): Promise<void> { return withSpan(`outbox.update`, { 'transaction.id': id }, async () => { const existing = await this.get(id) if (!existing) { throw new Error(`Transaction ${id} not found`) } const updated = { ...existing, ...updates } await this.add(updated) }) } async remove(id: string): Promise<void> { return withSpan(`outbox.remove`, { 'transaction.id': id }, async () => { const key = this.getStorageKey(id) await this.storage.delete(key) }) } async removeMany(ids: Array<string>): Promise<void> { return withSpan(`outbox.removeMany`, { count: ids.length }, async () => { await Promise.all(ids.map((id) => this.remove(id))) }) } async clear(): Promise<void> { const keys = await this.storage.keys() const transactionKeys = keys.filter((key) => key.startsWith(this.keyPrefix)) await Promise.all(transactionKeys.map((key) => this.storage.delete(key))) } async count(): Promise<number> { const keys = await this.storage.keys() return keys.filter((key) => key.startsWith(this.keyPrefix)).length } }