diff --git a/packages/state/api-report.md b/packages/state/api-report.md index e0fe68732..6fbb7e21c 100644 --- a/packages/state/api-report.md +++ b/packages/state/api-report.md @@ -14,26 +14,10 @@ export interface Atom extends Signal { } // @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( +name: string, +initialValue: Value, +options?: AtomOptions): Atom; // @public export interface AtomOptions { @@ -52,26 +36,13 @@ export interface Computed extends Signal { } // @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(name: string, compute: (previousValue: typeof UNINITIALIZED | Value, lastComputedEpoch: number) => Value | WithDiff, options?: ComputedOptions): Computed; + +// @public (undocumented) +export function computed(target: any, key: string, descriptor: PropertyDescriptor): PropertyDescriptor; + +// @public (undocumented) +export function computed(options?: ComputedOptions): (target: any, key: string, descriptor: PropertyDescriptor) => PropertyDescriptor; // @public export interface ComputedOptions { @@ -81,124 +52,32 @@ export interface ComputedOptions { } // @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 = __EffectScheduler__; + +// @public (undocumented) +export const EMPTY_ARRAY: []; // @public export function getComputedInstance(obj: Obj, propertyName: Prop): Computed; // @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; -// @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; // @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 { - scheduler: EffectScheduler_2; + scheduler: EffectScheduler; start(options?: { force?: boolean; }): void; @@ -206,26 +85,7 @@ export interface Reactor { } // @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(name: string, fn: (lastReactedEpoch: number) => Result, options?: EffectSchedulerOptions): Reactor; // @public (undocumented) export const RESET_VALUE: unique symbol; @@ -250,70 +110,13 @@ export interface Signal { export function track>(baseComponent: T): T extends React_2.MemoExoticComponent ? T : React_2.MemoExoticComponent; // @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(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(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(fn: () => T): T; // @public export function useAtom( @@ -343,48 +146,10 @@ export function useValue(value: Signal): Value; export function useValue(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: Value, diff: Diff): WithDiff; // (No @packageDocumentation comment for this package) diff --git a/packages/state/api/api.json b/packages/state/api/api.json index 5b3e16992..f5a495d5b 100644 --- a/packages/state/api/api.json +++ b/packages/state/api/api.json @@ -172,6 +172,125 @@ "name": "", "preserveMemberOrder": false, "members": [ + { + "kind": "Function", + "canonicalReference": "@tldraw/state!atom:function(1)", + "docComment": "/**\n * Creates a new [[Atom]].\n *\n * An Atom is a signal that can be updated directly by calling [[Atom.set]] or [[Atom.update]].\n *\n * @example\n * ```ts\n * const name = atom('name', 'John')\n *\n * name.get() // 'John'\n *\n * name.set('Jane')\n *\n * name.get() // 'Jane'\n * ```\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare function atom(\nname: " + }, + { + "kind": "Content", + "text": "string" + }, + { + "kind": "Content", + "text": ", \ninitialValue: " + }, + { + "kind": "Content", + "text": "Value" + }, + { + "kind": "Content", + "text": ", \noptions?: " + }, + { + "kind": "Reference", + "text": "AtomOptions", + "canonicalReference": "@tldraw/state!AtomOptions:interface" + }, + { + "kind": "Content", + "text": "" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Reference", + "text": "Atom", + "canonicalReference": "@tldraw/state!Atom:interface" + }, + { + "kind": "Content", + "text": "" + }, + { + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "packages/state/src/lib/core/Atom.ts", + "returnTypeTokenRange": { + "startIndex": 10, + "endIndex": 12 + }, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "name", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "isOptional": false + }, + { + "parameterName": "initialValue", + "parameterTypeTokenRange": { + "startIndex": 5, + "endIndex": 6 + }, + "isOptional": false + }, + { + "parameterName": "options", + "parameterTypeTokenRange": { + "startIndex": 7, + "endIndex": 9 + }, + "isOptional": true + } + ], + "typeParameters": [ + { + "typeParameterName": "Value", + "constraintTokenRange": { + "startIndex": 0, + "endIndex": 0 + }, + "defaultTypeTokenRange": { + "startIndex": 0, + "endIndex": 0 + } + }, + { + "typeParameterName": "Diff", + "constraintTokenRange": { + "startIndex": 0, + "endIndex": 0 + }, + "defaultTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } + } + ], + "name": "atom" + }, { "kind": "Interface", "canonicalReference": "@tldraw/state!Atom:interface", @@ -346,34 +465,6 @@ } ] }, - { - "kind": "Variable", - "canonicalReference": "@tldraw/state!atom:var", - "docComment": "/**\n * Creates a new [[Atom]].\n *\n * An Atom is a signal that can be updated directly by calling [[Atom.set]] or [[Atom.update]].\n *\n * @example\n * ```ts\n * const name = atom('name', 'John')\n *\n * name.get() // 'John'\n *\n * name.set('Jane')\n *\n * name.get() // 'Jane'\n * ```\n *\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "atom: " - }, - { - "kind": "Content", - "text": "typeof " - }, - { - "kind": "Reference", - "text": "_atom", - "canonicalReference": "@tldraw/state!~atom_2:function" - } - ], - "fileUrlPath": "packages/state/src/lib/core/index.ts", - "isReadonly": true, - "releaseTag": "Public", - "name": "atom", - "variableTypeTokenRange": { - "startIndex": 1, - "endIndex": 3 - } - }, { "kind": "Interface", "canonicalReference": "@tldraw/state!AtomOptions:interface", @@ -502,6 +593,318 @@ ], "extendsTokenRanges": [] }, + { + "kind": "Function", + "canonicalReference": "@tldraw/state!computed:function(1)", + "docComment": "/**\n * Creates a computed signal.\n *\n * @param name - The name of the signal.\n *\n * @param compute - The function that computes the value of the signal.\n *\n * @param options - Options for the signal.\n *\n * @example\n * ```ts\n * const name = atom('name', 'John')\n * const greeting = computed('greeting', () => `Hello ${name.get()}!`)\n * console.log(greeting.get()) // 'Hello John!'\n * ```\n *\n * `computed` may also be used as a decorator for creating computed getter methods.\n *\n * @example\n * ```ts\n * class Counter {\n * max = 100\n * count = atom(0)\n *\n * @computed getRemaining() {\n * return this.max - this.count.get()\n * }\n * }\n * ```\n *\n * You may optionally pass in a [[ComputedOptions]] when used as a decorator:\n *\n * @example\n * ```ts\n * class Counter {\n * max = 100\n * count = atom(0)\n *\n * @computed({isEqual: (a, b) => a === b})\n * getRemaining() {\n * return this.max - this.count.get()\n * }\n * }\n * ```\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare function computed(name: " + }, + { + "kind": "Content", + "text": "string" + }, + { + "kind": "Content", + "text": ", compute: " + }, + { + "kind": "Content", + "text": "(previousValue: typeof " + }, + { + "kind": "Reference", + "text": "UNINITIALIZED", + "canonicalReference": "@tldraw/state!~UNINITIALIZED:var" + }, + { + "kind": "Content", + "text": " | Value, lastComputedEpoch: number) => Value | " + }, + { + "kind": "Reference", + "text": "WithDiff", + "canonicalReference": "@tldraw/state!~WithDiff:type" + }, + { + "kind": "Content", + "text": "" + }, + { + "kind": "Content", + "text": ", options?: " + }, + { + "kind": "Reference", + "text": "ComputedOptions", + "canonicalReference": "@tldraw/state!ComputedOptions:interface" + }, + { + "kind": "Content", + "text": "" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Reference", + "text": "Computed", + "canonicalReference": "@tldraw/state!Computed:interface" + }, + { + "kind": "Content", + "text": "" + }, + { + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "packages/state/src/lib/core/Computed.ts", + "returnTypeTokenRange": { + "startIndex": 14, + "endIndex": 16 + }, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "name", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "isOptional": false + }, + { + "parameterName": "compute", + "parameterTypeTokenRange": { + "startIndex": 5, + "endIndex": 10 + }, + "isOptional": false + }, + { + "parameterName": "options", + "parameterTypeTokenRange": { + "startIndex": 11, + "endIndex": 13 + }, + "isOptional": true + } + ], + "typeParameters": [ + { + "typeParameterName": "Value", + "constraintTokenRange": { + "startIndex": 0, + "endIndex": 0 + }, + "defaultTypeTokenRange": { + "startIndex": 0, + "endIndex": 0 + } + }, + { + "typeParameterName": "Diff", + "constraintTokenRange": { + "startIndex": 0, + "endIndex": 0 + }, + "defaultTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } + } + ], + "name": "computed" + }, + { + "kind": "Function", + "canonicalReference": "@tldraw/state!computed:function(2)", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare function computed(target: " + }, + { + "kind": "Content", + "text": "any" + }, + { + "kind": "Content", + "text": ", key: " + }, + { + "kind": "Content", + "text": "string" + }, + { + "kind": "Content", + "text": ", descriptor: " + }, + { + "kind": "Reference", + "text": "PropertyDescriptor", + "canonicalReference": "!PropertyDescriptor:interface" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Reference", + "text": "PropertyDescriptor", + "canonicalReference": "!PropertyDescriptor:interface" + }, + { + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "packages/state/src/lib/core/Computed.ts", + "returnTypeTokenRange": { + "startIndex": 7, + "endIndex": 8 + }, + "releaseTag": "Public", + "overloadIndex": 2, + "parameters": [ + { + "parameterName": "target", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + }, + { + "parameterName": "key", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "isOptional": false + }, + { + "parameterName": "descriptor", + "parameterTypeTokenRange": { + "startIndex": 5, + "endIndex": 6 + }, + "isOptional": false + } + ], + "name": "computed" + }, + { + "kind": "Function", + "canonicalReference": "@tldraw/state!computed:function(3)", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare function computed(options?: " + }, + { + "kind": "Reference", + "text": "ComputedOptions", + "canonicalReference": "@tldraw/state!ComputedOptions:interface" + }, + { + "kind": "Content", + "text": "" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Content", + "text": "(target: any, key: string, descriptor: " + }, + { + "kind": "Reference", + "text": "PropertyDescriptor", + "canonicalReference": "!PropertyDescriptor:interface" + }, + { + "kind": "Content", + "text": ") => " + }, + { + "kind": "Reference", + "text": "PropertyDescriptor", + "canonicalReference": "!PropertyDescriptor:interface" + }, + { + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "packages/state/src/lib/core/Computed.ts", + "returnTypeTokenRange": { + "startIndex": 6, + "endIndex": 10 + }, + "releaseTag": "Public", + "overloadIndex": 3, + "parameters": [ + { + "parameterName": "options", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 5 + }, + "isOptional": true + } + ], + "typeParameters": [ + { + "typeParameterName": "Value", + "constraintTokenRange": { + "startIndex": 0, + "endIndex": 0 + }, + "defaultTypeTokenRange": { + "startIndex": 0, + "endIndex": 0 + } + }, + { + "typeParameterName": "Diff", + "constraintTokenRange": { + "startIndex": 0, + "endIndex": 0 + }, + "defaultTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } + } + ], + "name": "computed" + }, { "kind": "Interface", "canonicalReference": "@tldraw/state!Computed:interface", @@ -597,34 +1000,6 @@ } ] }, - { - "kind": "Variable", - "canonicalReference": "@tldraw/state!computed:var", - "docComment": "/**\n * Creates a computed signal.\n *\n * @param name - The name of the signal.\n *\n * @param compute - The function that computes the value of the signal.\n *\n * @param options - Options for the signal.\n *\n * @example\n * ```ts\n * const name = atom('name', 'John')\n * const greeting = computed('greeting', () => `Hello ${name.get()}!`)\n * console.log(greeting.get()) // 'Hello John!'\n * ```\n *\n * `computed` may also be used as a decorator for creating computed getter methods.\n *\n * @example\n * ```ts\n * class Counter {\n * max = 100\n * count = atom(0)\n *\n * @computed getRemaining() {\n * return this.max - this.count.get()\n * }\n * }\n * ```\n *\n * You may optionally pass in a [[ComputedOptions]] when used as a decorator:\n *\n * @example\n * ```ts\n * class Counter {\n * max = 100\n * count = atom(0)\n *\n * @computed({isEqual: (a, b) => a === b})\n * getRemaining() {\n * return this.max - this.count.get()\n * }\n * }\n * ```\n *\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "computed: " - }, - { - "kind": "Content", - "text": "typeof " - }, - { - "kind": "Reference", - "text": "_computed", - "canonicalReference": "@tldraw/state!~computed_2:function" - } - ], - "fileUrlPath": "packages/state/src/lib/core/index.ts", - "isReadonly": true, - "releaseTag": "Public", - "name": "computed", - "variableTypeTokenRange": { - "startIndex": 1, - "endIndex": 3 - } - }, { "kind": "Interface", "canonicalReference": "@tldraw/state!ComputedOptions:interface", @@ -753,6 +1128,50 @@ ], "extendsTokenRanges": [] }, + { + "kind": "TypeAlias", + "canonicalReference": "@tldraw/state!EffectScheduler:type", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export type EffectScheduler = " + }, + { + "kind": "Reference", + "text": "__EffectScheduler__", + "canonicalReference": "@tldraw/state!~__EffectScheduler__:class" + }, + { + "kind": "Content", + "text": "" + }, + { + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "packages/state/src/lib/core/EffectScheduler.ts", + "releaseTag": "Public", + "name": "EffectScheduler", + "typeParameters": [ + { + "typeParameterName": "Result", + "constraintTokenRange": { + "startIndex": 0, + "endIndex": 0 + }, + "defaultTypeTokenRange": { + "startIndex": 0, + "endIndex": 0 + } + } + ], + "typeTokenRange": { + "startIndex": 1, + "endIndex": 3 + } + }, { "kind": "Variable", "canonicalReference": "@tldraw/state!EffectScheduler:var", @@ -768,11 +1187,11 @@ }, { "kind": "Reference", - "text": "_EffectScheduler", - "canonicalReference": "@tldraw/state!~EffectScheduler_2:class" + "text": "__EffectScheduler__", + "canonicalReference": "@tldraw/state!~__EffectScheduler__:class" } ], - "fileUrlPath": "packages/state/src/lib/core/index.ts", + "fileUrlPath": "packages/state/src/lib/core/EffectScheduler.ts", "isReadonly": true, "releaseTag": "Public", "name": "EffectScheduler", @@ -795,7 +1214,7 @@ "text": "[]" } ], - "fileUrlPath": "packages/state/src/lib/core/index.ts", + "fileUrlPath": "packages/state/src/lib/core/helpers.ts", "isReadonly": true, "releaseTag": "Public", "name": "EMPTY_ARRAY", @@ -911,60 +1330,122 @@ "name": "getComputedInstance" }, { - "kind": "Variable", - "canonicalReference": "@tldraw/state!isAtom:var", + "kind": "Function", + "canonicalReference": "@tldraw/state!isAtom:function(1)", "docComment": "/**\n * Returns true if the given value is an [[Atom]].\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "isAtom: " + "text": "export declare function isAtom(value: " }, { "kind": "Content", - "text": "typeof " + "text": "unknown" + }, + { + "kind": "Content", + "text": "): " }, { "kind": "Reference", - "text": "_isAtom", - "canonicalReference": "@tldraw/state!~isAtom_2:function" + "text": "value", + "canonicalReference": "@tldraw/state!~value" + }, + { + "kind": "Content", + "text": " is " + }, + { + "kind": "Reference", + "text": "Atom", + "canonicalReference": "@tldraw/state!Atom:interface" + }, + { + "kind": "Content", + "text": "" + }, + { + "kind": "Content", + "text": ";" } ], - "fileUrlPath": "packages/state/src/lib/core/index.ts", - "isReadonly": true, + "fileUrlPath": "packages/state/src/lib/core/Atom.ts", + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 7 + }, "releaseTag": "Public", - "name": "isAtom", - "variableTypeTokenRange": { - "startIndex": 1, - "endIndex": 3 - } + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "value", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], + "name": "isAtom" }, { - "kind": "Variable", - "canonicalReference": "@tldraw/state!isSignal:var", - "docComment": "/**\n * Returns true if the given value is a signal (either an Atom or a Computed).\n *\n * @public\n */\n", + "kind": "Function", + "canonicalReference": "@tldraw/state!isSignal:function(1)", + "docComment": "/**\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "isSignal: " + "text": "export declare function isSignal(value: " }, { "kind": "Content", - "text": "typeof " + "text": "any" + }, + { + "kind": "Content", + "text": "): " }, { "kind": "Reference", - "text": "_isSignal", - "canonicalReference": "@tldraw/state!~isSignal_2:function" + "text": "value", + "canonicalReference": "@tldraw/state!~value" + }, + { + "kind": "Content", + "text": " is " + }, + { + "kind": "Reference", + "text": "Signal", + "canonicalReference": "@tldraw/state!Signal:interface" + }, + { + "kind": "Content", + "text": "" + }, + { + "kind": "Content", + "text": ";" } ], - "fileUrlPath": "packages/state/src/lib/core/index.ts", - "isReadonly": true, + "fileUrlPath": "packages/state/src/lib/core/isSignal.ts", + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 7 + }, "releaseTag": "Public", - "name": "isSignal", - "variableTypeTokenRange": { - "startIndex": 1, - "endIndex": 3 - } + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "value", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], + "name": "isSignal" }, { "kind": "Variable", @@ -1004,32 +1485,178 @@ } }, { - "kind": "Variable", - "canonicalReference": "@tldraw/state!react:var", + "kind": "Function", + "canonicalReference": "@tldraw/state!react:function(1)", "docComment": "/**\n * Starts a new effect scheduler, scheduling the effect immediately.\n *\n * Returns a function that can be called to stop the scheduler.\n *\n * @example\n * ```ts\n * const color = atom('color', 'red')\n * const stop = react('set style', () => {\n * divElem.style.color = color.get()\n * })\n * color.set('blue')\n * // divElem.style.color === 'blue'\n * stop()\n * color.set('green')\n * // divElem.style.color === 'blue'\n * ```\n *\n * Also useful in React applications for running effects outside of the render cycle.\n *\n * @example\n * ```ts\n * useEffect(() => react('set style', () => {\n * divRef.current.style.color = color.get()\n * }), [])\n * ```\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "react: " + "text": "export declare function react(name: " }, { "kind": "Content", - "text": "typeof " + "text": "string" + }, + { + "kind": "Content", + "text": ", fn: " + }, + { + "kind": "Content", + "text": "(lastReactedEpoch: number) => any" + }, + { + "kind": "Content", + "text": ", options?: " }, { "kind": "Reference", - "text": "_react", - "canonicalReference": "@tldraw/state!~react_2:function" + "text": "EffectSchedulerOptions", + "canonicalReference": "@tldraw/state!~EffectSchedulerOptions:interface" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Content", + "text": "() => void" + }, + { + "kind": "Content", + "text": ";" } ], - "fileUrlPath": "packages/state/src/lib/core/index.ts", - "isReadonly": true, + "fileUrlPath": "packages/state/src/lib/core/EffectScheduler.ts", + "returnTypeTokenRange": { + "startIndex": 7, + "endIndex": 8 + }, "releaseTag": "Public", - "name": "react", - "variableTypeTokenRange": { - "startIndex": 1, - "endIndex": 3 - } + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "name", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + }, + { + "parameterName": "fn", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "isOptional": false + }, + { + "parameterName": "options", + "parameterTypeTokenRange": { + "startIndex": 5, + "endIndex": 6 + }, + "isOptional": true + } + ], + "name": "react" + }, + { + "kind": "Function", + "canonicalReference": "@tldraw/state!reactor:function(1)", + "docComment": "/**\n * Creates a [[Reactor]], which is a thin wrapper around an [[EffectScheduler]].\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare function reactor(name: " + }, + { + "kind": "Content", + "text": "string" + }, + { + "kind": "Content", + "text": ", fn: " + }, + { + "kind": "Content", + "text": "(lastReactedEpoch: number) => Result" + }, + { + "kind": "Content", + "text": ", options?: " + }, + { + "kind": "Reference", + "text": "EffectSchedulerOptions", + "canonicalReference": "@tldraw/state!~EffectSchedulerOptions:interface" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Reference", + "text": "Reactor", + "canonicalReference": "@tldraw/state!Reactor:interface" + }, + { + "kind": "Content", + "text": "" + }, + { + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "packages/state/src/lib/core/EffectScheduler.ts", + "returnTypeTokenRange": { + "startIndex": 7, + "endIndex": 9 + }, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "name", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + }, + { + "parameterName": "fn", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "isOptional": false + }, + { + "parameterName": "options", + "parameterTypeTokenRange": { + "startIndex": 5, + "endIndex": 6 + }, + "isOptional": true + } + ], + "typeParameters": [ + { + "typeParameterName": "Result", + "constraintTokenRange": { + "startIndex": 0, + "endIndex": 0 + }, + "defaultTypeTokenRange": { + "startIndex": 0, + "endIndex": 0 + } + } + ], + "name": "reactor" }, { "kind": "Interface", @@ -1079,7 +1706,7 @@ { "kind": "Reference", "text": "EffectScheduler", - "canonicalReference": "@tldraw/state!~EffectScheduler_2:class" + "canonicalReference": "@tldraw/state!EffectScheduler:type" }, { "kind": "Content", @@ -1175,34 +1802,6 @@ ], "extendsTokenRanges": [] }, - { - "kind": "Variable", - "canonicalReference": "@tldraw/state!reactor:var", - "docComment": "/**\n * Creates a [[Reactor]], which is a thin wrapper around an [[EffectScheduler]].\n *\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "reactor: " - }, - { - "kind": "Content", - "text": "typeof " - }, - { - "kind": "Reference", - "text": "_reactor", - "canonicalReference": "@tldraw/state!~reactor_2:function" - } - ], - "fileUrlPath": "packages/state/src/lib/core/index.ts", - "isReadonly": true, - "releaseTag": "Public", - "name": "reactor", - "variableTypeTokenRange": { - "startIndex": 1, - "endIndex": 3 - } - }, { "kind": "TypeAlias", "canonicalReference": "@tldraw/state!RESET_VALUE:type", @@ -1584,88 +2183,178 @@ "name": "track" }, { - "kind": "Variable", - "canonicalReference": "@tldraw/state!transact:var", + "kind": "Function", + "canonicalReference": "@tldraw/state!transact:function(1)", "docComment": "/**\n * Like [transaction](#transaction), but does not create a new transaction if there is already one in progress.\n *\n * @param fn - The function to run in a transaction.\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "transact: " + "text": "export declare function transact(fn: " }, { "kind": "Content", - "text": "typeof " + "text": "() => T" }, { - "kind": "Reference", - "text": "_transact", - "canonicalReference": "@tldraw/state!~transact_2:function" + "kind": "Content", + "text": "): " + }, + { + "kind": "Content", + "text": "T" + }, + { + "kind": "Content", + "text": ";" } ], - "fileUrlPath": "packages/state/src/lib/core/index.ts", - "isReadonly": true, + "fileUrlPath": "packages/state/src/lib/core/transactions.ts", + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, "releaseTag": "Public", - "name": "transact", - "variableTypeTokenRange": { - "startIndex": 1, - "endIndex": 3 - } + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "fn", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], + "typeParameters": [ + { + "typeParameterName": "T", + "constraintTokenRange": { + "startIndex": 0, + "endIndex": 0 + }, + "defaultTypeTokenRange": { + "startIndex": 0, + "endIndex": 0 + } + } + ], + "name": "transact" }, { - "kind": "Variable", - "canonicalReference": "@tldraw/state!transaction:var", + "kind": "Function", + "canonicalReference": "@tldraw/state!transaction:function(1)", "docComment": "/**\n * Batches state updates, deferring side effects until after the transaction completes.\n *\n * @param fn - The function to run in a transaction, called with a function to roll back the change.\n *\n * @example\n * ```ts\n * const firstName = atom('John')\n * const lastName = atom('Doe')\n *\n * react('greet', () => {\n * print(`Hello, ${firstName.get()} ${lastName.get()}!`)\n * })\n *\n * // Logs \"Hello, John Doe!\"\n *\n * transaction(() => {\n * firstName.set('Jane')\n * lastName.set('Smith')\n * })\n *\n * // Logs \"Hello, Jane Smith!\"\n * ```\n *\n * 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.\n *\n * @example\n * ```ts\n * const firstName = atom('John')\n * const lastName = atom('Doe')\n *\n * react('greet', () => {\n * print(`Hello, ${firstName.get()} ${lastName.get()}!`)\n * })\n *\n * // Logs \"Hello, John Doe!\"\n *\n * transaction(() => {\n * firstName.set('Jane')\n * throw new Error('oops')\n * })\n *\n * // Does not log\n * // firstName.get() === 'John'\n * ```\n *\n * 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.\n *\n * *\n *\n * @example\n * ```ts\n * const firstName = atom('John')\n * const lastName = atom('Doe')\n *\n * react('greet', () => {\n * print(`Hello, ${firstName.get()} ${lastName.get()}!`)\n * })\n *\n * // Logs \"Hello, John Doe!\"\n *\n * transaction((rollback) => {\n * firstName.set('Jane')\n * lastName.set('Smith')\n * rollback()\n * })\n *\n * // Does not log\n * // firstName.get() === 'John'\n * // lastName.get() === 'Doe'\n * ```\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "transaction: " + "text": "export declare function transaction(fn: " }, { "kind": "Content", - "text": "typeof " + "text": "(rollback: () => void) => T" }, { - "kind": "Reference", - "text": "_transaction", - "canonicalReference": "@tldraw/state!~transaction_2:function" + "kind": "Content", + "text": "): " + }, + { + "kind": "Content", + "text": "T" + }, + { + "kind": "Content", + "text": ";" } ], - "fileUrlPath": "packages/state/src/lib/core/index.ts", - "isReadonly": true, + "fileUrlPath": "packages/state/src/lib/core/transactions.ts", + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, "releaseTag": "Public", - "name": "transaction", - "variableTypeTokenRange": { - "startIndex": 1, - "endIndex": 3 - } + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "fn", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], + "typeParameters": [ + { + "typeParameterName": "T", + "constraintTokenRange": { + "startIndex": 0, + "endIndex": 0 + }, + "defaultTypeTokenRange": { + "startIndex": 0, + "endIndex": 0 + } + } + ], + "name": "transaction" }, { - "kind": "Variable", - "canonicalReference": "@tldraw/state!unsafe__withoutCapture:var", + "kind": "Function", + "canonicalReference": "@tldraw/state!unsafe__withoutCapture:function(1)", "docComment": "/**\n * Executes the given function without capturing any parents in the current capture context.\n *\n * 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.\n *\n * @example\n * ```ts\n * const name = atom('name', 'Sam')\n * const time = atom('time', () => new Date().getTime())\n *\n * setInterval(() => {\n * time.set(new Date().getTime())\n * })\n *\n * react('log name changes', () => {\n * \t print(name.get(), 'was changed at', unsafe__withoutCapture(() => time.get()))\n * })\n *\n * ```\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "unsafe__withoutCapture: " + "text": "export declare function unsafe__withoutCapture(fn: " }, { "kind": "Content", - "text": "typeof " + "text": "() => T" }, { - "kind": "Reference", - "text": "_unsafe__withoutCapture", - "canonicalReference": "@tldraw/state!~unsafe__withoutCapture_2:function" + "kind": "Content", + "text": "): " + }, + { + "kind": "Content", + "text": "T" + }, + { + "kind": "Content", + "text": ";" } ], - "fileUrlPath": "packages/state/src/lib/core/index.ts", - "isReadonly": true, + "fileUrlPath": "packages/state/src/lib/core/capture.ts", + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, "releaseTag": "Public", - "name": "unsafe__withoutCapture", - "variableTypeTokenRange": { - "startIndex": 1, - "endIndex": 3 - } + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "fn", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], + "typeParameters": [ + { + "typeParameterName": "T", + "constraintTokenRange": { + "startIndex": 0, + "endIndex": 0 + }, + "defaultTypeTokenRange": { + "startIndex": 0, + "endIndex": 0 + } + } + ], + "name": "unsafe__withoutCapture" }, { "kind": "Function", @@ -2324,60 +3013,122 @@ "name": "useValue" }, { - "kind": "Variable", - "canonicalReference": "@tldraw/state!whyAmIRunning:var", + "kind": "Function", + "canonicalReference": "@tldraw/state!whyAmIRunning:function(1)", "docComment": "/**\n * 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.\n *\n * @example\n * ```ts\n * const name = atom('name', 'Bob')\n * react('greeting', () => {\n * \twhyAmIRunning()\n * \tprint('Hello', name.get())\n * })\n *\n * name.set('Alice')\n *\n * // 'greeting' is running because:\n * // 'name' changed => 'Alice'\n * ```\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "whyAmIRunning: " + "text": "export declare function whyAmIRunning(): " }, { "kind": "Content", - "text": "typeof " + "text": "void" }, { - "kind": "Reference", - "text": "_whyAmIRunning", - "canonicalReference": "@tldraw/state!~whyAmIRunning_2:function" + "kind": "Content", + "text": ";" } ], - "fileUrlPath": "packages/state/src/lib/core/index.ts", - "isReadonly": true, - "releaseTag": "Public", - "name": "whyAmIRunning", - "variableTypeTokenRange": { + "fileUrlPath": "packages/state/src/lib/core/capture.ts", + "returnTypeTokenRange": { "startIndex": 1, - "endIndex": 3 - } + "endIndex": 2 + }, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [], + "name": "whyAmIRunning" }, { - "kind": "Variable", - "canonicalReference": "@tldraw/state!withDiff:var", + "kind": "Function", + "canonicalReference": "@tldraw/state!withDiff:function(1)", "docComment": "/**\n * When writing incrementally-computed signals it is convenient (and usually more performant) to incrementally compute the diff too.\n *\n * 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]].\n *\n * @param value - The value.\n *\n * @param diff - The diff.\n *\n * @example\n * ```ts\n * const count = atom('count', 0)\n * const double = computed('double', (prevValue) => {\n * const nextValue = count.get() * 2\n * if (isUninitialized(prevValue)) {\n * return nextValue\n * }\n * return withDiff(nextValue, nextValue - prevValue)\n * }, { historyLength: 10 })\n * ```\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "withDiff: " + "text": "export declare function withDiff(value: " }, { "kind": "Content", - "text": "typeof " + "text": "Value" + }, + { + "kind": "Content", + "text": ", diff: " + }, + { + "kind": "Content", + "text": "Diff" + }, + { + "kind": "Content", + "text": "): " }, { "kind": "Reference", - "text": "_withDiff", - "canonicalReference": "@tldraw/state!~withDiff_2:function" + "text": "WithDiff", + "canonicalReference": "@tldraw/state!~WithDiff:type" + }, + { + "kind": "Content", + "text": "" + }, + { + "kind": "Content", + "text": ";" } ], - "fileUrlPath": "packages/state/src/lib/core/index.ts", - "isReadonly": true, + "fileUrlPath": "packages/state/src/lib/core/Computed.ts", + "returnTypeTokenRange": { + "startIndex": 5, + "endIndex": 7 + }, "releaseTag": "Public", - "name": "withDiff", - "variableTypeTokenRange": { - "startIndex": 1, - "endIndex": 3 - } + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "value", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + }, + { + "parameterName": "diff", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "isOptional": false + } + ], + "typeParameters": [ + { + "typeParameterName": "Value", + "constraintTokenRange": { + "startIndex": 0, + "endIndex": 0 + }, + "defaultTypeTokenRange": { + "startIndex": 0, + "endIndex": 0 + } + }, + { + "typeParameterName": "Diff", + "constraintTokenRange": { + "startIndex": 0, + "endIndex": 0 + }, + "defaultTypeTokenRange": { + "startIndex": 0, + "endIndex": 0 + } + } + ], + "name": "withDiff" } ] } diff --git a/packages/state/src/lib/core/Atom.ts b/packages/state/src/lib/core/Atom.ts index 5d1b99ad9..ef2e40ef7 100644 --- a/packages/state/src/lib/core/Atom.ts +++ b/packages/state/src/lib/core/Atom.ts @@ -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 extends Signal { /** * @internal */ -export class _Atom implements Atom { +class __Atom__ implements Atom { constructor( public readonly name: string, private current: Value, @@ -90,7 +90,7 @@ export class _Atom implements Atom { computeDiff?: ComputeDiff - lastChangedEpoch = globalEpoch + lastChangedEpoch = getGlobalEpoch() children = new ArraySet() @@ -127,21 +127,21 @@ export class _Atom implements Atom { 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 implements Atom { } } +export const _Atom = singleton('Atom', () => __Atom__) +export type _Atom = InstanceType + +/** + * 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( /** * 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( return new _Atom(name, initialValue, options) } +/** + * Returns true if the given value is an [[Atom]]. + * @public + */ export function isAtom(value: unknown): value is Atom { return value instanceof _Atom } diff --git a/packages/state/src/lib/core/Computed.ts b/packages/state/src/lib/core/Computed.ts index 68b08f246..a498b9992 100644 --- a/packages/state/src/lib/core/Computed.ts +++ b/packages/state/src/lib/core/Computed.ts @@ -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 { - constructor(public value: Value, public diff: Diff) {} -} +export const WithDiff = singleton( + 'WithDiff', + () => + class WithDiff { + constructor(public value: Value, public diff: Diff) {} + } +) +export type WithDiff = { 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: Value, diff: Diff): WithDiff { return new WithDiff(value, diff) } @@ -102,7 +129,7 @@ export interface Computed extends Signal { /** * @internal */ -export class _Computed implements Computed { +class __UNSAFE__Computed implements Computed { lastChangedEpoch = GLOBAL_START_EPOCH lastTraversedEpoch = GLOBAL_START_EPOCH @@ -154,8 +181,8 @@ export class _Computed implements Computed { __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 implements Computed { 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 implements Computed { } } +export const _Computed = singleton('Computed', () => __UNSAFE__Computed) +export type _Computed = InstanceType + function computedMethodAnnotation( options: ComputedOptions = {}, _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 | undefined + let d = this[derivationKey] as Computed | 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 | undefined + let d = this[derivationKey] as Computed | undefined if (!d) { d = new _Computed(key, originalMethod!.bind(this) as any, options) @@ -315,7 +345,7 @@ export function getComputedInstance( propertyName: Prop ): Computed { const key = Symbol.for('__@tldraw/state__computed__' + propertyName.toString()) - let inst = obj[key as keyof typeof obj] as _Computed | undefined + let inst = obj[key as keyof typeof obj] as Computed | undefined if (!inst) { // deref to make sure it exists first const val = obj[propertyName] @@ -323,11 +353,56 @@ export function getComputedInstance( val.call(obj) } - inst = obj[key as keyof typeof obj] as _Computed | undefined + inst = obj[key as keyof typeof obj] as Computed | 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(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(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( name: string, compute: ( diff --git a/packages/state/src/lib/core/EffectScheduler.ts b/packages/state/src/lib/core/EffectScheduler.ts index d6d26a02b..c7d2b55b3 100644 --- a/packages/state/src/lib/core/EffectScheduler.ts +++ b/packages/state/src/lib/core/EffectScheduler.ts @@ -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 { +class __EffectScheduler__ { private _isActivelyListening = false /** * Whether this scheduler is attached and actively listening to its parents. @@ -80,11 +80,11 @@ export class EffectScheduler { // 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 { 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 { } } +/** + * 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 = __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 + */ export function react( name: string, fn: (lastReactedEpoch: number) => any, @@ -194,6 +245,11 @@ export interface Reactor { stop(): void } +/** + * Creates a [[Reactor]], which is a thin wrapper around an [[EffectScheduler]]. + * + * @public + */ export function reactor( name: string, fn: (lastReactedEpoch: number) => Result, diff --git a/packages/state/src/lib/core/__tests__/atom.test.ts b/packages/state/src/lib/core/__tests__/atom.test.ts index 424f64f38..5ded8b499 100644 --- a/packages/state/src/lib/core/__tests__/atom.test.ts +++ b/packages/state/src/lib/core/__tests__/atom.test.ts @@ -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) diff --git a/packages/state/src/lib/core/__tests__/capture.test.ts b/packages/state/src/lib/core/__tests__/capture.test.ts index 6c463fb77..7d378b013 100644 --- a/packages/state/src/lib/core/__tests__/capture.test.ts +++ b/packages/state/src/lib/core/__tests__/capture.test.ts @@ -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 = {}) => @@ -22,7 +22,7 @@ const emptyChild = (props: Partial = {}) => 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() diff --git a/packages/state/src/lib/core/__tests__/computed.test.ts b/packages/state/src/lib/core/__tests__/computed.test.ts index 24e148cb3..7a192ff87 100644 --- a/packages/state/src/lib/core/__tests__/computed.test.ts +++ b/packages/state/src/lib/core/__tests__/computed.test.ts @@ -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): number { @@ -12,7 +12,7 @@ function getLastCheckedEpoch(derivation: Computed): 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) }) }) diff --git a/packages/state/src/lib/core/capture.ts b/packages/state/src/lib/core/capture.ts index af380d0f9..da228e8aa 100644 --- a/packages/state/src/lib/core/capture.ts +++ b/packages/state/src/lib/core/capture.ts @@ -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(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) { - 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') } diff --git a/packages/state/src/lib/core/helpers.ts b/packages/state/src/lib/core/helpers.ts index 7c0f3f6f4..11dd6f4a3 100644 --- a/packages/state/src/lib/core/helpers.ts +++ b/packages/state/src/lib/core/helpers.ts @@ -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(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) diff --git a/packages/state/src/lib/core/index.ts b/packages/state/src/lib/core/index.ts index f2a294fc6..05e2fff52 100644 --- a/packages/state/src/lib/core/index.ts +++ b/packages/state/src/lib/core/index.ts @@ -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 = 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(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(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 } diff --git a/packages/state/src/lib/core/isSignal.ts b/packages/state/src/lib/core/isSignal.ts index fa1406d05..57199bfb3 100644 --- a/packages/state/src/lib/core/isSignal.ts +++ b/packages/state/src/lib/core/isSignal.ts @@ -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 { return value instanceof _Atom || value instanceof _Computed } diff --git a/packages/state/src/lib/core/transactions.ts b/packages/state/src/lib/core/transactions.ts index a81508082..afb92d7d1 100644 --- a/packages/state/src/lib/core/transactions.ts +++ b/packages/state/src/lib/core/transactions.ts @@ -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>() + 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>) { - 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>() // 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).children.visit(traverse) @@ -107,7 +110,7 @@ function flushChanges(atoms: Iterable<_Atom>) { r.maybeScheduleEffect() } } finally { - globalIsReacting = false + inst.globalIsReacting = false } } @@ -119,27 +122,95 @@ function flushChanges(atoms: Iterable<_Atom>) { * * @internal */ -export function atomDidChange(atom: _Atom, 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(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(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(fn: () => T): T { - if (currentTransaction) { + if (inst.currentTransaction) { return fn() } return transaction(fn) diff --git a/packages/state/src/lib/core/types.ts b/packages/state/src/lib/core/types.ts index 89688973e..a61a1eebc 100644 --- a/packages/state/src/lib/core/types.ts +++ b/packages/state/src/lib/core/types.ts @@ -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 { } /** @internal */ -export type Child = EffectScheduler | _Computed +export type Child = { + lastTraversedEpoch: number + parents: Signal[] + parentEpochs: number[] + isActivelyListening: boolean +} /** * Computes the diff between the previous and current value.