Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cool-cups-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tanstack/db": patch
---

Add `getOptimisticInfo()` method to track optimistic state per record. Returns metadata including `isOptimistic` flag, original/modified states, changes delta, and active mutations array for building UI features like loading badges and diff views.
33 changes: 33 additions & 0 deletions packages/db/src/collection/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import type {
NonSingleResult,
OnLoadMoreOptions,
OperationConfig,
OptimisticInfo,
SingleResult,
SubscribeChangesOptions,
Transaction as TransactionType,
Expand Down Expand Up @@ -397,6 +398,38 @@ export class CollectionImpl<
return this._state.has(key)
}

/**
* Get optimistic state information for a record
* @param key - The key of the record to check
* @returns OptimisticInfo object with details about optimistic mutations, or undefined if record doesn't exist
* @example
* // Check if a record is being optimistically updated
* const info = collection.getOptimisticInfo(todoId)
* if (info?.isOptimistic) {
* return <Badge>Saving...</Badge>
* }
*
* @example
* // Show a diff view for optimistic changes
* const info = collection.getOptimisticInfo(todoId)
* if (info?.changes) {
* showDiff(info.original, info.modified, info.changes)
* }
*
* @example
* // Check mutation details
* const info = collection.getOptimisticInfo(todoId)
* if (info?.isOptimistic) {
* console.log(`${info.mutations.length} pending mutations`)
* info.mutations.forEach(m => {
* console.log(`- ${m.type} at ${m.createdAt}`)
* })
* }
*/
public getOptimisticInfo(key: TKey): OptimisticInfo<TOutput> | undefined {
return this._state.getOptimisticInfo(key)
}

/**
* Get the current size of the collection (cached)
*/
Expand Down
55 changes: 55 additions & 0 deletions packages/db/src/collection/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
ChangeMessage,
CollectionConfig,
OptimisticChangeMessage,
OptimisticInfo,
} from "../types"
import type { CollectionImpl } from "./index.js"
import type { CollectionLifecycleManager } from "./lifecycle"
Expand Down Expand Up @@ -119,6 +120,60 @@ export class CollectionStateManager<
return syncedData.has(key)
}

/**
* Get optimistic state information for a key
*/
public getOptimisticInfo(key: TKey): OptimisticInfo<TOutput> | undefined {
// Get the current value (which includes optimistic changes)
const modified = this.get(key)
if (modified === undefined) {
return undefined
}

// Get all active mutations for this key
const mutations: Array<any> = []
let original: TOutput | undefined
let changes: Partial<TOutput> | undefined

for (const transaction of this.transactions.values()) {
if ([`completed`, `failed`].includes(transaction.state)) {
continue
}

for (const mutation of transaction.mutations) {
if (
this.isThisCollection(mutation.collection) &&
mutation.key === key &&
mutation.optimistic
) {
mutations.push(mutation)

// Track the original value (from the first mutation)
if (original === undefined && mutation.type !== `insert`) {
original = mutation.original as TOutput
}

// For updates, accumulate changes
if (mutation.type === `update`) {
if (changes === undefined) {
changes = { ...mutation.changes } as Partial<TOutput>
} else {
changes = { ...changes, ...mutation.changes } as Partial<TOutput>
}
}
}
}
}

return {
isOptimistic: mutations.length > 0,
original,
modified,
changes,
mutations,
}
}

/**
* Get all keys (virtual derived state)
*/
Expand Down
17 changes: 17 additions & 0 deletions packages/db/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,23 @@ export type ChangeListener<
TKey extends string | number = string | number,
> = (changes: Array<ChangeMessage<T, TKey>>) => void

/**
* Information about the optimistic state of a record
* @template T - The type of the record
*/
export interface OptimisticInfo<T extends object = Record<string, unknown>> {
/** Whether this record has any active optimistic mutations */
isOptimistic: boolean
/** The pre-mutation state (undefined if optimistic insert or not optimistic) */
original?: T
/** The current state of the record (after applying all optimistic mutations) */
modified: T
/** The delta changes (only present for optimistic updates) */
changes?: Partial<T>
/** All active mutations affecting this record */
mutations: Array<PendingMutation<T>>
}

// Adapted from https://github.com/sindresorhus/type-fest
// MIT License Copyright (c) Sindre Sorhus

Expand Down
Loading
Loading