Fix TSDoc for @tldraw/state (#2327)

This PR opts to split the big singleton out into other smaller
singletons so that we can revert the moving of the tsdoc comments that
happened in #2322

### Change Type

- [x] `patch` — Bug fix


[^1]: publishes a `patch` release, for devDependencies use `internal`
[^2]: will not publish a new version
This commit is contained in:
David Sheldrick 2023-12-18 10:57:37 +00:00 committed by GitHub
parent 4e50c9c162
commit 0b434d61f0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 1411 additions and 925 deletions

View file

@ -14,26 +14,10 @@ export interface Atom<Value, Diff = unknown> extends Signal<Value, Diff> {
}
// @public
export const
/**
* 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.get() // 'John'
*
* name.set('Jane')
*
* name.get() // 'Jane'
* ```
*
* @public
*/
atom: typeof atom_2;
export function atom<Value, Diff = unknown>(
name: string,
initialValue: Value,
options?: AtomOptions<Value, Diff>): Atom<Value, Diff>;
// @public
export interface AtomOptions<Value, Diff> {
@ -52,26 +36,13 @@ export interface Computed<Value, Diff = unknown> extends Signal<Value, Diff> {
}
// @public
export const
/**
* 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.get() // 'John'
*
* name.set('Jane')
*
* name.get() // 'Jane'
* ```
*
* @public
*/
computed: typeof computed_2;
export function computed<Value, Diff = unknown>(name: string, compute: (previousValue: typeof UNINITIALIZED | Value, lastComputedEpoch: number) => Value | WithDiff<Value, Diff>, options?: ComputedOptions<Value, Diff>): Computed<Value, Diff>;
// @public (undocumented)
export function computed(target: any, key: string, descriptor: PropertyDescriptor): PropertyDescriptor;
// @public (undocumented)
export function computed<Value, Diff = unknown>(options?: ComputedOptions<Value, Diff>): (target: any, key: string, descriptor: PropertyDescriptor) => PropertyDescriptor;
// @public
export interface ComputedOptions<Value, Diff> {
@ -81,124 +52,32 @@ export interface ComputedOptions<Value, Diff> {
}
// @public
export const
/**
* 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.get() // 'John'
*
* name.set('Jane')
*
* name.get() // 'Jane'
* ```
*
* @public
*/
EffectScheduler: typeof EffectScheduler_2;
export const EffectScheduler: typeof __EffectScheduler__;
// @public (undocumented)
export const
/**
* 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.get() // 'John'
*
* name.set('Jane')
*
* name.get() // 'Jane'
* ```
*
* @public
*/
EMPTY_ARRAY: [];
export type EffectScheduler<Result> = __EffectScheduler__<Result>;
// @public (undocumented)
export const EMPTY_ARRAY: [];
// @public
export function getComputedInstance<Obj extends object, Prop extends keyof Obj>(obj: Obj, propertyName: Prop): Computed<Obj[Prop]>;
// @public
export const
/**
* 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.get() // 'John'
*
* name.set('Jane')
*
* name.get() // 'Jane'
* ```
*
* @public
*/
isAtom: typeof isAtom_2;
export function isAtom(value: unknown): value is Atom<unknown>;
// @public
export const
/**
* 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.get() // 'John'
*
* name.set('Jane')
*
* name.get() // 'Jane'
* ```
*
* @public
*/
isSignal: typeof isSignal_2;
// @public (undocumented)
export function isSignal(value: any): value is Signal<any>;
// @public
export const isUninitialized: (value: any) => value is typeof UNINITIALIZED;
// @public
export const
/**
* 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.get() // 'John'
*
* name.set('Jane')
*
* name.get() // 'Jane'
* ```
*
* @public
*/
react: typeof react_2;
export function react(name: string, fn: (lastReactedEpoch: number) => any, options?: EffectSchedulerOptions): () => void;
// @public
export interface Reactor<T = unknown> {
scheduler: EffectScheduler_2<T>;
scheduler: EffectScheduler<T>;
start(options?: {
force?: boolean;
}): void;
@ -206,26 +85,7 @@ export interface Reactor<T = unknown> {
}
// @public
export const
/**
* 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.get() // 'John'
*
* name.set('Jane')
*
* name.get() // 'Jane'
* ```
*
* @public
*/
reactor: typeof reactor_2;
export function reactor<Result>(name: string, fn: (lastReactedEpoch: number) => Result, options?: EffectSchedulerOptions): Reactor<Result>;
// @public (undocumented)
export const RESET_VALUE: unique symbol;
@ -250,70 +110,13 @@ export interface Signal<Value, Diff = unknown> {
export function track<T extends FunctionComponent<any>>(baseComponent: T): T extends React_2.MemoExoticComponent<any> ? T : React_2.MemoExoticComponent<T>;
// @public
export const
/**
* 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.get() // 'John'
*
* name.set('Jane')
*
* name.get() // 'Jane'
* ```
*
* @public
*/
transact: typeof transact_2;
export function transact<T>(fn: () => T): T;
// @public
export const
/**
* 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.get() // 'John'
*
* name.set('Jane')
*
* name.get() // 'Jane'
* ```
*
* @public
*/
transaction: typeof transaction_2;
export function transaction<T>(fn: (rollback: () => void) => T): T;
// @public
export const
/**
* 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.get() // 'John'
*
* name.set('Jane')
*
* name.get() // 'Jane'
* ```
*
* @public
*/
unsafe__withoutCapture: typeof unsafe__withoutCapture_2;
export function unsafe__withoutCapture<T>(fn: () => T): T;
// @public
export function useAtom<Value, Diff = unknown>(
@ -343,48 +146,10 @@ export function useValue<Value>(value: Signal<Value>): Value;
export function useValue<Value>(name: string, fn: () => Value, deps: unknown[]): Value;
// @public
export const
/**
* 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.get() // 'John'
*
* name.set('Jane')
*
* name.get() // 'Jane'
* ```
*
* @public
*/
whyAmIRunning: typeof whyAmIRunning_2;
export function whyAmIRunning(): void;
// @public
export const
/**
* 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.get() // 'John'
*
* name.set('Jane')
*
* name.get() // 'Jane'
* ```
*
* @public
*/
withDiff: typeof withDiff_2;
export function withDiff<Value, Diff>(value: Value, diff: Diff): WithDiff<Value, Diff>;
// (No @packageDocumentation comment for this package)

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,8 @@
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 { EMPTY_ARRAY, equals, singleton } from './helpers'
import { advanceGlobalEpoch, atomDidChange, getGlobalEpoch } from './transactions'
import { Child, ComputeDiff, RESET_VALUE, Signal } from './types'
import { logDotValueWarning } from './warnings'
@ -69,7 +69,7 @@ export interface Atom<Value, Diff = unknown> extends Signal<Value, Diff> {
/**
* @internal
*/
export class _Atom<Value, Diff = unknown> implements Atom<Value, Diff> {
class __Atom__<Value, Diff = unknown> implements Atom<Value, Diff> {
constructor(
public readonly name: string,
private current: Value,
@ -90,7 +90,7 @@ export class _Atom<Value, Diff = unknown> implements Atom<Value, Diff> {
computeDiff?: ComputeDiff<Value, Diff>
lastChangedEpoch = globalEpoch
lastChangedEpoch = getGlobalEpoch()
children = new ArraySet<Child>()
@ -127,21 +127,21 @@ export class _Atom<Value, Diff = unknown> implements Atom<Value, Diff> {
if (this.historyBuffer) {
this.historyBuffer.pushEntry(
this.lastChangedEpoch,
globalEpoch,
getGlobalEpoch(),
diff ??
this.computeDiff?.(this.current, value, this.lastChangedEpoch, globalEpoch) ??
this.computeDiff?.(this.current, value, this.lastChangedEpoch, getGlobalEpoch()) ??
RESET_VALUE
)
}
// Update the atom's record of the epoch when last changed.
this.lastChangedEpoch = globalEpoch
this.lastChangedEpoch = getGlobalEpoch()
const oldValue = this.current
this.current = value
// Notify all children that this atom has changed.
atomDidChange(this, oldValue)
atomDidChange(this as any, oldValue)
return value
}
@ -162,6 +162,27 @@ export class _Atom<Value, Diff = unknown> implements Atom<Value, Diff> {
}
}
export const _Atom = singleton('Atom', () => __Atom__)
export type _Atom = InstanceType<typeof _Atom>
/**
* 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.get() // 'John'
*
* name.set('Jane')
*
* name.get() // '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.
@ -179,6 +200,10 @@ export function atom<Value, Diff = unknown>(
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
}

View file

@ -3,8 +3,8 @@ import { ArraySet } from './ArraySet'
import { HistoryBuffer } from './HistoryBuffer'
import { maybeCaptureParent, startCapturingParents, stopCapturingParents } from './capture'
import { GLOBAL_START_EPOCH } from './constants'
import { EMPTY_ARRAY, equals, haveParentsChanged } from './helpers'
import { globalEpoch } from './transactions'
import { EMPTY_ARRAY, equals, haveParentsChanged, singleton } from './helpers'
import { getGlobalEpoch } from './transactions'
import { Child, ComputeDiff, RESET_VALUE, Signal } from './types'
import { logComputedGetterWarning, logDotValueWarning } from './warnings'
@ -43,10 +43,37 @@ export const isUninitialized = (value: any): value is UNINITIALIZED => {
return value === UNINITIALIZED
}
export const WithDiff = singleton(
'WithDiff',
() =>
class WithDiff<Value, Diff> {
constructor(public value: Value, public diff: Diff) {}
}
)
export type WithDiff<Value, Diff> = { value: Value; diff: Diff }
/**
* When writing incrementally-computed signals it is convenient (and usually more performant) to incrementally compute the diff too.
*
* You can use this function to wrap the return value of a computed signal function to indicate that the diff should be used instead of calculating a new one with [[AtomOptions.computeDiff]].
*
* @example
* ```ts
* const count = atom('count', 0)
* const double = computed('double', (prevValue) => {
* const nextValue = count.get() * 2
* if (isUninitialized(prevValue)) {
* return nextValue
* }
* return withDiff(nextValue, nextValue - prevValue)
* }, { historyLength: 10 })
* ```
*
*
* @param value - The value.
* @param diff - The diff.
* @public
*/
export function withDiff<Value, Diff>(value: Value, diff: Diff): WithDiff<Value, Diff> {
return new WithDiff(value, diff)
}
@ -102,7 +129,7 @@ export interface Computed<Value, Diff = unknown> extends Signal<Value, Diff> {
/**
* @internal
*/
export class _Computed<Value, Diff = unknown> implements Computed<Value, Diff> {
class __UNSAFE__Computed<Value, Diff = unknown> implements Computed<Value, Diff> {
lastChangedEpoch = GLOBAL_START_EPOCH
lastTraversedEpoch = GLOBAL_START_EPOCH
@ -154,8 +181,8 @@ export class _Computed<Value, Diff = unknown> implements Computed<Value, Diff> {
__unsafe__getWithoutCapture(): Value {
const isNew = this.lastChangedEpoch === GLOBAL_START_EPOCH
if (!isNew && (this.lastCheckedEpoch === globalEpoch || !haveParentsChanged(this))) {
this.lastCheckedEpoch = globalEpoch
if (!isNew && (this.lastCheckedEpoch === getGlobalEpoch() || !haveParentsChanged(this))) {
this.lastCheckedEpoch = getGlobalEpoch()
return this.state
}
@ -168,16 +195,16 @@ export class _Computed<Value, Diff = unknown> implements Computed<Value, Diff> {
const diff = result instanceof WithDiff ? result.diff : undefined
this.historyBuffer.pushEntry(
this.lastChangedEpoch,
globalEpoch,
getGlobalEpoch(),
diff ??
this.computeDiff?.(this.state, newState, this.lastCheckedEpoch, globalEpoch) ??
this.computeDiff?.(this.state, newState, this.lastCheckedEpoch, getGlobalEpoch()) ??
RESET_VALUE
)
}
this.lastChangedEpoch = globalEpoch
this.lastChangedEpoch = getGlobalEpoch()
this.state = newState
}
this.lastCheckedEpoch = globalEpoch
this.lastCheckedEpoch = getGlobalEpoch()
return this.state
} finally {
@ -213,6 +240,9 @@ export class _Computed<Value, Diff = unknown> implements Computed<Value, Diff> {
}
}
export const _Computed = singleton('Computed', () => __UNSAFE__Computed)
export type _Computed = InstanceType<typeof __UNSAFE__Computed>
function computedMethodAnnotation(
options: ComputedOptions<any, any> = {},
_target: any,
@ -223,7 +253,7 @@ function computedMethodAnnotation(
const derivationKey = Symbol.for('__@tldraw/state__computed__' + key)
descriptor.value = function (this: any) {
let d = this[derivationKey] as _Computed<any> | undefined
let d = this[derivationKey] as Computed<any> | undefined
if (!d) {
d = new _Computed(key, originalMethod!.bind(this) as any, options)
@ -265,7 +295,7 @@ function computedGetterAnnotation(
const derivationKey = Symbol.for('__@tldraw/state__computed__' + key)
descriptor.get = function (this: any) {
let d = this[derivationKey] as _Computed<any> | undefined
let d = this[derivationKey] as Computed<any> | undefined
if (!d) {
d = new _Computed(key, originalMethod!.bind(this) as any, options)
@ -315,7 +345,7 @@ export function getComputedInstance<Obj extends object, Prop extends keyof Obj>(
propertyName: Prop
): Computed<Obj[Prop]> {
const key = Symbol.for('__@tldraw/state__computed__' + propertyName.toString())
let inst = obj[key as keyof typeof obj] as _Computed<Obj[Prop]> | undefined
let inst = obj[key as keyof typeof obj] as Computed<Obj[Prop]> | undefined
if (!inst) {
// deref to make sure it exists first
const val = obj[propertyName]
@ -323,11 +353,56 @@ export function getComputedInstance<Obj extends object, Prop extends keyof Obj>(
val.call(obj)
}
inst = obj[key as keyof typeof obj] as _Computed<Obj[Prop]> | undefined
inst = obj[key as keyof typeof obj] as Computed<Obj[Prop]> | undefined
}
return inst as any
}
/**
* Creates a computed signal.
*
* @example
* ```ts
* const name = atom('name', 'John')
* const greeting = computed('greeting', () => `Hello ${name.get()}!`)
* console.log(greeting.get()) // 'Hello John!'
* ```
*
* `computed` may also be used as a decorator for creating computed getter methods.
*
* @example
* ```ts
* class Counter {
* max = 100
* count = atom<number>(0)
*
* @computed getRemaining() {
* return this.max - this.count.get()
* }
* }
* ```
*
* You may optionally pass in a [[ComputedOptions]] when used as a decorator:
*
* @example
* ```ts
* class Counter {
* max = 100
* count = atom<number>(0)
*
* @computed({isEqual: (a, b) => a === b})
* getRemaining() {
* return this.max - this.count.get()
* }
* }
* ```
*
* @param name - The name of the signal.
* @param compute - The function that computes the value of the signal.
* @param options - Options for the signal.
*
* @public
*/
export function computed<Value, Diff = unknown>(
name: string,
compute: (

View file

@ -1,7 +1,7 @@
import { startCapturingParents, stopCapturingParents } from './capture'
import { GLOBAL_START_EPOCH } from './constants'
import { attach, detach, haveParentsChanged } from './helpers'
import { globalEpoch } from './transactions'
import { attach, detach, haveParentsChanged, singleton } from './helpers'
import { getGlobalEpoch } from './transactions'
import { Signal } from './types'
interface EffectSchedulerOptions {
@ -37,7 +37,7 @@ interface EffectSchedulerOptions {
scheduleEffect?: (execute: () => void) => void
}
export class EffectScheduler<Result> {
class __EffectScheduler__<Result> {
private _isActivelyListening = false
/**
* Whether this scheduler is attached and actively listening to its parents.
@ -80,11 +80,11 @@ export class EffectScheduler<Result> {
// bail out if we have been cancelled by another effect
if (!this._isActivelyListening) return
// bail out if no atoms have changed since the last time we ran this effect
if (this.lastReactedEpoch === globalEpoch) return
if (this.lastReactedEpoch === getGlobalEpoch()) return
// bail out if we have parents and they have not changed since last time
if (this.parents.length && !haveParentsChanged(this)) {
this.lastReactedEpoch = globalEpoch
this.lastReactedEpoch = getGlobalEpoch()
return
}
// if we don't have parents it's probably the first time this is running.
@ -141,7 +141,7 @@ export class EffectScheduler<Result> {
try {
startCapturingParents(this)
const result = this.runEffect(this.lastReactedEpoch)
this.lastReactedEpoch = globalEpoch
this.lastReactedEpoch = getGlobalEpoch()
return result
} finally {
stopCapturingParents()
@ -149,6 +149,57 @@ export class EffectScheduler<Result> {
}
}
/**
* An EffectScheduler is responsible for executing side effects in response to changes in state.
*
* You probably don't need to use this directly unless you're integrating this library with a framework of some kind.
*
* Instead, use the [[react]] and [[reactor]] functions.
*
* @example
* ```ts
* const render = new EffectScheduler('render', drawToCanvas)
*
* render.attach()
* render.execute()
* ```
*
* @public
*/
export const EffectScheduler = singleton('EffectScheduler', () => __EffectScheduler__)
/** @public */
export type EffectScheduler<Result> = __EffectScheduler__<Result>
/**
* Starts a new effect scheduler, scheduling the effect immediately.
*
* Returns a function that can be called to stop the scheduler.
*
* @example
* ```ts
* const color = atom('color', 'red')
* const stop = react('set style', () => {
* divElem.style.color = color.get()
* })
* color.set('blue')
* // divElem.style.color === 'blue'
* stop()
* color.set('green')
* // divElem.style.color === 'blue'
* ```
*
*
* Also useful in React applications for running effects outside of the render cycle.
*
* @example
* ```ts
* useEffect(() => react('set style', () => {
* divRef.current.style.color = color.get()
* }), [])
* ```
*
* @public
*/
export function react(
name: string,
fn: (lastReactedEpoch: number) => any,
@ -194,6 +245,11 @@ export interface Reactor<T = unknown> {
stop(): void
}
/**
* Creates a [[Reactor]], which is a thin wrapper around an [[EffectScheduler]].
*
* @public
*/
export function reactor<Result>(
name: string,
fn: (lastReactedEpoch: number) => Result,

View file

@ -1,6 +1,6 @@
import { atom } from '../Atom'
import { reactor } from '../EffectScheduler'
import { globalEpoch, transact, transaction } from '../transactions'
import { getGlobalEpoch, transact, transaction } from '../transactions'
import { RESET_VALUE } from '../types'
describe('atoms', () => {
@ -17,20 +17,20 @@ describe('atoms', () => {
expect(a.get()).toBe(2)
})
it('will not advance the global epoch on creation', () => {
const startEpoch = globalEpoch
const startEpoch = getGlobalEpoch()
atom('', 3)
expect(globalEpoch).toBe(startEpoch)
expect(getGlobalEpoch()).toBe(startEpoch)
})
it('will advance the global epoch on .set', () => {
const startEpoch = globalEpoch
const startEpoch = getGlobalEpoch()
const a = atom('', 3)
a.set(4)
expect(globalEpoch).toBe(startEpoch + 1)
expect(getGlobalEpoch()).toBe(startEpoch + 1)
})
it('can store history', () => {
const a = atom('', 1, { historyLength: 3, computeDiff: (a, b) => b - a })
const startEpoch = globalEpoch
const startEpoch = getGlobalEpoch()
expect(a.getDiffSince(startEpoch)).toEqual([])
@ -55,24 +55,24 @@ describe('atoms', () => {
const a = atom('', 1, { historyLength: 3, computeDiff: (a, b) => b - a })
const b = atom('', 1, { historyLength: 3, computeDiff: (a, b) => b - a })
const startEpoch = globalEpoch
const startEpoch = getGlobalEpoch()
b.set(-5)
b.set(-10)
b.set(-20)
expect(b.getDiffSince(startEpoch)).toEqual([-6, -5, -10])
expect(b.getDiffSince(globalEpoch)).toEqual([])
expect(b.getDiffSince(getGlobalEpoch())).toEqual([])
expect(a.getDiffSince(startEpoch)).toEqual([])
a.set(5)
expect(a.getDiffSince(startEpoch)).toEqual([+4])
expect(b.getDiffSince(startEpoch)).toEqual([-6, -5, -10])
expect(b.getDiffSince(globalEpoch)).toEqual([])
expect(b.getDiffSince(getGlobalEpoch())).toEqual([])
})
it('still updates history during transactions', () => {
const a = atom('', 1, { historyLength: 3, computeDiff: (a, b) => b - a })
const startEpoch = globalEpoch
const startEpoch = getGlobalEpoch()
transact(() => {
expect(a.getDiffSince(startEpoch)).toEqual([])
@ -95,7 +95,7 @@ describe('atoms', () => {
it('will clear the history if the transaction aborts', () => {
const a = atom('', 1, { historyLength: 3, computeDiff: (a, b) => b - a })
const startEpoch = globalEpoch
const startEpoch = getGlobalEpoch()
transaction((rollback) => {
expect(a.getDiffSince(startEpoch)).toEqual([])
@ -110,18 +110,18 @@ describe('atoms', () => {
expect(a.getDiffSince(startEpoch)).toEqual(RESET_VALUE)
})
it('supports an update operation', () => {
const startEpoch = globalEpoch
const startEpoch = getGlobalEpoch()
const a = atom('', 1)
a.update((value) => value + 1)
expect(a.get()).toBe(2)
expect(globalEpoch).toBe(startEpoch + 1)
expect(getGlobalEpoch()).toBe(startEpoch + 1)
})
it('supports passing diffs in .set', () => {
const a = atom('', 1, { historyLength: 3 })
const startEpoch = globalEpoch
const startEpoch = getGlobalEpoch()
a.set(5, +4)
expect(a.getDiffSince(startEpoch)).toEqual([+4])
@ -132,7 +132,7 @@ describe('atoms', () => {
it('does not push history if nothing changed', () => {
const a = atom('', 1, { historyLength: 3 })
const startEpoch = globalEpoch
const startEpoch = getGlobalEpoch()
a.set(5, +4)
expect(a.getDiffSince(startEpoch)).toEqual([+4])
@ -141,7 +141,7 @@ describe('atoms', () => {
})
it('clears the history buffer if you fail to provide a diff', () => {
const a = atom('', 1, { historyLength: 3 })
const startEpoch = globalEpoch
const startEpoch = getGlobalEpoch()
a.set(5, +4)

View file

@ -7,7 +7,7 @@ import {
stopCapturingParents,
unsafe__withoutCapture,
} from '../capture'
import { advanceGlobalEpoch, globalEpoch } from '../transactions'
import { advanceGlobalEpoch, getGlobalEpoch } from '../transactions'
import { Child } from '../types'
const emptyChild = (props: Partial<Child> = {}) =>
@ -22,7 +22,7 @@ const emptyChild = (props: Partial<Child> = {}) =>
describe('capturing parents', () => {
it('can be started and stopped', () => {
const a = atom('', 1)
const startEpoch = globalEpoch
const startEpoch = getGlobalEpoch()
const child = emptyChild()
const originalParentEpochs = child.parentEpochs
@ -42,13 +42,13 @@ describe('capturing parents', () => {
it('can handle several parents', () => {
const atomA = atom('', 1)
const atomAEpoch = globalEpoch
const atomAEpoch = getGlobalEpoch()
advanceGlobalEpoch() // let's say time has passed
const atomB = atom('', 1)
const atomBEpoch = globalEpoch
const atomBEpoch = getGlobalEpoch()
advanceGlobalEpoch() // let's say time has passed
const atomC = atom('', 1)
const atomCEpoch = globalEpoch
const atomCEpoch = getGlobalEpoch()
expect(atomAEpoch < atomBEpoch).toBe(true)
expect(atomBEpoch < atomCEpoch).toBe(true)
@ -109,13 +109,13 @@ describe('capturing parents', () => {
it('will shrink the parent arrays if the number of captured parents shrinks', () => {
const atomA = atom('', 1)
const atomAEpoch = globalEpoch
const atomAEpoch = getGlobalEpoch()
advanceGlobalEpoch() // let's say time has passed
const atomB = atom('', 1)
const atomBEpoch = globalEpoch
const atomBEpoch = getGlobalEpoch()
advanceGlobalEpoch() // let's say time has passed
const atomC = atom('', 1)
const atomCEpoch = globalEpoch
const atomCEpoch = getGlobalEpoch()
const child = emptyChild()

View file

@ -2,7 +2,7 @@ import { atom } from '../Atom'
import { Computed, _Computed, computed, getComputedInstance, isUninitialized } from '../Computed'
import { reactor } from '../EffectScheduler'
import { assertNever } from '../helpers'
import { advanceGlobalEpoch, globalEpoch, transact, transaction } from '../transactions'
import { advanceGlobalEpoch, getGlobalEpoch, transact, transaction } from '../transactions'
import { RESET_VALUE, Signal } from '../types'
function getLastCheckedEpoch(derivation: Computed<any>): number {
@ -12,7 +12,7 @@ function getLastCheckedEpoch(derivation: Computed<any>): number {
describe('derivations', () => {
it('will cache a value forever if it has no parents', () => {
const derive = jest.fn(() => 1)
const startEpoch = globalEpoch
const startEpoch = getGlobalEpoch()
const derivation = computed('', derive)
expect(derive).toHaveBeenCalledTimes(0)
@ -47,7 +47,7 @@ describe('derivations', () => {
const a = atom('', 1)
const double = jest.fn(() => a.get() * 2)
const derivation = computed('', double)
const startEpoch = globalEpoch
const startEpoch = getGlobalEpoch()
expect(double).toHaveBeenCalledTimes(0)
expect(derivation.get()).toBe(2)
@ -61,7 +61,7 @@ describe('derivations', () => {
expect(derivation.lastChangedEpoch).toBe(startEpoch)
a.set(2)
const nextEpoch = globalEpoch
const nextEpoch = getGlobalEpoch()
expect(nextEpoch > startEpoch).toBe(true)
expect(double).toHaveBeenCalledTimes(1)
@ -87,7 +87,7 @@ describe('derivations', () => {
})
it('supports history', () => {
const startEpoch = globalEpoch
const startEpoch = getGlobalEpoch()
const a = atom('', 1)
const derivation = computed('', () => a.get() * 2, {
@ -119,7 +119,7 @@ describe('derivations', () => {
})
it('doesnt update history if it doesnt change', () => {
const startEpoch = globalEpoch
const startEpoch = getGlobalEpoch()
const a = atom('', 1)
const floor = jest.fn((n: number) => Math.floor(n))
@ -159,7 +159,7 @@ describe('derivations', () => {
})
it('updates the lastCheckedEpoch whenever the globalEpoch advances', () => {
const startEpoch = globalEpoch
const startEpoch = getGlobalEpoch()
const a = atom('', 1)
const double = jest.fn(() => a.get() * 2)
@ -203,7 +203,7 @@ describe('derivations', () => {
expect(derivation.get()).toBe(2)
const startEpoch = globalEpoch
const startEpoch = getGlobalEpoch()
a.set(2)
@ -300,7 +300,7 @@ describe('derivations', () => {
computeDiff: (a, b) => b - a,
})
const startEpoch = globalEpoch
const startEpoch = getGlobalEpoch()
transaction((rollback) => {
expect(c.getDiffSince(startEpoch)).toEqual([])
@ -322,7 +322,7 @@ describe('derivations', () => {
computeDiff: (a, b) => b - a,
})
expect(c.getDiffSince(globalEpoch - 1)).toEqual(RESET_VALUE)
expect(c.getDiffSince(getGlobalEpoch() - 1)).toEqual(RESET_VALUE)
})
})

View file

@ -1,5 +1,5 @@
import { attach, detach } from './helpers'
import { Child, Signal } from './types'
import { attach, detach, singleton } from './helpers'
import type { Child, Signal } from './types'
class CaptureStackFrame {
offset = 0
@ -10,25 +10,48 @@ class CaptureStackFrame {
constructor(public readonly below: CaptureStackFrame | null, public readonly child: Child) {}
}
let stack: CaptureStackFrame | null = null
const inst = singleton('capture', () => ({ stack: null as null | CaptureStackFrame }))
/**
* Executes the given function without capturing any parents in the current capture context.
*
* This is mainly useful if you want to run an effect only when certain signals change while also
* dereferencing other signals which should not cause the effect to rerun on their own.
*
* @example
* ```ts
* const name = atom('name', 'Sam')
* const time = atom('time', () => new Date().getTime())
*
* setInterval(() => {
* time.set(new Date().getTime())
* })
*
* react('log name changes', () => {
* print(name.get(), 'was changed at', unsafe__withoutCapture(() => time.get()))
* })
*
* ```
*
* @public
*/
export function unsafe__withoutCapture<T>(fn: () => T): T {
const oldStack = stack
stack = null
const oldStack = inst.stack
inst.stack = null
try {
return fn()
} finally {
stack = oldStack
inst.stack = oldStack
}
}
export function startCapturingParents(child: Child) {
stack = new CaptureStackFrame(stack, child)
inst.stack = new CaptureStackFrame(inst.stack, child)
}
export function stopCapturingParents() {
const frame = stack!
stack = frame.below
const frame = inst.stack!
inst.stack = frame.below
const didParentsChange = frame.numNewParents > 0 || frame.offset !== frame.child.parents.length
@ -47,9 +70,9 @@ export function stopCapturingParents() {
frame.child.parents.length = frame.offset
frame.child.parentEpochs.length = frame.offset
if (stack?.maybeRemoved) {
for (let i = 0; i < stack.maybeRemoved.length; i++) {
const maybeRemovedParent = stack.maybeRemoved[i]
if (inst.stack?.maybeRemoved) {
for (let i = 0; i < inst.stack.maybeRemoved.length; i++) {
const maybeRemovedParent = inst.stack.maybeRemoved[i]
if (frame.child.parents.indexOf(maybeRemovedParent) === -1) {
detach(maybeRemovedParent, frame.child)
}
@ -59,40 +82,60 @@ export function stopCapturingParents() {
// this must be called after the parent is up to date
export function maybeCaptureParent(p: Signal<any, any>) {
if (stack) {
const idx = stack.child.parents.indexOf(p)
if (inst.stack) {
const idx = inst.stack.child.parents.indexOf(p)
// if the child didn't deref this parent last time it executed, then idx will be -1
// if the child did deref this parent last time but in a different order relative to other parents, then idx will be greater than stack.offset
// if the child did deref this parent last time in the same order, then idx will be the same as stack.offset
// if the child did deref this parent already during this capture session then 0 <= idx < stack.offset
if (idx < 0) {
stack.numNewParents++
if (stack.child.isActivelyListening) {
attach(p, stack.child)
inst.stack.numNewParents++
if (inst.stack.child.isActivelyListening) {
attach(p, inst.stack.child)
}
}
if (idx < 0 || idx >= stack.offset) {
if (idx !== stack.offset && idx > 0) {
const maybeRemovedParent = stack.child.parents[stack.offset]
if (idx < 0 || idx >= inst.stack.offset) {
if (idx !== inst.stack.offset && idx > 0) {
const maybeRemovedParent = inst.stack.child.parents[inst.stack.offset]
if (!stack.maybeRemoved) {
stack.maybeRemoved = [maybeRemovedParent]
} else if (stack.maybeRemoved.indexOf(maybeRemovedParent) === -1) {
stack.maybeRemoved.push(maybeRemovedParent)
if (!inst.stack.maybeRemoved) {
inst.stack.maybeRemoved = [maybeRemovedParent]
} else if (inst.stack.maybeRemoved.indexOf(maybeRemovedParent) === -1) {
inst.stack.maybeRemoved.push(maybeRemovedParent)
}
}
stack.child.parents[stack.offset] = p
stack.child.parentEpochs[stack.offset] = p.lastChangedEpoch
stack.offset++
inst.stack.child.parents[inst.stack.offset] = p
inst.stack.child.parentEpochs[inst.stack.offset] = p.lastChangedEpoch
inst.stack.offset++
}
}
}
/**
* A debugging tool that tells you why a computed signal or effect is running.
* Call in the body of a computed signal or effect function.
*
* @example
* ```ts
* const name = atom('name', 'Bob')
* react('greeting', () => {
* whyAmIRunning()
* print('Hello', name.get())
* })
*
* name.set('Alice')
*
* // 'greeting' is running because:
* // 'name' changed => 'Alice'
* ```
*
* @public
*/
export function whyAmIRunning() {
const child = stack?.child
const child = inst.stack?.child
if (!child) {
throw new Error('whyAmIRunning() called outside of a reactive context')
}

View file

@ -84,5 +84,14 @@ export function equals(a: any, b: any): boolean {
export declare function assertNever(x: never): never
/** @public */
export const EMPTY_ARRAY: [] = Object.freeze([]) as any
export function singleton<T>(key: string, init: () => T): T {
const symbol = Symbol.for(`com.tldraw.state/${key}`)
const global = globalThis as any
global[symbol] ??= init()
return global[symbol]
}
/**
* @public
*/
export const EMPTY_ARRAY: [] = singleton('empty_array', () => Object.freeze([]) as any)

View file

@ -1,347 +1,26 @@
import { atom as _atom, isAtom as _isAtom } from './Atom'
import { computed as _computed, withDiff as _withDiff } from './Computed'
import {
EffectScheduler as _EffectScheduler,
react as _react,
reactor as _reactor,
} from './EffectScheduler'
import {
unsafe__withoutCapture as _unsafe__withoutCapture,
whyAmIRunning as _whyAmIRunning,
} from './capture'
import { EMPTY_ARRAY as _EMPTY_ARRAY } from './helpers'
import { isSignal as _isSignal } from './isSignal'
import { transact as _transact, transaction as _transaction } from './transactions'
import { singleton } from './helpers'
const sym = Symbol.for('com.tldraw.state')
const glob = globalThis as any
export { atom, isAtom } from './Atom'
export type { Atom, AtomOptions } from './Atom'
export { computed, getComputedInstance, isUninitialized, withDiff } from './Computed'
export type { Computed, ComputedOptions } from './Computed'
export { EffectScheduler, react, reactor } from './EffectScheduler'
export type { Reactor } from './EffectScheduler'
export { unsafe__withoutCapture, whyAmIRunning } from './capture'
export { EMPTY_ARRAY } from './helpers'
export { isSignal } from './isSignal'
export { transact, transaction } from './transactions'
export { RESET_VALUE } from './types'
export type { Signal } from './types'
// This should be incremented any time an API change is made. i.e. for additions or removals.
// Bugfixes need not increment this.
const currentApiVersion = 1
function init() {
return {
apiVersion: currentApiVersion,
atom: _atom,
isAtom: _isAtom,
computed: _computed,
withDiff: _withDiff,
EffectScheduler: _EffectScheduler,
react: _react,
reactor: _reactor,
unsafe__withoutCapture: _unsafe__withoutCapture,
whyAmIRunning: _whyAmIRunning,
EMPTY_ARRAY: _EMPTY_ARRAY,
isSignal: _isSignal,
transact: _transact,
transaction: _transaction,
}
}
const actualApiVersion = singleton('apiVersion', () => currentApiVersion)
const obj: ReturnType<typeof init> = glob[sym] || init()
glob[sym] = obj
const {
apiVersion,
/**
* 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.get() // 'John'
*
* name.set('Jane')
*
* name.get() // 'Jane'
* ```
*
* @public
*/
atom,
/**
* Returns true if the given value is an [[Atom]].
* @public
*/
isAtom,
/**
* Creates a computed signal.
*
* @example
* ```ts
* const name = atom('name', 'John')
* const greeting = computed('greeting', () => `Hello ${name.get()}!`)
* console.log(greeting.get()) // 'Hello John!'
* ```
*
* `computed` may also be used as a decorator for creating computed getter methods.
*
* @example
* ```ts
* class Counter {
* max = 100
* count = atom<number>(0)
*
* @computed getRemaining() {
* return this.max - this.count.get()
* }
* }
* ```
*
* You may optionally pass in a [[ComputedOptions]] when used as a decorator:
*
* @example
* ```ts
* class Counter {
* max = 100
* count = atom<number>(0)
*
* @computed({isEqual: (a, b) => a === b})
* getRemaining() {
* return this.max - this.count.get()
* }
* }
* ```
*
* @param name - The name of the signal.
* @param compute - The function that computes the value of the signal.
* @param options - Options for the signal.
*
* @public
*/
computed,
/**
* When writing incrementally-computed signals it is convenient (and usually more performant) to incrementally compute the diff too.
*
* You can use this function to wrap the return value of a computed signal function to indicate that the diff should be used instead of calculating a new one with [[AtomOptions.computeDiff]].
*
* @example
* ```ts
* const count = atom('count', 0)
* const double = computed('double', (prevValue) => {
* const nextValue = count.get() * 2
* if (isUninitialized(prevValue)) {
* return nextValue
* }
* return withDiff(nextValue, nextValue - prevValue)
* }, { historyLength: 10 })
* ```
*
*
* @param value - The value.
* @param diff - The diff.
* @public
*/
withDiff,
/**
* An EffectScheduler is responsible for executing side effects in response to changes in state.
*
* You probably don't need to use this directly unless you're integrating this library with a framework of some kind.
*
* Instead, use the [[react]] and [[reactor]] functions.
*
* @example
* ```ts
* const render = new EffectScheduler('render', drawToCanvas)
*
* render.attach()
* render.execute()
* ```
*
* @public
*/
EffectScheduler,
/**
* Starts a new effect scheduler, scheduling the effect immediately.
*
* Returns a function that can be called to stop the scheduler.
*
* @example
* ```ts
* const color = atom('color', 'red')
* const stop = react('set style', () => {
* divElem.style.color = color.get()
* })
* color.set('blue')
* // divElem.style.color === 'blue'
* stop()
* color.set('green')
* // divElem.style.color === 'blue'
* ```
*
*
* Also useful in React applications for running effects outside of the render cycle.
*
* @example
* ```ts
* useEffect(() => react('set style', () => {
* divRef.current.style.color = color.get()
* }), [])
* ```
*
* @public
*/
react,
/**
* Creates a [[Reactor]], which is a thin wrapper around an [[EffectScheduler]].
*
* @public
*/
reactor,
/**
* Executes the given function without capturing any parents in the current capture context.
*
* This is mainly useful if you want to run an effect only when certain signals change while also
* dereferencing other signals which should not cause the effect to rerun on their own.
*
* @example
* ```ts
* const name = atom('name', 'Sam')
* const time = atom('time', () => new Date().getTime())
*
* setInterval(() => {
* time.set(new Date().getTime())
* })
*
* react('log name changes', () => {
* print(name.get(), 'was changed at', unsafe__withoutCapture(() => time.get()))
* })
*
* ```
*
* @public
*/
unsafe__withoutCapture,
/**
* A debugging tool that tells you why a computed signal or effect is running.
* Call in the body of a computed signal or effect function.
*
* @example
* ```ts
* const name = atom('name', 'Bob')
* react('greeting', () => {
* whyAmIRunning()
* print('Hello', name.get())
* })
*
* name.set('Alice')
*
* // 'greeting' is running because:
* // 'name' changed => 'Alice'
* ```
*
* @public
*/
whyAmIRunning,
/** @public */
EMPTY_ARRAY,
/**
* Returns true if the given value is a signal (either an Atom or a Computed).
* @public
*/
isSignal,
/**
* Like [transaction](#transaction), but does not create a new transaction if there is already one in progress.
*
* @param fn - The function to run in a transaction.
* @public
*/
transact,
/**
* Batches state updates, deferring side effects until after the transaction completes.
*
* @example
* ```ts
* const firstName = atom('John')
* const lastName = atom('Doe')
*
* react('greet', () => {
* print(`Hello, ${firstName.get()} ${lastName.get()}!`)
* })
*
* // Logs "Hello, John Doe!"
*
* transaction(() => {
* firstName.set('Jane')
* lastName.set('Smith')
* })
*
* // Logs "Hello, Jane Smith!"
* ```
*
* If the function throws, the transaction is aborted and any signals that were updated during the transaction revert to their state before the transaction began.
*
* @example
* ```ts
* const firstName = atom('John')
* const lastName = atom('Doe')
*
* react('greet', () => {
* print(`Hello, ${firstName.get()} ${lastName.get()}!`)
* })
*
* // Logs "Hello, John Doe!"
*
* transaction(() => {
* firstName.set('Jane')
* throw new Error('oops')
* })
*
* // Does not log
* // firstName.get() === 'John'
* ```
*
* A `rollback` callback is passed into the function.
* Calling this will prevent the transaction from committing and will revert any signals that were updated during the transaction to their state before the transaction began.
*
* * @example
* ```ts
* const firstName = atom('John')
* const lastName = atom('Doe')
*
* react('greet', () => {
* print(`Hello, ${firstName.get()} ${lastName.get()}!`)
* })
*
* // Logs "Hello, John Doe!"
*
* transaction((rollback) => {
* firstName.set('Jane')
* lastName.set('Smith')
* rollback()
* })
*
* // Does not log
* // firstName.get() === 'John'
* // lastName.get() === 'Doe'
* ```
*
* @param fn - The function to run in a transaction, called with a function to roll back the change.
* @public
*/
transaction,
} = obj
if (apiVersion !== currentApiVersion) {
if (actualApiVersion !== currentApiVersion) {
throw new Error(
'@tldraw/state: Multiple versions of @tldraw/state are being used. Please ensure that there is only one version of @tldraw/state in your dependency tree.'
`You have multiple incompatible versions of @tldraw/state in your app. Please deduplicate the package.`
)
}
export type { Atom, AtomOptions } from './Atom'
export { getComputedInstance, isUninitialized } from './Computed'
export type { Computed, ComputedOptions } from './Computed'
export type { Reactor } from './EffectScheduler'
export { RESET_VALUE } from './types'
export type { Signal } from './types'
export { atom, isAtom }
export { computed, withDiff }
export { EffectScheduler, react, reactor }
export { unsafe__withoutCapture, whyAmIRunning }
export { EMPTY_ARRAY }
export { isSignal }
export { transact, transaction }

View file

@ -2,6 +2,9 @@ import { _Atom } from './Atom'
import { _Computed } from './Computed'
import { Signal } from './types'
/**
* @public
*/
export function isSignal(value: any): value is Signal<any> {
return value instanceof _Atom || value instanceof _Computed
}

View file

@ -1,21 +1,12 @@
import { _Atom } from './Atom'
import { GLOBAL_START_EPOCH } from './constants'
import { EffectScheduler } from './EffectScheduler'
import { singleton } from './helpers'
import { Child, Signal } from './types'
// The current epoch (global to all atoms).
export let globalEpoch = GLOBAL_START_EPOCH + 1
// Whether any transaction is reacting.
let globalIsReacting = false
export function advanceGlobalEpoch() {
globalEpoch++
}
class Transaction {
constructor(public readonly parent: Transaction | null) {}
initialAtomValues = new Map<_Atom<any>, any>()
initialAtomValues = new Map<_Atom, any>()
/**
* Get whether this transaction is a root (no parents).
@ -54,7 +45,7 @@ class Transaction {
* @public
*/
abort() {
globalEpoch++
inst.globalEpoch++
// Reset each of the transaction's atoms to its initial value.
this.initialAtomValues.forEach((value, atom) => {
@ -67,31 +58,43 @@ class Transaction {
}
}
const inst = singleton('transactions', () => ({
// The current epoch (global to all atoms).
globalEpoch: GLOBAL_START_EPOCH + 1,
// Whether any transaction is reacting.
globalIsReacting: false,
currentTransaction: null as Transaction | null,
}))
export function getGlobalEpoch() {
return inst.globalEpoch
}
/**
* Collect all of the reactors that need to run for an atom and run them.
*
* @param atom The atom to flush changes for.
*/
function flushChanges(atoms: Iterable<_Atom<any>>) {
if (globalIsReacting) {
function flushChanges(atoms: Iterable<_Atom>) {
if (inst.globalIsReacting) {
throw new Error('cannot change atoms during reaction cycle')
}
try {
globalIsReacting = true
inst.globalIsReacting = true
// Collect all of the visited reactors.
const reactors = new Set<EffectScheduler<unknown>>()
// Visit each descendant of the atom, collecting reactors.
const traverse = (node: Child) => {
if (node.lastTraversedEpoch === globalEpoch) {
if (node.lastTraversedEpoch === inst.globalEpoch) {
return
}
node.lastTraversedEpoch = globalEpoch
node.lastTraversedEpoch = inst.globalEpoch
if ('maybeScheduleEffect' in node) {
if (node instanceof EffectScheduler) {
reactors.add(node)
} else {
;(node as any as Signal<any>).children.visit(traverse)
@ -107,7 +110,7 @@ function flushChanges(atoms: Iterable<_Atom<any>>) {
r.maybeScheduleEffect()
}
} finally {
globalIsReacting = false
inst.globalIsReacting = false
}
}
@ -119,27 +122,95 @@ function flushChanges(atoms: Iterable<_Atom<any>>) {
*
* @internal
*/
export function atomDidChange(atom: _Atom<any>, previousValue: any) {
if (!currentTransaction) {
export function atomDidChange(atom: _Atom, previousValue: any) {
if (!inst.currentTransaction) {
flushChanges([atom])
} else if (!currentTransaction.initialAtomValues.has(atom)) {
currentTransaction.initialAtomValues.set(atom, previousValue)
} else if (!inst.currentTransaction.initialAtomValues.has(atom)) {
inst.currentTransaction.initialAtomValues.set(atom, previousValue)
}
}
export function advanceGlobalEpoch() {
inst.globalEpoch++
}
/**
* The current transaction, if there is one.
* Batches state updates, deferring side effects until after the transaction completes.
*
* @global
* @example
* ```ts
* const firstName = atom('John')
* const lastName = atom('Doe')
*
* react('greet', () => {
* print(`Hello, ${firstName.get()} ${lastName.get()}!`)
* })
*
* // Logs "Hello, John Doe!"
*
* transaction(() => {
* firstName.set('Jane')
* lastName.set('Smith')
* })
*
* // Logs "Hello, Jane Smith!"
* ```
*
* If the function throws, the transaction is aborted and any signals that were updated during the transaction revert to their state before the transaction began.
*
* @example
* ```ts
* const firstName = atom('John')
* const lastName = atom('Doe')
*
* react('greet', () => {
* print(`Hello, ${firstName.get()} ${lastName.get()}!`)
* })
*
* // Logs "Hello, John Doe!"
*
* transaction(() => {
* firstName.set('Jane')
* throw new Error('oops')
* })
*
* // Does not log
* // firstName.get() === 'John'
* ```
*
* A `rollback` callback is passed into the function.
* Calling this will prevent the transaction from committing and will revert any signals that were updated during the transaction to their state before the transaction began.
*
* * @example
* ```ts
* const firstName = atom('John')
* const lastName = atom('Doe')
*
* react('greet', () => {
* print(`Hello, ${firstName.get()} ${lastName.get()}!`)
* })
*
* // Logs "Hello, John Doe!"
*
* transaction((rollback) => {
* firstName.set('Jane')
* lastName.set('Smith')
* rollback()
* })
*
* // Does not log
* // firstName.get() === 'John'
* // lastName.get() === 'Doe'
* ```
*
* @param fn - The function to run in a transaction, called with a function to roll back the change.
* @public
*/
export let currentTransaction = null as Transaction | null
export function transaction<T>(fn: (rollback: () => void) => T) {
const txn = new Transaction(currentTransaction)
const txn = new Transaction(inst.currentTransaction)
// Set the current transaction to the transaction
currentTransaction = txn
inst.currentTransaction = txn
try {
let rollback = false
@ -162,12 +233,18 @@ export function transaction<T>(fn: (rollback: () => void) => T) {
throw e
} finally {
// Set the current transaction to the transaction's parent.
currentTransaction = currentTransaction.parent
inst.currentTransaction = inst.currentTransaction.parent
}
}
/**
* Like [transaction](#transaction), but does not create a new transaction if there is already one in progress.
*
* @param fn - The function to run in a transaction.
* @public
*/
export function transact<T>(fn: () => T): T {
if (currentTransaction) {
if (inst.currentTransaction) {
return fn()
}
return transaction(fn)

View file

@ -1,6 +1,4 @@
import { ArraySet } from './ArraySet'
import { _Computed } from './Computed'
import { EffectScheduler } from './EffectScheduler'
/** @public */
export const RESET_VALUE: unique symbol = Symbol.for('com.tldraw.state/RESET_VALUE')
@ -54,7 +52,12 @@ export interface Signal<Value, Diff = unknown> {
}
/** @internal */
export type Child = EffectScheduler<any> | _Computed<any>
export type Child = {
lastTraversedEpoch: number
parents: Signal<any, any>[]
parentEpochs: number[]
isActivelyListening: boolean
}
/**
* Computes the diff between the previous and current value.