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:
parent
4e50c9c162
commit
0b434d61f0
14 changed files with 1411 additions and 925 deletions
|
@ -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
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
class WithDiff<Value, Diff> {
|
||||
constructor(public value: Value, public diff: Diff) {}
|
||||
}
|
||||
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: (
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -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')
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in a new issue