Use a global singleton for tlstate (#2322)

One minor issue with signia is that it uses global state for
bookkeeping, so it is potentially disastrous if there is more than one
version of it included in a bundle.

To prevent that being an issue before we had a warning that would
trigger if signia detects multiple initializations.

> Multiple versions of @tldraw/state detected. This will cause
unexpected behavior. Please add "resolutions" (yarn/pnpm) or "overrides"
(npm) in your package.json to ensure only one version of @tldraw/state
is loaded.

Alas I think this warning triggers too often in development
environments, e.g. during HMR or janky bundlers.


Something that can prevent the need for this particular warning is
having a global singleton version of signia that we only instantiate
once, and then re-use that one on subsequent module initializations. We
didn't do this before because it has a few downsides:

- breaks HMR if you are working on signia itself, since updated modules
won't be used and you'll need to do a full refresh.
- introduces the possibility of breakage if we remove or even add APIs
to signia. We can't rely on having the latest version of signia be the
first to instantiate, and we can't allow later instantiations to take
precedence since atoms n stuff may have already been created with the
prior version. To mitigate this I've introduced a `apiVersion` const
that we can increment when we make any kind of additions or removals. If
there is a mismatch between the `apiVersion` in the global singleton vs
the currently-initializing module, then it throws.

Ultimately i think the pros outweigh the cons here, i.e. far fewer
people will see and have to deal with the error message shown above, and
fewer people should encounter a situation where the editor appears to
load but nothing changes when you interact with it.


### Change Type

- [x] `patch` — Bug fix

[^1]: publishes a `patch` release, for devDependencies use `internal`
[^2]: will not publish a new version

### Release Notes

- Make a global singleton for tlstate.
This commit is contained in:
David Sheldrick 2023-12-14 13:35:34 +00:00 committed by GitHub
parent e8761c8e51
commit b133c59391
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 821 additions and 1500 deletions

View file

@ -14,10 +14,26 @@ export interface Atom<Value, Diff = unknown> extends Signal<Value, Diff> {
} }
// @public // @public
export function atom<Value, Diff = unknown>( export const
name: string, /**
initialValue: Value, * Creates a new [[Atom]].
options?: AtomOptions<Value, Diff>): Atom<Value, Diff>; *
* 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;
// @public // @public
export interface AtomOptions<Value, Diff> { export interface AtomOptions<Value, Diff> {
@ -36,13 +52,26 @@ export interface Computed<Value, Diff = unknown> extends Signal<Value, Diff> {
} }
// @public // @public
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>; export const
/**
// @public (undocumented) * Creates a new [[Atom]].
export function computed(target: any, key: string, descriptor: PropertyDescriptor): PropertyDescriptor; *
* An Atom is a signal that can be updated directly by calling [[Atom.set]] or [[Atom.update]].
// @public (undocumented) *
export function computed<Value, Diff = unknown>(options?: ComputedOptions<Value, Diff>): (target: any, key: string, descriptor: PropertyDescriptor) => PropertyDescriptor; * @example
* ```ts
* const name = atom('name', 'John')
*
* name.get() // 'John'
*
* name.set('Jane')
*
* name.get() // 'Jane'
* ```
*
* @public
*/
computed: typeof computed_2;
// @public // @public
export interface ComputedOptions<Value, Diff> { export interface ComputedOptions<Value, Diff> {
@ -52,48 +81,124 @@ export interface ComputedOptions<Value, Diff> {
} }
// @public // @public
export class EffectScheduler<Result> { export const
constructor(name: string, runEffect: (lastReactedEpoch: number) => Result, options?: EffectSchedulerOptions); /**
attach(): void; * Creates a new [[Atom]].
detach(): void; *
execute(): Result; * An Atom is a signal that can be updated directly by calling [[Atom.set]] or [[Atom.update]].
get isActivelyListening(): boolean; *
// @internal (undocumented) * @example
lastTraversedEpoch: number; * ```ts
// @internal (undocumented) * const name = atom('name', 'John')
maybeScheduleEffect(): void; *
// (undocumented) * name.get() // 'John'
readonly name: string; *
// @internal (undocumented) * name.set('Jane')
parentEpochs: number[]; *
// @internal (undocumented) * name.get() // 'Jane'
parents: Signal<any, any>[]; * ```
get scheduleCount(): number; *
// @internal (undocumented) * @public
scheduleEffect(): void; */
} EffectScheduler: typeof EffectScheduler_2;
// @public (undocumented) // @public (undocumented)
export const EMPTY_ARRAY: []; 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: [];
// @public // @public
export function getComputedInstance<Obj extends object, Prop extends keyof Obj>(obj: Obj, propertyName: Prop): Computed<Obj[Prop]>; export function getComputedInstance<Obj extends object, Prop extends keyof Obj>(obj: Obj, propertyName: Prop): Computed<Obj[Prop]>;
// @public // @public
export function isAtom(value: unknown): value is Atom<unknown>; 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;
// @public // @public
export function isSignal(value: any): value is Signal<any>; 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 // @public
export const isUninitialized: (value: any) => value is typeof UNINITIALIZED; export const isUninitialized: (value: any) => value is typeof UNINITIALIZED;
// @public // @public
export function react(name: string, fn: (lastReactedEpoch: number) => any, options?: EffectSchedulerOptions): () => void; 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;
// @public // @public
export interface Reactor<T = unknown> { export interface Reactor<T = unknown> {
scheduler: EffectScheduler<T>; scheduler: EffectScheduler_2<T>;
start(options?: { start(options?: {
force?: boolean; force?: boolean;
}): void; }): void;
@ -101,7 +206,26 @@ export interface Reactor<T = unknown> {
} }
// @public // @public
export function reactor<Result>(name: string, fn: (lastReactedEpoch: number) => Result, options?: EffectSchedulerOptions): Reactor<Result>; 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;
// @public (undocumented) // @public (undocumented)
export const RESET_VALUE: unique symbol; export const RESET_VALUE: unique symbol;
@ -126,13 +250,70 @@ 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>; export function track<T extends FunctionComponent<any>>(baseComponent: T): T extends React_2.MemoExoticComponent<any> ? T : React_2.MemoExoticComponent<T>;
// @public // @public
export function transact<T>(fn: () => T): T; 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;
// @public // @public
export function transaction<T>(fn: (rollback: () => void) => T): T; 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;
// @public // @public
export function unsafe__withoutCapture<T>(fn: () => T): T; 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;
// @public // @public
export function useAtom<Value, Diff = unknown>( export function useAtom<Value, Diff = unknown>(
@ -162,10 +343,48 @@ export function useValue<Value>(value: Signal<Value>): Value;
export function useValue<Value>(name: string, fn: () => Value, deps: unknown[]): Value; export function useValue<Value>(name: string, fn: () => Value, deps: unknown[]): Value;
// @public // @public
export function whyAmIRunning(): void; 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;
// @public // @public
export function withDiff<Value, Diff>(value: Value, diff: Diff): WithDiff<Value, Diff>; 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;
// (No @packageDocumentation comment for this package) // (No @packageDocumentation comment for this package)

File diff suppressed because it is too large Load diff

View file

@ -162,24 +162,6 @@ export class _Atom<Value, Diff = unknown> implements Atom<Value, Diff> {
} }
} }
/**
* 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>( 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. * A name for the signal. This is used for debugging and profiling purposes, it does not need to be unique.
@ -197,10 +179,6 @@ export function atom<Value, Diff = unknown>(
return new _Atom(name, initialValue, options) 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> { export function isAtom(value: unknown): value is Atom<unknown> {
return value instanceof _Atom return value instanceof _Atom
} }

View file

@ -8,14 +8,17 @@ import { globalEpoch } from './transactions'
import { Child, ComputeDiff, RESET_VALUE, Signal } from './types' import { Child, ComputeDiff, RESET_VALUE, Signal } from './types'
import { logComputedGetterWarning, logDotValueWarning } from './warnings' import { logComputedGetterWarning, logDotValueWarning } from './warnings'
const UNINITIALIZED = Symbol('UNINITIALIZED') /**
* @public
*/
export const UNINITIALIZED = Symbol.for('com.tldraw.state/UNINITIALIZED')
/** /**
* The type of the first value passed to a computed signal function as the 'prevValue' parameter. * The type of the first value passed to a computed signal function as the 'prevValue' parameter.
* *
* @see [[isUninitialized]]. * @see [[isUninitialized]].
* @public * @public
*/ */
type UNINITIALIZED = typeof UNINITIALIZED export type UNINITIALIZED = typeof UNINITIALIZED
/** /**
* Call this inside a computed signal function to determine whether it is the first time the function is being called. * Call this inside a computed signal function to determine whether it is the first time the function is being called.
@ -44,28 +47,6 @@ class WithDiff<Value, Diff> {
constructor(public value: Value, public diff: Diff) {} constructor(public value: Value, public 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> { export function withDiff<Value, Diff>(value: Value, diff: Diff): WithDiff<Value, Diff> {
return new WithDiff(value, diff) return new WithDiff(value, diff)
} }
@ -347,51 +328,6 @@ export function getComputedInstance<Obj extends object, Prop extends keyof Obj>(
return inst as any 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>( export function computed<Value, Diff = unknown>(
name: string, name: string,
compute: ( compute: (

View file

@ -37,23 +37,6 @@ interface EffectSchedulerOptions {
scheduleEffect?: (execute: () => void) => void scheduleEffect?: (execute: () => void) => void
} }
/**
* 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 class EffectScheduler<Result> { export class EffectScheduler<Result> {
private _isActivelyListening = false private _isActivelyListening = false
/** /**
@ -166,36 +149,6 @@ export class 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( export function react(
name: string, name: string,
fn: (lastReactedEpoch: number) => any, fn: (lastReactedEpoch: number) => any,
@ -241,11 +194,6 @@ export interface Reactor<T = unknown> {
stop(): void stop(): void
} }
/**
* Creates a [[Reactor]], which is a thin wrapper around an [[EffectScheduler]].
*
* @public
*/
export function reactor<Result>( export function reactor<Result>(
name: string, name: string,
fn: (lastReactedEpoch: number) => Result, fn: (lastReactedEpoch: number) => Result,

View file

@ -1,17 +1,6 @@
import { attach, detach } from './helpers' import { attach, detach } from './helpers'
import { Child, Signal } from './types' import { Child, Signal } from './types'
const tldrawStateGlobalKey = Symbol.for('__@tldraw/state__')
const tldrawStateGlobal = globalThis as { [tldrawStateGlobalKey]?: true }
if (tldrawStateGlobal[tldrawStateGlobalKey]) {
console.error(
'Multiple versions of @tldraw/state detected. This will cause unexpected behavior. Please add "resolutions" (yarn/pnpm) or "overrides" (npm) in your package.json to ensure only one version of @tldraw/state is loaded.'
)
} else {
tldrawStateGlobal[tldrawStateGlobalKey] = true
}
class CaptureStackFrame { class CaptureStackFrame {
offset = 0 offset = 0
numNewParents = 0 numNewParents = 0
@ -23,29 +12,6 @@ class CaptureStackFrame {
let stack: CaptureStackFrame | null = null let stack: CaptureStackFrame | null = null
/**
* 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 { export function unsafe__withoutCapture<T>(fn: () => T): T {
const oldStack = stack const oldStack = stack
stack = null stack = null
@ -125,26 +91,6 @@ export function maybeCaptureParent(p: Signal<any, any>) {
} }
} }
/**
* 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() { export function whyAmIRunning() {
const child = stack?.child const child = stack?.child
if (!child) { if (!child) {

View file

@ -1,12 +1,347 @@
export { atom, isAtom } from './Atom' 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'
const sym = Symbol.for('com.tldraw.state')
const glob = globalThis as any
// 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 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) {
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.'
)
}
export type { Atom, AtomOptions } from './Atom' export type { Atom, AtomOptions } from './Atom'
export { computed, getComputedInstance, isUninitialized, withDiff } from './Computed' export { getComputedInstance, isUninitialized } from './Computed'
export type { Computed, ComputedOptions } from './Computed' export type { Computed, ComputedOptions } from './Computed'
export { EffectScheduler, react, reactor } from './EffectScheduler'
export type { 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 { RESET_VALUE } from './types'
export type { Signal } 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,10 +2,6 @@ import { _Atom } from './Atom'
import { _Computed } from './Computed' import { _Computed } from './Computed'
import { Signal } from './types' import { Signal } from './types'
/**
* Returns true if the given value is a signal (either an Atom or a Computed).
* @public
*/
export function isSignal(value: any): value is Signal<any> { export function isSignal(value: any): value is Signal<any> {
return value instanceof _Atom || value instanceof _Computed return value instanceof _Atom || value instanceof _Computed
} }

View file

@ -135,78 +135,6 @@ export function atomDidChange(atom: _Atom<any>, previousValue: any) {
*/ */
export let currentTransaction = null as Transaction | null export let currentTransaction = null as Transaction | null
/**
* 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
*/
export function transaction<T>(fn: (rollback: () => void) => T) { export function transaction<T>(fn: (rollback: () => void) => T) {
const txn = new Transaction(currentTransaction) const txn = new Transaction(currentTransaction)
@ -238,12 +166,6 @@ export function transaction<T>(fn: (rollback: () => void) => T) {
} }
} }
/**
* 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 { export function transact<T>(fn: () => T): T {
if (currentTransaction) { if (currentTransaction) {
return fn() return fn()

View file

@ -3,7 +3,7 @@ import { _Computed } from './Computed'
import { EffectScheduler } from './EffectScheduler' import { EffectScheduler } from './EffectScheduler'
/** @public */ /** @public */
export const RESET_VALUE: unique symbol = Symbol('RESET_VALUE') export const RESET_VALUE: unique symbol = Symbol.for('com.tldraw.state/RESET_VALUE')
/** @public */ /** @public */
export type RESET_VALUE = typeof RESET_VALUE export type RESET_VALUE = typeof RESET_VALUE