Tldraw/packages/state/src/lib/core/Atom.ts

206 wiersze
5.5 KiB
TypeScript

import { ArraySet } from './ArraySet'
import { HistoryBuffer } from './HistoryBuffer'
import { maybeCaptureParent } from './capture'
import { EMPTY_ARRAY, equals } from './helpers'
import { advanceGlobalEpoch, atomDidChange, globalEpoch } from './transactions'
import { Child, ComputeDiff, RESET_VALUE, Signal } from './types'
import { logDotValueWarning } from './warnings'
/**
* The options to configure an atom, passed into the [[atom]] function.
* @public
*/
export interface AtomOptions<Value, Diff> {
/**
* The maximum number of diffs to keep in the history buffer.
*
* If you don't need to compute diffs, or if you will supply diffs manually via [[Atom.set]], you can leave this as `undefined` and no history buffer will be created.
*
* If you expect the value to be part of an active effect subscription all the time, and to not change multiple times inside of a single transaction, you can set this to a relatively low number (e.g. 10).
*
* Otherwise, set this to a higher number based on your usage pattern and memory constraints.
*
*/
historyLength?: number
/**
* A method used to compute a diff between the atom's old and new values. If provided, it will not be used unless you also specify [[AtomOptions.historyLength]].
*/
computeDiff?: ComputeDiff<Value, Diff>
/**
* If provided, this will be used to compare the old and new values of the atom to determine if the value has changed.
* By default, values are compared using first using strict equality (`===`), then `Object.is`, and finally any `.equals` method present in the object's prototype chain.
* @param a - The old value
* @param b - The new value
* @returns
*/
isEqual?: (a: any, b: any) => boolean
}
/**
* An Atom is a signal that can be updated directly by calling [[Atom.set]] or [[Atom.update]].
*
* Atoms are created using the [[atom]] function.
*
* @example
* ```ts
* const name = atom('name', 'John')
*
* print(name.value) // 'John'
* ```
*
* @public
*/
export interface Atom<Value, Diff = unknown> extends Signal<Value, Diff> {
/**
* Sets the value of this atom to the given value. If the value is the same as the current value, this is a no-op.
*
* @param value - The new value to set.
* @param diff - The diff to use for the update. If not provided, the diff will be computed using [[AtomOptions.computeDiff]].
*/
set(value: Value, diff?: Diff): Value
/**
* Updates the value of this atom using the given updater function. If the returned value is the same as the current value, this is a no-op.
*
* @param updater - A function that takes the current value and returns the new value.
*/
update(updater: (value: Value) => Value): Value
}
/**
* @internal
*/
export class _Atom<Value, Diff = unknown> implements Atom<Value, Diff> {
constructor(
public readonly name: string,
private current: Value,
options?: AtomOptions<Value, Diff>
) {
this.isEqual = options?.isEqual ?? null
if (!options) return
if (options.historyLength) {
this.historyBuffer = new HistoryBuffer(options.historyLength)
}
this.computeDiff = options.computeDiff
}
readonly isEqual: null | ((a: any, b: any) => boolean)
computeDiff?: ComputeDiff<Value, Diff>
lastChangedEpoch = globalEpoch
children = new ArraySet<Child>()
historyBuffer?: HistoryBuffer<Diff>
__unsafe__getWithoutCapture(): Value {
return this.current
}
get() {
maybeCaptureParent(this)
return this.current
}
/**
* @deprecated Use [[Atom.get]] instead.
*/
get value() {
logDotValueWarning()
return this.get()
}
set(value: Value, diff?: Diff): Value {
// If the value has not changed, do nothing.
if (this.isEqual?.(this.current, value) ?? equals(this.current, value)) {
return this.current
}
// Tick forward the global epoch
advanceGlobalEpoch()
// Add the diff to the history buffer.
if (this.historyBuffer) {
this.historyBuffer.pushEntry(
this.lastChangedEpoch,
globalEpoch,
diff ??
this.computeDiff?.(this.current, value, this.lastChangedEpoch, globalEpoch) ??
RESET_VALUE
)
}
// Update the atom's record of the epoch when last changed.
this.lastChangedEpoch = globalEpoch
const oldValue = this.current
this.current = value
// Notify all children that this atom has changed.
atomDidChange(this, oldValue)
return value
}
update(updater: (value: Value) => Value): Value {
return this.set(updater(this.current))
}
getDiffSince(epoch: number): RESET_VALUE | Diff[] {
maybeCaptureParent(this)
// If no changes have occurred since the given epoch, return an empty array.
if (epoch >= this.lastChangedEpoch) {
return EMPTY_ARRAY
}
return this.historyBuffer?.getChangesSince(epoch) ?? RESET_VALUE
}
}
/**
* Creates a new [[Atom]].
*
* An Atom is a signal that can be updated directly by calling [[Atom.set]] or [[Atom.update]].
*
* @example
* ```ts
* const name = atom('name', 'John')
*
* name.value // 'John'
*
* name.set('Jane')
*
* name.value // 'Jane'
* ```
*
* @public
*/
export function atom<Value, Diff = unknown>(
/**
* A name for the signal. This is used for debugging and profiling purposes, it does not need to be unique.
*/
name: string,
/**
* The initial value of the signal.
*/
initialValue: Value,
/**
* The options to configure the atom. See [[AtomOptions]].
*/
options?: AtomOptions<Value, Diff>
): Atom<Value, Diff> {
return new _Atom(name, initialValue, options)
}
/**
* Returns true if the given value is an [[Atom]].
* @public
*/
export function isAtom(value: unknown): value is Atom<unknown> {
return value instanceof _Atom
}