UNPKG

@tldraw/state

Version:

tldraw infinite canvas SDK (state).

679 lines (642 loc) • 21.3 kB
/* 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 { }