UNPKG

@tanstack/offline-transactions

Version:

Offline-first transaction capabilities for TanStack DB

160 lines (159 loc) • 5.42 kB
import { withSpan } from "../telemetry/tracer.js"; import { TransactionSerializer, MissingTemporalConstructorError } from "./TransactionSerializer.js"; class OutboxTransactionNotFoundError extends Error { } class OutboxManager { constructor(storage, collections) { this.keyPrefix = `tx:`; this.removalRevision = 0; this.activeReads = /* @__PURE__ */ new Set(); this.removedAtRevision = /* @__PURE__ */ new Map(); this.storage = storage; this.serializer = new TransactionSerializer(collections); } getStorageKey(id) { return `${this.keyPrefix}${id}`; } recordRemovals(ids) { this.removalRevision++; if (this.activeReads.size === 0) return; for (const id of ids) this.removedAtRevision.set(id, this.removalRevision); this.pruneRemovalHistory(); } pruneRemovalHistory() { if (this.activeReads.size === 0) { this.removedAtRevision.clear(); return; } let oldestRead = Number.POSITIVE_INFINITY; for (const read of this.activeReads) oldestRead = Math.min(oldestRead, read.revision); for (const [id, revision] of this.removedAtRevision) if (revision <= oldestRead) this.removedAtRevision.delete(id); } async add(transaction) { 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) { return withSpan(`outbox.get`, {}, 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) { if (error instanceof MissingTemporalConstructorError) { error.message = `transaction ${id}: ${error.message}`; throw error; } console.warn(`Failed to deserialize transaction ${id}:`, error); span.setAttribute(`result`, `deserialize_error`); return null; } }); } async getAll() { return this.withAll((transactions) => transactions); } async withAll(consume) { const read = { revision: this.removalRevision }; this.activeReads.add(read); try { return await 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 = []; 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) { if (error instanceof MissingTemporalConstructorError) { error.message = `transaction ${key.slice(this.keyPrefix.length)}: ${error.message}`; throw error; } console.warn( `Failed to deserialize transaction from key ${key}:`, error ); } } } const currentTransactions = transactions.filter( ({ id }) => (this.removedAtRevision.get(id) ?? 0) <= read.revision ).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()); return consume(currentTransactions); }); } finally { this.activeReads.delete(read); this.pruneRemovalHistory(); } } async getByKeys(keys) { const allTransactions = await this.getAll(); const keySet = new Set(keys); return allTransactions.filter( (transaction) => transaction.keys.some((key) => keySet.has(key)) ); } async update(id, updates) { return withSpan(`outbox.update`, {}, async () => { const existing = await this.get(id); if (!existing) { throw new OutboxTransactionNotFoundError(`Transaction ${id} not found`); } const updated = { ...existing, ...updates }; await this.add(updated); }); } async remove(id) { return withSpan(`outbox.remove`, {}, async () => { const key = this.getStorageKey(id); await this.storage.delete(key); this.recordRemovals([id]); }); } async removeMany(ids) { return withSpan(`outbox.removeMany`, { count: ids.length }, async () => { await Promise.all(ids.map((id) => this.remove(id))); }); } async clear() { const keys = await this.storage.keys(); const transactionKeys = keys.filter((key) => key.startsWith(this.keyPrefix)); await this.removeMany( transactionKeys.map((key) => key.slice(this.keyPrefix.length)) ); } async count() { const keys = await this.storage.keys(); return keys.filter((key) => key.startsWith(this.keyPrefix)).length; } } export { OutboxManager, OutboxTransactionNotFoundError }; //# sourceMappingURL=OutboxManager.js.map