@tanstack/offline-transactions
Version:
Offline-first transaction capabilities for TanStack DB
174 lines (151 loc) • 5.24 kB
text/typescript
import type {
OfflineTransaction,
SerializedError,
SerializedMutation,
SerializedOfflineTransaction,
} from '../types'
import type { Collection, PendingMutation } from '@tanstack/db'
export class TransactionSerializer {
private collections: Record<string, Collection<any, any, any, any, any>>
private collectionIdToKey: Map<string, string>
constructor(
collections: Record<string, Collection<any, any, any, any, any>>,
) {
this.collections = collections
// Create reverse lookup from collection.id to registry key
this.collectionIdToKey = new Map()
for (const [key, collection] of Object.entries(collections)) {
this.collectionIdToKey.set(collection.id, key)
}
}
serialize(transaction: OfflineTransaction): string {
const serialized: SerializedOfflineTransaction = {
...transaction,
createdAt: transaction.createdAt.toISOString(),
mutations: transaction.mutations.map((mutation) =>
this.serializeMutation(mutation),
),
}
return JSON.stringify(serialized)
}
deserialize(data: string): OfflineTransaction {
// Parse without a reviver - let deserializeValue handle dates in mutation data
// using the { __type: 'Date' } marker system
const parsed: SerializedOfflineTransaction = JSON.parse(data)
const createdAt = new Date(parsed.createdAt)
if (isNaN(createdAt.getTime())) {
throw new Error(
`Failed to deserialize transaction: invalid createdAt value "${parsed.createdAt}"`,
)
}
return {
...parsed,
createdAt,
mutations: parsed.mutations.map((mutationData) =>
this.deserializeMutation(mutationData),
),
}
}
private serializeMutation(mutation: PendingMutation): SerializedMutation {
const registryKey = this.collectionIdToKey.get(mutation.collection.id)
if (!registryKey) {
throw new Error(
`Collection with id ${mutation.collection.id} not found in registry`,
)
}
return {
globalKey: mutation.globalKey,
type: mutation.type,
modified: this.serializeValue(mutation.modified),
original: this.serializeValue(mutation.original),
changes: this.serializeValue(mutation.changes),
collectionId: registryKey, // Store registry key instead of collection.id
}
}
private deserializeMutation(data: SerializedMutation): PendingMutation {
const collection = this.collections[data.collectionId]
if (!collection) {
throw new Error(`Collection with id ${data.collectionId} not found`)
}
const modified = this.deserializeValue(data.modified)
// Extract the key from the modified data using the collection's getKey function
// This is needed for optimistic state restoration to work correctly
const key = modified ? collection.getKeyFromItem(modified) : null
// Create a partial PendingMutation - we can't fully reconstruct it but
// we provide what we can. The executor will need to handle the rest.
return {
globalKey: data.globalKey,
type: data.type as any,
modified,
original: this.deserializeValue(data.original),
changes: this.deserializeValue(data.changes) ?? {},
collection,
// These fields would need to be reconstructed by the executor
mutationId: ``, // Will be regenerated
key,
metadata: undefined,
syncMetadata: {},
optimistic: true,
createdAt: new Date(),
updatedAt: new Date(),
} as PendingMutation
}
private serializeValue(value: any): any {
if (value === null || value === undefined) {
return value
}
if (value instanceof Date) {
return { __type: `Date`, value: value.toISOString() }
}
if (typeof value === `object`) {
const result: any = Array.isArray(value) ? [] : {}
for (const key in value) {
if (Object.prototype.hasOwnProperty.call(value, key)) {
result[key] = this.serializeValue(value[key])
}
}
return result
}
return value
}
private deserializeValue(value: any): any {
if (value === null || value === undefined) {
return value
}
if (typeof value === `object` && value.__type === `Date`) {
if (value.value === undefined || value.value === null) {
throw new Error(`Corrupted Date marker: missing value field`)
}
const date = new Date(value.value)
if (isNaN(date.getTime())) {
throw new Error(
`Failed to deserialize Date marker: invalid date value "${value.value}"`,
)
}
return date
}
if (typeof value === `object`) {
const result: any = Array.isArray(value) ? [] : {}
for (const key in value) {
if (Object.prototype.hasOwnProperty.call(value, key)) {
result[key] = this.deserializeValue(value[key])
}
}
return result
}
return value
}
serializeError(error: Error): SerializedError {
return {
name: error.name,
message: error.message,
stack: error.stack,
}
}
deserializeError(data: SerializedError): Error {
const error = new Error(data.message)
error.name = data.name
error.stack = data.stack
return error
}
}