@tanstack/offline-transactions
Version:
Offline-first transaction capabilities for TanStack DB
169 lines (149 loc) • 4.35 kB
text/typescript
import type {
Collection,
MutationFnParams,
PendingMutation,
Transaction,
} from '@tanstack/db'
// Extended mutation function that includes idempotency key
export type OfflineMutationFnParams<
T extends object = Record<string, unknown>,
> = MutationFnParams<T> & {
idempotencyKey: string
}
export type OfflineMutationFn<T extends object = Record<string, unknown>> = (
params: OfflineMutationFnParams<T>,
) => Promise<any>
// Simplified mutation structure for serialization
export interface SerializedMutation {
globalKey: string
type: string
modified: any
original: any
changes: any
collectionId: string
}
export interface SerializedError {
name: string
message: string
stack?: string
}
export interface SerializedSpanContext {
traceId: string
spanId: string
traceFlags: number
traceState?: string
}
// In-memory representation with full PendingMutation objects
export interface OfflineTransaction {
id: string
mutationFnName: string
mutations: Array<PendingMutation>
keys: Array<string>
idempotencyKey: string
createdAt: Date
retryCount: number
nextAttemptAt: number
lastError?: SerializedError
metadata?: Record<string, any>
spanContext?: SerializedSpanContext
version: 1
}
// Serialized representation for storage
export interface SerializedOfflineTransaction {
id: string
mutationFnName: string
mutations: Array<SerializedMutation>
keys: Array<string>
idempotencyKey: string
createdAt: string
retryCount: number
nextAttemptAt: number
lastError?: SerializedError
metadata?: Record<string, any>
spanContext?: SerializedSpanContext
version: 1
}
// Storage diagnostics and mode
export type OfflineMode = `offline` | `online-only`
export type StorageDiagnosticCode =
| `STORAGE_AVAILABLE`
| `INDEXEDDB_UNAVAILABLE`
| `LOCALSTORAGE_UNAVAILABLE`
| `STORAGE_BLOCKED`
| `QUOTA_EXCEEDED`
| `UNKNOWN_ERROR`
export interface StorageDiagnostic {
code: StorageDiagnosticCode
mode: OfflineMode
message: string
error?: Error
}
export interface OfflineConfig {
collections: Record<string, Collection<any, any, any, any, any>>
mutationFns: Record<string, OfflineMutationFn>
storage?: StorageAdapter
maxConcurrency?: number
jitter?: boolean
beforeRetry?: (
transactions: Array<OfflineTransaction>,
) => Array<OfflineTransaction>
onUnknownMutationFn?: (name: string, tx: OfflineTransaction) => void
onLeadershipChange?: (isLeader: boolean) => void
onStorageFailure?: (diagnostic: StorageDiagnostic) => void
leaderElection?: LeaderElection
/**
* Custom online detector implementation.
* Defaults to WebOnlineDetector for browser environments.
* The '@tanstack/offline-transactions/react-native' entry point uses ReactNativeOnlineDetector automatically.
*/
onlineDetector?: OnlineDetector
}
export interface StorageAdapter {
get: (key: string) => Promise<string | null>
set: (key: string, value: string) => Promise<void>
delete: (key: string) => Promise<void>
keys: () => Promise<Array<string>>
clear: () => Promise<void>
}
export interface RetryPolicy {
calculateDelay: (retryCount: number) => number
shouldRetry: (error: Error, retryCount: number) => boolean
}
export interface LeaderElection {
requestLeadership: () => Promise<boolean>
releaseLeadership: () => void
isLeader: () => boolean
onLeadershipChange: (callback: (isLeader: boolean) => void) => () => void
}
export interface TransactionSignaler {
resolveTransaction: (transactionId: string, result: any) => void
rejectTransaction: (transactionId: string, error: Error) => void
registerRestorationTransaction: (
offlineTransactionId: string,
restorationTransaction: Transaction,
) => void
isOnline: () => boolean
}
export interface OnlineDetector {
subscribe: (callback: () => void) => () => void
notifyOnline: () => void
isOnline: () => boolean
dispose: () => void
}
export interface CreateOfflineTransactionOptions {
id?: string
mutationFnName: string
autoCommit?: boolean
idempotencyKey?: string
metadata?: Record<string, any>
}
export interface CreateOfflineActionOptions<T> {
mutationFnName: string
onMutate: (variables: T) => void
}
export class NonRetriableError extends Error {
constructor(message: string) {
super(message)
this.name = `NonRetriableError`
}
}