@tldraw/state
Version:
tldraw infinite canvas SDK (state).
679 lines (642 loc) • 21.3 kB
text/typescript
/* Excluded from this release type: ArraySet */
/**
* An Atom is a signal that can be updated directly by calling {@link Atom.set} or {@link Atom.update}.
*
* Atoms are created using the {@link atom} function.
*
* @example
* ```ts
* const name = atom('name', 'John')
*
* print(name.get()) // 'John'
* ```
*
* @public
*/
export declare 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 {@link 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;
}
/**
* Creates a new {@link Atom}.
*
* An Atom is a signal that can be updated directly by calling {@link Atom.set} or {@link Atom.update}.
*
* @example
* ```ts
* const name = atom('name', 'John')
*
* name.get() // 'John'
*
* name.set('Jane')
*
* name.get() // 'Jane'
* ```
*
* @public
*/
export declare 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 {@link AtomOptions}.
*/
options?: AtomOptions<Value, Diff>): Atom<Value, Diff>;
/**
* The options to configure an atom, passed into the {@link atom} function.
* @public
*/
export declare 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 {@link 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 {@link 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;
}
/* Excluded from this release type: Child */
/**
* A computed signal created via `computed`.
*
* @public
*/
export declare interface Computed<Value, Diff = unknown> extends Signal<Value, Diff> {
/**
* Whether this computed child is involved in an actively-running effect graph.
* @public
*/
readonly isActivelyListening: boolean;
/* Excluded from this release type: parentSet */
/* Excluded from this release type: parents */
/* Excluded from this release type: parentEpochs */
}
/**
* 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 {@link 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 declare 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>;
/**
* `@computed` decorator (TC39 decorators).
* @public
*/
export declare function computed<This extends object, Value>(compute: () => Value, context: ClassMethodDecoratorContext<This, () => Value>): () => Value;
/**
* `@computed` decorator (legacy typescript decorator syntax).
*
* @public */
export declare function computed(target: any, key: string, descriptor: PropertyDescriptor): PropertyDescriptor;
/**
* `@computed` decorator with options.
* @public
*/
export declare function computed<Value, Diff = unknown>(options?: ComputedOptions<Value, Diff>): ((target: any, key: string, descriptor: PropertyDescriptor) => PropertyDescriptor) & (<This>(compute: () => Value, context: ClassMethodDecoratorContext<This, () => Value>) => () => Value);
/**
* Computes the diff between the previous and current value.
*
* If the diff cannot be computed for whatever reason, it should return {@link state#RESET_VALUE}.
*
* @public
*/
export declare type ComputeDiff<Value, Diff> = (previousValue: Value, currentValue: Value, lastComputedEpoch: number, currentEpoch: number) => Diff | RESET_VALUE;
/**
* Options for creating computed signals. Used when calling `computed`.
* @public
*/
export declare interface ComputedOptions<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 {@link 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 {@link 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;
}
/* Excluded from this release type: deferAsyncEffects */
/**
* 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 {@link react} and {@link reactor} functions.
*
* @example
* ```ts
* const render = new EffectScheduler('render', drawToCanvas)
*
* render.attach()
* render.execute()
* ```
*
* @public
*/
export declare const EffectScheduler: new <Result>(name: string, runEffect: (lastReactedEpoch: number) => Result, options?: EffectSchedulerOptions) => EffectScheduler<Result>;
/** @public */
export declare interface EffectScheduler<Result> {
/** @internal */
/**
* Whether this scheduler is attached and actively listening to its parents.
* @public
*/
readonly isActivelyListening: boolean;
/* Excluded from this release type: lastTraversedEpoch */
/** @public */
readonly name: string;
/* Excluded from this release type: __debug_ancestor_epochs__ */
/**
* The number of times this effect has been scheduled.
* @public
*/
readonly scheduleCount: number;
/* Excluded from this release type: parentSet */
/* Excluded from this release type: parentEpochs */
/* Excluded from this release type: parents */
/* Excluded from this release type: maybeScheduleEffect */
/* Excluded from this release type: scheduleEffect */
/* Excluded from this release type: maybeExecute */
/**
* Makes this scheduler become 'actively listening' to its parents.
* If it has been executed before it will immediately become eligible to receive 'maybeScheduleEffect' calls.
* If it has not executed before it will need to be manually executed once to become eligible for scheduling, i.e. by calling `EffectScheduler.execute`.
* @public
*/
attach(): void;
/**
* Makes this scheduler stop 'actively listening' to its parents.
* It will no longer be eligible to receive 'maybeScheduleEffect' calls until `EffectScheduler.attach` is called again.
*/
detach(): void;
/**
* Executes the effect immediately and returns the result.
* @returns The result of the effect.
*/
execute(): Result;
}
/** @public */
export declare interface EffectSchedulerOptions {
/**
* scheduleEffect is a function that will be called when the effect is scheduled.
*
* It can be used to defer running effects until a later time, for example to batch them together with requestAnimationFrame.
*
*
* @example
* ```ts
* let isRafScheduled = false
* const scheduledEffects: Array<() => void> = []
* const scheduleEffect = (runEffect: () => void) => {
* scheduledEffects.push(runEffect)
* if (!isRafScheduled) {
* isRafScheduled = true
* requestAnimationFrame(() => {
* isRafScheduled = false
* scheduledEffects.forEach((runEffect) => runEffect())
* scheduledEffects.length = 0
* })
* }
* }
* const stop = react('set page title', () => {
* document.title = doc.title,
* }, scheduleEffect)
* ```
*
* @param execute - A function that will execute the effect.
* @returns
*/
scheduleEffect?: (execute: () => void) => void;
}
/**
* @public
*/
export declare const EMPTY_ARRAY: [];
/**
* Retrieves the underlying computed instance for a given property created with the `computed`
* decorator.
*
* @example
* ```ts
* class Counter {
* max = 100
* count = atom(0)
*
* @computed getRemaining() {
* return this.max - this.count.get()
* }
* }
*
* const c = new Counter()
* const remaining = getComputedInstance(c, 'getRemaining')
* remaining.get() === 100 // true
* c.count.set(13)
* remaining.get() === 87 // true
* ```
*
* @param obj - The object
* @param propertyName - The property name
* @public
*/
export declare function getComputedInstance<Obj extends object, Prop extends keyof Obj>(obj: Obj, propertyName: Prop): Computed<Obj[Prop]>;
/**
* Returns true if the given value is an {@link Atom}.
* @public
*/
export declare function isAtom(value: unknown): value is Atom<unknown>;
/**
* @public
*/
export declare function isSignal(value: any): value is Signal<any>;
/**
* Call this inside a computed signal function to determine whether it is the first time the function is being called.
*
* Mainly useful for incremental signal computation.
*
* @example
* ```ts
* const count = atom('count', 0)
* const double = computed('double', (prevValue) => {
* if (isUninitialized(prevValue)) {
* print('First time!')
* }
* return count.get() * 2
* })
* ```
*
* @param value - The value to check.
* @public
*/
export declare function isUninitialized(value: any): value is UNINITIALIZED;
/**
* 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 declare function react(name: string, fn: (lastReactedEpoch: number) => any, options?: EffectSchedulerOptions): () => void;
/**
* The reactor is a user-friendly interface for starting and stopping an `EffectScheduler`.
*
* Calling `.start()` will attach the scheduler and execute the effect immediately the first time it is called.
*
* If the reactor is stopped, calling `.start()` will re-attach the scheduler but will only execute the effect if any of its parents have changed since it was stopped.
*
* You can create a reactor with {@link reactor}.
* @public
*/
export declare interface Reactor<T = unknown> {
/**
* The underlying effect scheduler.
* @public
*/
scheduler: EffectScheduler<T>;
/**
* Start the scheduler. The first time this is called the effect will be scheduled immediately.
*
* If the reactor is stopped, calling this will start the scheduler again but will only execute the effect if any of its parents have changed since it was stopped.
*
* If you need to force re-execution of the effect, pass `{ force: true }`.
* @public
*/
start(options?: {
force?: boolean;
}): void;
/**
* Stop the scheduler.
* @public
*/
stop(): void;
}
/**
* Creates a {@link Reactor}, which is a thin wrapper around an `EffectScheduler`.
*
* @public
*/
export declare function reactor<Result>(name: string, fn: (lastReactedEpoch: number) => Result, options?: EffectSchedulerOptions): Reactor<Result>;
/** @public */
export declare const RESET_VALUE: unique symbol;
/** @public */
export declare type RESET_VALUE = typeof RESET_VALUE;
/**
* A Signal is a reactive value container. The value may change over time, and it may keep track of the diffs between sequential values.
*
* There are two types of signal:
*
* - Atomic signals, created using {@link atom}. These are mutable references to values that can be changed using {@link Atom.set}.
* - Computed signals, created using `computed`. These are values that are computed from other signals. They are recomputed lazily if their dependencies change.
*
* @public
*/
export declare interface Signal<Value, Diff = unknown> {
/**
* The name of the signal. This is used at runtime for debugging and perf profiling only. It does not need to be globally unique.
*/
name: string;
/**
* The current value of the signal. This is a reactive value, and will update when the signal changes.
* Any computed signal that depends on this signal will be lazily recomputed if this signal changes.
* Any effect that depends on this signal will be rescheduled if this signal changes.
*/
get(): Value;
/**
* The epoch when this signal's value last changed. Note that this is not the same as when the value was last computed.
* A signal may recompute it's value without changing it.
*/
lastChangedEpoch: number;
/**
* Returns the sequence of diffs between the the value at the given epoch and the current value.
* Returns the `RESET_VALUE` constant if there is not enough information to compute the diff sequence.
* @param epoch - The epoch to get diffs since.
*/
getDiffSince(epoch: number): Diff[] | RESET_VALUE;
/**
* Returns the current value of the signal without capturing it as a dependency.
* Use this if you need to retrieve the signal's value in a hot loop where the performance overhead of dependency tracking is too high.
*/
__unsafe__getWithoutCapture(ignoreErrors?: boolean): Value;
/* Excluded from this release type: children */
}
/**
* Like {@link 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 declare function transact<T>(fn: () => T): T;
/**
* 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 declare function transaction<T>(fn: (rollback: () => void) => T): T;
/**
* @public
*/
export declare const UNINITIALIZED: unique symbol;
/**
* The type of the first value passed to a computed signal function as the 'prevValue' parameter.
*
* @see {@link isUninitialized}.
* @public
*/
export declare type UNINITIALIZED = typeof UNINITIALIZED;
/**
* 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 declare function unsafe__withoutCapture<T>(fn: () => T): T;
/**
* 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 declare function whyAmIRunning(): void;
/** @public */
export declare const WithDiff: {
new <Value, Diff>(value: Value, diff: Diff): {
diff: Diff;
value: Value;
};
};
/** @public */
export declare interface 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 {@link 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 declare function withDiff<Value, Diff>(value: Value, diff: Diff): WithDiff<Value, Diff>;
export { }