kopia lustrzana https://github.com/Tldraw/Tldraw
alex/auto-undo-redo: nullable RecordsDiff
rodzic
463957ae94
commit
a631642b2d
|
@ -1,13 +1,7 @@
|
|||
export type { BaseRecord, IdOf, RecordId, UnknownRecord } from './lib/BaseRecord'
|
||||
export { IncrementalSetConstructor } from './lib/IncrementalSetConstructor'
|
||||
export { RecordType, assertIdType, createRecordType } from './lib/RecordType'
|
||||
export {
|
||||
isRecordsDiffEmpty,
|
||||
reverseRecordsDiff,
|
||||
squashRecordDiffs,
|
||||
squashRecordDiffsMutable,
|
||||
type RecordsDiff,
|
||||
} from './lib/RecordsDiff'
|
||||
export { RecordsDiff } from './lib/RecordsDiff'
|
||||
export { Store } from './lib/Store'
|
||||
export type {
|
||||
CollectionDiff,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { objectMapEntries } from '@tldraw/utils'
|
||||
import { objectMapValues } from '@tldraw/utils'
|
||||
import { IdOf, UnknownRecord } from './BaseRecord'
|
||||
|
||||
/**
|
||||
|
@ -7,96 +7,135 @@ import { IdOf, UnknownRecord } from './BaseRecord'
|
|||
* @public
|
||||
*/
|
||||
export type RecordsDiff<R extends UnknownRecord> = {
|
||||
added: Record<IdOf<R>, R>
|
||||
updated: Record<IdOf<R>, [from: R, to: R]>
|
||||
removed: Record<IdOf<R>, R>
|
||||
added: Record<IdOf<R>, R> | null
|
||||
updated: Record<IdOf<R>, [from: R, to: R]> | null
|
||||
removed: Record<IdOf<R>, R> | null
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function reverseRecordsDiff(diff: RecordsDiff<any>) {
|
||||
const result: RecordsDiff<any> = { added: diff.removed, removed: diff.added, updated: {} }
|
||||
for (const [from, to] of Object.values(diff.updated)) {
|
||||
result.updated[from.id] = [to, from]
|
||||
}
|
||||
return result
|
||||
}
|
||||
export const RecordsDiff = {
|
||||
/** Create an empty RecordsDiff */
|
||||
create<R extends UnknownRecord>(): RecordsDiff<R> {
|
||||
return { added: null, updated: null, removed: null } as RecordsDiff<R>
|
||||
},
|
||||
|
||||
/**
|
||||
* Is a records diff empty?
|
||||
* @internal
|
||||
*/
|
||||
export function isRecordsDiffEmpty<T extends UnknownRecord>(diff: RecordsDiff<T>) {
|
||||
return (
|
||||
Object.keys(diff.added).length === 0 &&
|
||||
Object.keys(diff.updated).length === 0 &&
|
||||
Object.keys(diff.removed).length === 0
|
||||
)
|
||||
}
|
||||
/**
|
||||
* Reverse a record diff. Applying the reversed diff will undo the changes of the original diff.
|
||||
*/
|
||||
reverse<R extends UnknownRecord>(diff: RecordsDiff<R>): RecordsDiff<R> {
|
||||
const result: RecordsDiff<any> = { added: diff.removed, removed: diff.added, updated: null }
|
||||
if (diff.updated) {
|
||||
result.updated = {}
|
||||
for (const [from, to] of objectMapValues(diff.updated)) {
|
||||
result.updated[from.id] = [to, from]
|
||||
}
|
||||
}
|
||||
return result
|
||||
},
|
||||
|
||||
/**
|
||||
* Squash a collection of diffs into a single diff.
|
||||
*
|
||||
* @param diffs - An array of diffs to squash.
|
||||
* @returns A single diff that represents the squashed diffs.
|
||||
* @public
|
||||
*/
|
||||
export function squashRecordDiffs<T extends UnknownRecord>(
|
||||
diffs: RecordsDiff<T>[]
|
||||
): RecordsDiff<T> {
|
||||
const result = { added: {}, removed: {}, updated: {} } as RecordsDiff<T>
|
||||
/**
|
||||
* Check if a diff is completely empty.
|
||||
*/
|
||||
isEmpty<R extends UnknownRecord>(diff: RecordsDiff<R>): boolean {
|
||||
return !diff.added && !diff.updated && !diff.removed
|
||||
},
|
||||
|
||||
squashRecordDiffsMutable(result, diffs)
|
||||
return result
|
||||
}
|
||||
/**
|
||||
* Squash a collection of diffs into a single diff.
|
||||
*
|
||||
* @param diffs - An array of diffs to squash.
|
||||
* @returns A single diff that represents the squashed diffs.
|
||||
* @public
|
||||
*/
|
||||
squash<R extends UnknownRecord>(diffs: RecordsDiff<R>[]): RecordsDiff<R> {
|
||||
const result = RecordsDiff.create<R>()
|
||||
for (const diff of diffs) {
|
||||
RecordsDiff.applyInPlace(result, diff)
|
||||
}
|
||||
return result
|
||||
},
|
||||
|
||||
/**
|
||||
* Apply the array `diffs` to the `target` diff, mutating it in-place.
|
||||
* @internal
|
||||
*/
|
||||
export function squashRecordDiffsMutable<T extends UnknownRecord>(
|
||||
target: RecordsDiff<T>,
|
||||
diffs: RecordsDiff<T>[]
|
||||
): void {
|
||||
for (const diff of diffs) {
|
||||
for (const [id, value] of objectMapEntries(diff.added)) {
|
||||
if (target.removed[id]) {
|
||||
const original = target.removed[id]
|
||||
delete target.removed[id]
|
||||
if (original !== value) {
|
||||
target.updated[id] = [original, value]
|
||||
}
|
||||
} else {
|
||||
target.added[id] = value
|
||||
/**
|
||||
* Apply the array `diffs` to the `target` diff, mutating it in-place.
|
||||
* @internal
|
||||
*/
|
||||
applyInPlace<T extends UnknownRecord>(target: RecordsDiff<T>, diff: RecordsDiff<T>): void {
|
||||
if (diff.added) {
|
||||
for (const record of objectMapValues(diff.added)) {
|
||||
RecordsDiff.addRecord(target, record)
|
||||
}
|
||||
}
|
||||
|
||||
for (const [id, [_from, to]] of objectMapEntries(diff.updated)) {
|
||||
if (target.added[id]) {
|
||||
target.added[id] = to
|
||||
delete target.updated[id]
|
||||
delete target.removed[id]
|
||||
continue
|
||||
}
|
||||
if (target.updated[id]) {
|
||||
target.updated[id] = [target.updated[id][0], to]
|
||||
delete target.removed[id]
|
||||
continue
|
||||
if (diff.updated) {
|
||||
for (const update of objectMapValues(diff.updated)) {
|
||||
RecordsDiff.updateRecord(target, update[0], update[1])
|
||||
}
|
||||
}
|
||||
|
||||
target.updated[id] = diff.updated[id]
|
||||
if (diff.removed) {
|
||||
for (const value of objectMapValues(diff.removed)) {
|
||||
RecordsDiff.removeRecord(target, value)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Record `record` as being added in diff `target`. Updates the diff in-place.
|
||||
* @internal
|
||||
*/
|
||||
addRecord<R extends UnknownRecord>(target: RecordsDiff<R>, record: R): void {
|
||||
const id: IdOf<R> = record.id
|
||||
if (target.removed?.[id]) {
|
||||
const original = target.removed[id]
|
||||
delete target.removed[id]
|
||||
if (original !== record) {
|
||||
if (!target.updated) target.updated = {} as Record<IdOf<R>, [R, R]>
|
||||
target.updated[id] = [original, record]
|
||||
}
|
||||
} else {
|
||||
if (!target.added) target.added = {} as Record<IdOf<R>, R>
|
||||
target.added[id] = record
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Record `record` as being updated in diff `target`. Updates the diff in-place.
|
||||
* @internal
|
||||
*/
|
||||
updateRecord<R extends UnknownRecord>(target: RecordsDiff<R>, from: R, to: R) {
|
||||
const id: IdOf<R> = from.id
|
||||
if (target.added?.[id]) {
|
||||
target.added[id] = to
|
||||
if (target.updated) delete target.updated[id]
|
||||
if (target.removed) delete target.removed[id]
|
||||
return
|
||||
}
|
||||
if (target.updated?.[id]) {
|
||||
target.updated[id][1] = to
|
||||
if (target.removed) delete target.removed[id]
|
||||
return
|
||||
}
|
||||
|
||||
for (const [id, value] of objectMapEntries(diff.removed)) {
|
||||
// the same record was added in this diff sequence, just drop it
|
||||
if (target.added[id]) {
|
||||
delete target.added[id]
|
||||
} else if (target.updated[id]) {
|
||||
target.removed[id] = target.updated[id][0]
|
||||
delete target.updated[id]
|
||||
} else {
|
||||
target.removed[id] = value
|
||||
}
|
||||
if (!target.updated) target.updated = {} as Record<IdOf<R>, [R, R]>
|
||||
target.updated[id] = [from, to]
|
||||
if (target.removed) delete target.removed[id]
|
||||
},
|
||||
|
||||
/**
|
||||
* Record `record` as being removed in diff `target`. Updates the diff in-place.
|
||||
* @internal
|
||||
*/
|
||||
removeRecord<R extends UnknownRecord>(target: RecordsDiff<R>, record: R) {
|
||||
const id: IdOf<R> = record.id
|
||||
// the same record was added in this diff sequence, just drop it
|
||||
if (target.added?.[id]) {
|
||||
delete target.added[id]
|
||||
return
|
||||
}
|
||||
}
|
||||
if (target.updated?.[id]) {
|
||||
delete target.updated[id]
|
||||
}
|
||||
if (!target.removed) target.removed = {} as Record<IdOf<R>, R>
|
||||
target.removed[id] = record
|
||||
},
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import { nanoid } from 'nanoid'
|
|||
import { IdOf, RecordId, UnknownRecord } from './BaseRecord'
|
||||
import { Cache } from './Cache'
|
||||
import { RecordScope } from './RecordType'
|
||||
import { RecordsDiff, squashRecordDiffs, squashRecordDiffsMutable } from './RecordsDiff'
|
||||
import { RecordsDiff } from './RecordsDiff'
|
||||
import { StoreQueries } from './StoreQueries'
|
||||
import { SerializedSchema, StoreSchema } from './StoreSchema'
|
||||
import { devFreeze } from './devFreeze'
|
||||
|
@ -258,19 +258,22 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|||
* @param change - the records diff
|
||||
* @returns
|
||||
*/
|
||||
filterChangesByScope(change: RecordsDiff<R>, scope: RecordScope) {
|
||||
filterChangesByScope(change: RecordsDiff<R>, scope: RecordScope): RecordsDiff<R> | null {
|
||||
const result = {
|
||||
added: filterEntries(change.added, (_, r) => this.scopedTypes[scope].has(r.typeName)),
|
||||
updated: filterEntries(change.updated, (_, r) => this.scopedTypes[scope].has(r[1].typeName)),
|
||||
removed: filterEntries(change.removed, (_, r) => this.scopedTypes[scope].has(r.typeName)),
|
||||
}
|
||||
if (
|
||||
Object.keys(result.added).length === 0 &&
|
||||
Object.keys(result.updated).length === 0 &&
|
||||
Object.keys(result.removed).length === 0
|
||||
) {
|
||||
return null
|
||||
added: change.added
|
||||
? filterEntries(change.added, (_, r) => this.scopedTypes[scope].has(r.typeName))
|
||||
: null,
|
||||
updated: change.updated
|
||||
? filterEntries(change.updated, (_, r) => this.scopedTypes[scope].has(r[1].typeName))
|
||||
: null,
|
||||
removed: change.removed
|
||||
? filterEntries(change.removed, (_, r) => this.scopedTypes[scope].has(r.typeName))
|
||||
: null,
|
||||
}
|
||||
if (result.added && Object.keys(result.added).length === 0) result.added = null
|
||||
if (result.updated && Object.keys(result.updated).length === 0) result.updated = null
|
||||
if (result.removed && Object.keys(result.removed).length === 0) result.removed = null
|
||||
if (RecordsDiff.isEmpty(result)) return null
|
||||
return result
|
||||
}
|
||||
|
||||
|
@ -702,23 +705,7 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|||
const dispose = this.historyAccumulator.addInterceptor((entry) => changes.push(entry.changes))
|
||||
try {
|
||||
transact(fn)
|
||||
return squashRecordDiffs(changes)
|
||||
} finally {
|
||||
dispose()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Like {@link extractingChanges}, but accumulates the changes into the provided accumulator
|
||||
* diff instead of returning a fresh diff.
|
||||
* @internal
|
||||
*/
|
||||
accumulatingChanges(accumulator: RecordsDiff<R>, fn: () => void) {
|
||||
const changes: Array<RecordsDiff<R>> = []
|
||||
const dispose = this.historyAccumulator.addInterceptor((entry) => changes.push(entry.changes))
|
||||
try {
|
||||
transact(fn)
|
||||
squashRecordDiffsMutable(accumulator, changes)
|
||||
return RecordsDiff.squash(changes)
|
||||
} finally {
|
||||
dispose()
|
||||
}
|
||||
|
@ -726,10 +713,10 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|||
|
||||
applyDiff(diff: RecordsDiff<R>, runCallbacks = true) {
|
||||
this.transactWithAfterEvents(() => {
|
||||
const toPut = objectMapValues(diff.added).concat(
|
||||
objectMapValues(diff.updated).map(([_from, to]) => to)
|
||||
)
|
||||
const toRemove = objectMapKeys(diff.removed)
|
||||
const toAdd = diff.added ? objectMapValues(diff.added) : []
|
||||
const toUpdate = diff.updated ? objectMapValues(diff.updated).map((u) => u[1]) : []
|
||||
const toPut = toAdd.concat(toUpdate)
|
||||
const toRemove = diff.removed ? objectMapKeys(diff.removed) : []
|
||||
if (toPut.length) {
|
||||
this.put(toPut)
|
||||
}
|
||||
|
@ -922,7 +909,7 @@ function squashHistoryEntries<T extends UnknownRecord>(
|
|||
return devFreeze(
|
||||
chunked.map((chunk) => ({
|
||||
source: chunk[0].source,
|
||||
changes: squashRecordDiffs(chunk.map((e) => e.changes)),
|
||||
changes: RecordsDiff.squash(chunk.map((e) => e.changes)),
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
|
|
@ -81,63 +81,35 @@ export class StoreQueries<R extends UnknownRecord> {
|
|||
const diff = this.history.getDiffSince(lastComputedEpoch)
|
||||
if (diff === RESET_VALUE) return this.history.get()
|
||||
|
||||
const res = { added: {}, removed: {}, updated: {} } as RecordsDiff<S>
|
||||
let numAdded = 0
|
||||
let numRemoved = 0
|
||||
let numUpdated = 0
|
||||
const res = RecordsDiff.create<S>()
|
||||
|
||||
for (const changes of diff) {
|
||||
for (const added of objectMapValues(changes.added)) {
|
||||
if (added.typeName === typeName) {
|
||||
if (res.removed[added.id as IdOf<S>]) {
|
||||
const original = res.removed[added.id as IdOf<S>]
|
||||
delete res.removed[added.id as IdOf<S>]
|
||||
numRemoved--
|
||||
if (original !== added) {
|
||||
res.updated[added.id as IdOf<S>] = [original, added as S]
|
||||
numUpdated++
|
||||
}
|
||||
} else {
|
||||
res.added[added.id as IdOf<S>] = added as S
|
||||
numAdded++
|
||||
if (changes.added) {
|
||||
for (const added of objectMapValues(changes.added)) {
|
||||
if (added.typeName === typeName) {
|
||||
RecordsDiff.addRecord(res, added)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [from, to] of objectMapValues(changes.updated)) {
|
||||
if (to.typeName === typeName) {
|
||||
if (res.added[to.id as IdOf<S>]) {
|
||||
res.added[to.id as IdOf<S>] = to as S
|
||||
} else if (res.updated[to.id as IdOf<S>]) {
|
||||
res.updated[to.id as IdOf<S>] = [res.updated[to.id as IdOf<S>][0], to as S]
|
||||
} else {
|
||||
res.updated[to.id as IdOf<S>] = [from as S, to as S]
|
||||
numUpdated++
|
||||
if (changes.updated) {
|
||||
for (const [from, to] of objectMapValues(changes.updated)) {
|
||||
if (to.typeName === typeName) {
|
||||
RecordsDiff.updateRecord(res, from, to)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const removed of objectMapValues(changes.removed)) {
|
||||
if (removed.typeName === typeName) {
|
||||
if (res.added[removed.id as IdOf<S>]) {
|
||||
// was added during this diff sequence, so just undo the add
|
||||
delete res.added[removed.id as IdOf<S>]
|
||||
numAdded--
|
||||
} else if (res.updated[removed.id as IdOf<S>]) {
|
||||
// remove oldest version
|
||||
res.removed[removed.id as IdOf<S>] = res.updated[removed.id as IdOf<S>][0]
|
||||
delete res.updated[removed.id as IdOf<S>]
|
||||
numUpdated--
|
||||
numRemoved++
|
||||
} else {
|
||||
res.removed[removed.id as IdOf<S>] = removed as S
|
||||
numRemoved++
|
||||
if (changes.removed) {
|
||||
for (const removed of objectMapValues(changes.removed)) {
|
||||
if (removed.typeName === typeName) {
|
||||
RecordsDiff.removeRecord(res, removed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (numAdded || numRemoved || numUpdated) {
|
||||
if (!RecordsDiff.isEmpty(res)) {
|
||||
return withDiff(this.history.get(), res)
|
||||
} else {
|
||||
return lastValue
|
||||
|
@ -239,26 +211,32 @@ export class StoreQueries<R extends UnknownRecord> {
|
|||
}
|
||||
|
||||
for (const changes of history) {
|
||||
for (const record of objectMapValues(changes.added)) {
|
||||
if (record.typeName === typeName) {
|
||||
const value = (record as S)[property]
|
||||
add(value, record.id)
|
||||
}
|
||||
}
|
||||
for (const [from, to] of objectMapValues(changes.updated)) {
|
||||
if (to.typeName === typeName) {
|
||||
const prev = (from as S)[property]
|
||||
const next = (to as S)[property]
|
||||
if (prev !== next) {
|
||||
remove(prev, to.id)
|
||||
add(next, to.id)
|
||||
if (changes.added) {
|
||||
for (const record of objectMapValues(changes.added)) {
|
||||
if (record.typeName === typeName) {
|
||||
const value = (record as S)[property]
|
||||
add(value, record.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const record of objectMapValues(changes.removed)) {
|
||||
if (record.typeName === typeName) {
|
||||
const value = (record as S)[property]
|
||||
remove(value, record.id)
|
||||
if (changes.updated) {
|
||||
for (const [from, to] of objectMapValues(changes.updated)) {
|
||||
if (to.typeName === typeName) {
|
||||
const prev = (from as S)[property]
|
||||
const next = (to as S)[property]
|
||||
if (prev !== next) {
|
||||
remove(prev, to.id)
|
||||
add(next, to.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (changes.removed) {
|
||||
for (const record of objectMapValues(changes.removed)) {
|
||||
if (record.typeName === typeName) {
|
||||
const value = (record as S)[property]
|
||||
remove(value, record.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -416,23 +394,29 @@ export class StoreQueries<R extends UnknownRecord> {
|
|||
) as IncrementalSetConstructor<IdOf<S>>
|
||||
|
||||
for (const changes of history) {
|
||||
for (const added of objectMapValues(changes.added)) {
|
||||
if (added.typeName === typeName && objectMatchesQuery(query, added)) {
|
||||
setConstructor.add(added.id)
|
||||
}
|
||||
}
|
||||
for (const [_, updated] of objectMapValues(changes.updated)) {
|
||||
if (updated.typeName === typeName) {
|
||||
if (objectMatchesQuery(query, updated)) {
|
||||
setConstructor.add(updated.id)
|
||||
} else {
|
||||
setConstructor.remove(updated.id)
|
||||
if (changes.added) {
|
||||
for (const added of objectMapValues(changes.added)) {
|
||||
if (added.typeName === typeName && objectMatchesQuery(query, added)) {
|
||||
setConstructor.add(added.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const removed of objectMapValues(changes.removed)) {
|
||||
if (removed.typeName === typeName) {
|
||||
setConstructor.remove(removed.id)
|
||||
if (changes.updated) {
|
||||
for (const [_, updated] of objectMapValues(changes.updated)) {
|
||||
if (updated.typeName === typeName) {
|
||||
if (objectMatchesQuery(query, updated)) {
|
||||
setConstructor.add(updated.id)
|
||||
} else {
|
||||
setConstructor.remove(updated.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (changes.removed) {
|
||||
for (const removed of objectMapValues(changes.removed)) {
|
||||
if (removed.typeName === typeName) {
|
||||
setConstructor.remove(removed.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue