From b133c59391400a27cc7d91084bfd72be72a8dd43 Mon Sep 17 00:00:00 2001 From: David Sheldrick Date: Thu, 14 Dec 2023 13:35:34 +0000 Subject: [PATCH] Use a global singleton for tlstate (#2322) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One minor issue with signia is that it uses global state for bookkeeping, so it is potentially disastrous if there is more than one version of it included in a bundle. To prevent that being an issue before we had a warning that would trigger if signia detects multiple initializations. > Multiple versions of @tldraw/state detected. This will cause unexpected behavior. Please add "resolutions" (yarn/pnpm) or "overrides" (npm) in your package.json to ensure only one version of @tldraw/state is loaded. Alas I think this warning triggers too often in development environments, e.g. during HMR or janky bundlers. Something that can prevent the need for this particular warning is having a global singleton version of signia that we only instantiate once, and then re-use that one on subsequent module initializations. We didn't do this before because it has a few downsides: - breaks HMR if you are working on signia itself, since updated modules won't be used and you'll need to do a full refresh. - introduces the possibility of breakage if we remove or even add APIs to signia. We can't rely on having the latest version of signia be the first to instantiate, and we can't allow later instantiations to take precedence since atoms n stuff may have already been created with the prior version. To mitigate this I've introduced a `apiVersion` const that we can increment when we make any kind of additions or removals. If there is a mismatch between the `apiVersion` in the global singleton vs the currently-initializing module, then it throws. Ultimately i think the pros outweigh the cons here, i.e. far fewer people will see and have to deal with the error message shown above, and fewer people should encounter a situation where the editor appears to load but nothing changes when you interact with it. ### Change Type - [x] `patch` — Bug fix [^1]: publishes a `patch` release, for devDependencies use `internal` [^2]: will not publish a new version ### Release Notes - Make a global singleton for tlstate. --- packages/state/api-report.md | 303 +++- packages/state/api/api.json | 1383 +++-------------- packages/state/src/lib/core/Atom.ts | 22 - packages/state/src/lib/core/Computed.ts | 74 +- .../state/src/lib/core/EffectScheduler.ts | 52 - packages/state/src/lib/core/capture.ts | 54 - packages/state/src/lib/core/index.ts | 349 ++++- packages/state/src/lib/core/isSignal.ts | 4 - packages/state/src/lib/core/transactions.ts | 78 - packages/state/src/lib/core/types.ts | 2 +- 10 files changed, 821 insertions(+), 1500 deletions(-) diff --git a/packages/state/api-report.md b/packages/state/api-report.md index 1eb39796f..e0fe68732 100644 --- a/packages/state/api-report.md +++ b/packages/state/api-report.md @@ -14,10 +14,26 @@ export interface Atom extends Signal { } // @public -export function atom( -name: string, -initialValue: Value, -options?: AtomOptions): Atom; +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; // @public export interface AtomOptions { @@ -36,13 +52,26 @@ export interface Computed extends Signal { } // @public -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; +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; // @public export interface ComputedOptions { @@ -52,48 +81,124 @@ export interface ComputedOptions { } // @public -export class EffectScheduler { - constructor(name: string, runEffect: (lastReactedEpoch: number) => Result, options?: EffectSchedulerOptions); - attach(): void; - detach(): void; - execute(): Result; - get isActivelyListening(): boolean; - // @internal (undocumented) - lastTraversedEpoch: number; - // @internal (undocumented) - maybeScheduleEffect(): void; - // (undocumented) - readonly name: string; - // @internal (undocumented) - parentEpochs: number[]; - // @internal (undocumented) - parents: Signal[]; - get scheduleCount(): number; - // @internal (undocumented) - scheduleEffect(): void; -} +export const +/** +* Creates a new [[Atom]]. +* +* An Atom is a signal that can be updated directly by calling [[Atom.set]] or [[Atom.update]]. +* +* @example +* ```ts +* const name = atom('name', 'John') +* +* name.get() // 'John' +* +* name.set('Jane') +* +* name.get() // 'Jane' +* ``` +* +* @public +*/ +EffectScheduler: typeof EffectScheduler_2; // @public (undocumented) -export const EMPTY_ARRAY: []; +export const +/** +* Creates a new [[Atom]]. +* +* An Atom is a signal that can be updated directly by calling [[Atom.set]] or [[Atom.update]]. +* +* @example +* ```ts +* const name = atom('name', 'John') +* +* name.get() // 'John' +* +* name.set('Jane') +* +* name.get() // 'Jane' +* ``` +* +* @public +*/ +EMPTY_ARRAY: []; // @public export function getComputedInstance(obj: Obj, propertyName: Prop): Computed; // @public -export function isAtom(value: unknown): value is Atom; +export const +/** +* Creates a new [[Atom]]. +* +* An Atom is a signal that can be updated directly by calling [[Atom.set]] or [[Atom.update]]. +* +* @example +* ```ts +* const name = atom('name', 'John') +* +* name.get() // 'John' +* +* name.set('Jane') +* +* name.get() // 'Jane' +* ``` +* +* @public +*/ +isAtom: typeof isAtom_2; // @public -export function isSignal(value: any): value is Signal; +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 export const isUninitialized: (value: any) => value is typeof UNINITIALIZED; // @public -export function react(name: string, fn: (lastReactedEpoch: number) => any, options?: EffectSchedulerOptions): () => void; +export const +/** +* Creates a new [[Atom]]. +* +* An Atom is a signal that can be updated directly by calling [[Atom.set]] or [[Atom.update]]. +* +* @example +* ```ts +* const name = atom('name', 'John') +* +* name.get() // 'John' +* +* name.set('Jane') +* +* name.get() // 'Jane' +* ``` +* +* @public +*/ +react: typeof react_2; // @public export interface Reactor { - scheduler: EffectScheduler; + scheduler: EffectScheduler_2; start(options?: { force?: boolean; }): void; @@ -101,7 +206,26 @@ export interface Reactor { } // @public -export function reactor(name: string, fn: (lastReactedEpoch: number) => Result, options?: EffectSchedulerOptions): Reactor; +export const +/** +* Creates a new [[Atom]]. +* +* An Atom is a signal that can be updated directly by calling [[Atom.set]] or [[Atom.update]]. +* +* @example +* ```ts +* const name = atom('name', 'John') +* +* name.get() // 'John' +* +* name.set('Jane') +* +* name.get() // 'Jane' +* ``` +* +* @public +*/ +reactor: typeof reactor_2; // @public (undocumented) export const RESET_VALUE: unique symbol; @@ -126,13 +250,70 @@ export interface Signal { export function track>(baseComponent: T): T extends React_2.MemoExoticComponent ? T : React_2.MemoExoticComponent; // @public -export function transact(fn: () => T): T; +export const +/** +* Creates a new [[Atom]]. +* +* An Atom is a signal that can be updated directly by calling [[Atom.set]] or [[Atom.update]]. +* +* @example +* ```ts +* const name = atom('name', 'John') +* +* name.get() // 'John' +* +* name.set('Jane') +* +* name.get() // 'Jane' +* ``` +* +* @public +*/ +transact: typeof transact_2; // @public -export function transaction(fn: (rollback: () => void) => T): T; +export const +/** +* Creates a new [[Atom]]. +* +* An Atom is a signal that can be updated directly by calling [[Atom.set]] or [[Atom.update]]. +* +* @example +* ```ts +* const name = atom('name', 'John') +* +* name.get() // 'John' +* +* name.set('Jane') +* +* name.get() // 'Jane' +* ``` +* +* @public +*/ +transaction: typeof transaction_2; // @public -export function unsafe__withoutCapture(fn: () => T): T; +export const +/** +* Creates a new [[Atom]]. +* +* An Atom is a signal that can be updated directly by calling [[Atom.set]] or [[Atom.update]]. +* +* @example +* ```ts +* const name = atom('name', 'John') +* +* name.get() // 'John' +* +* name.set('Jane') +* +* name.get() // 'Jane' +* ``` +* +* @public +*/ +unsafe__withoutCapture: typeof unsafe__withoutCapture_2; // @public export function useAtom( @@ -162,10 +343,48 @@ export function useValue(value: Signal): Value; export function useValue(name: string, fn: () => Value, deps: unknown[]): Value; // @public -export function whyAmIRunning(): void; +export const +/** +* Creates a new [[Atom]]. +* +* An Atom is a signal that can be updated directly by calling [[Atom.set]] or [[Atom.update]]. +* +* @example +* ```ts +* const name = atom('name', 'John') +* +* name.get() // 'John' +* +* name.set('Jane') +* +* name.get() // 'Jane' +* ``` +* +* @public +*/ +whyAmIRunning: typeof whyAmIRunning_2; // @public -export function withDiff(value: Value, diff: Diff): WithDiff; +export const +/** +* Creates a new [[Atom]]. +* +* An Atom is a signal that can be updated directly by calling [[Atom.set]] or [[Atom.update]]. +* +* @example +* ```ts +* const name = atom('name', 'John') +* +* name.get() // 'John' +* +* name.set('Jane') +* +* name.get() // 'Jane' +* ``` +* +* @public +*/ +withDiff: typeof withDiff_2; // (No @packageDocumentation comment for this package) diff --git a/packages/state/api/api.json b/packages/state/api/api.json index ad361e6fc..5b3e16992 100644 --- a/packages/state/api/api.json +++ b/packages/state/api/api.json @@ -172,125 +172,6 @@ "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", @@ -465,6 +346,34 @@ } ] }, + { + "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", @@ -593,318 +502,6 @@ ], "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:class" - }, - { - "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", @@ -1000,6 +597,34 @@ } ] }, + { + "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", @@ -1129,284 +754,32 @@ "extendsTokenRanges": [] }, { - "kind": "Class", - "canonicalReference": "@tldraw/state!EffectScheduler:class", + "kind": "Variable", + "canonicalReference": "@tldraw/state!EffectScheduler:var", "docComment": "/**\n * An EffectScheduler is responsible for executing side effects in response to changes in state.\n *\n * You probably don't need to use this directly unless you're integrating this library with a framework of some kind.\n *\n * Instead, use the [[react]] and [[reactor]] functions.\n *\n * @example\n * ```ts\n * const render = new EffectScheduler('render', drawToCanvas)\n *\n * render.attach()\n * render.execute()\n * ```\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "export declare class EffectScheduler " + "text": "EffectScheduler: " + }, + { + "kind": "Content", + "text": "typeof " + }, + { + "kind": "Reference", + "text": "_EffectScheduler", + "canonicalReference": "@tldraw/state!~EffectScheduler_2:class" } ], - "fileUrlPath": "packages/state/src/lib/core/EffectScheduler.ts", + "fileUrlPath": "packages/state/src/lib/core/index.ts", + "isReadonly": true, "releaseTag": "Public", - "typeParameters": [ - { - "typeParameterName": "Result", - "constraintTokenRange": { - "startIndex": 0, - "endIndex": 0 - }, - "defaultTypeTokenRange": { - "startIndex": 0, - "endIndex": 0 - } - } - ], - "isAbstract": false, "name": "EffectScheduler", - "preserveMemberOrder": false, - "members": [ - { - "kind": "Constructor", - "canonicalReference": "@tldraw/state!EffectScheduler:constructor(1)", - "docComment": "/**\n * Constructs a new instance of the `EffectScheduler` class\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "constructor(name: " - }, - { - "kind": "Content", - "text": "string" - }, - { - "kind": "Content", - "text": ", runEffect: " - }, - { - "kind": "Content", - "text": "(lastReactedEpoch: number) => Result" - }, - { - "kind": "Content", - "text": ", options?: " - }, - { - "kind": "Reference", - "text": "EffectSchedulerOptions", - "canonicalReference": "@tldraw/state!~EffectSchedulerOptions:interface" - }, - { - "kind": "Content", - "text": ");" - } - ], - "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "name", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false - }, - { - "parameterName": "runEffect", - "parameterTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 - }, - "isOptional": false - }, - { - "parameterName": "options", - "parameterTypeTokenRange": { - "startIndex": 5, - "endIndex": 6 - }, - "isOptional": true - } - ] - }, - { - "kind": "Method", - "canonicalReference": "@tldraw/state!EffectScheduler#attach:member(1)", - "docComment": "/**\n * Makes this scheduler become 'actively listening' to its parents. If it has been executed before it will immediately become eligible to receive 'maybeScheduleEffect' calls. If it has not executed before it will need to be manually executed once to become eligible for scheduling, i.e. by calling [[EffectScheduler.execute]].\n *\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "attach(): " - }, - { - "kind": "Content", - "text": "void" - }, - { - "kind": "Content", - "text": ";" - } - ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [], - "isOptional": false, - "isAbstract": false, - "name": "attach" - }, - { - "kind": "Method", - "canonicalReference": "@tldraw/state!EffectScheduler#detach:member(1)", - "docComment": "/**\n * Makes this scheduler stop 'actively listening' to its parents. It will no longer be eligible to receive 'maybeScheduleEffect' calls until [[EffectScheduler.attach]] is called again.\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "detach(): " - }, - { - "kind": "Content", - "text": "void" - }, - { - "kind": "Content", - "text": ";" - } - ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [], - "isOptional": false, - "isAbstract": false, - "name": "detach" - }, - { - "kind": "Method", - "canonicalReference": "@tldraw/state!EffectScheduler#execute:member(1)", - "docComment": "/**\n * Executes the effect immediately and returns the result.\n *\n * @returns The result of the effect.\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "execute(): " - }, - { - "kind": "Content", - "text": "Result" - }, - { - "kind": "Content", - "text": ";" - } - ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [], - "isOptional": false, - "isAbstract": false, - "name": "execute" - }, - { - "kind": "Property", - "canonicalReference": "@tldraw/state!EffectScheduler#isActivelyListening:member", - "docComment": "/**\n * Whether this scheduler is attached and actively listening to its parents.\n *\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "get isActivelyListening(): " - }, - { - "kind": "Content", - "text": "boolean" - }, - { - "kind": "Content", - "text": ";" - } - ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "isActivelyListening", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false - }, - { - "kind": "Property", - "canonicalReference": "@tldraw/state!EffectScheduler#name:member", - "docComment": "", - "excerptTokens": [ - { - "kind": "Content", - "text": "readonly name: " - }, - { - "kind": "Content", - "text": "string" - }, - { - "kind": "Content", - "text": ";" - } - ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "name", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false - }, - { - "kind": "Property", - "canonicalReference": "@tldraw/state!EffectScheduler#scheduleCount:member", - "docComment": "/**\n * The number of times this effect has been scheduled.\n *\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "get scheduleCount(): " - }, - { - "kind": "Content", - "text": "number" - }, - { - "kind": "Content", - "text": ";" - } - ], - "isReadonly": true, - "isOptional": false, - "releaseTag": "Public", - "name": "scheduleCount", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false - } - ], - "implementsTokenRanges": [] + "variableTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 + } }, { "kind": "Variable", @@ -1422,7 +795,7 @@ "text": "[]" } ], - "fileUrlPath": "packages/state/src/lib/core/helpers.ts", + "fileUrlPath": "packages/state/src/lib/core/index.ts", "isReadonly": true, "releaseTag": "Public", "name": "EMPTY_ARRAY", @@ -1538,122 +911,60 @@ "name": "getComputedInstance" }, { - "kind": "Function", - "canonicalReference": "@tldraw/state!isAtom:function(1)", + "kind": "Variable", + "canonicalReference": "@tldraw/state!isAtom:var", "docComment": "/**\n * Returns true if the given value is an [[Atom]].\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "export declare function isAtom(value: " + "text": "isAtom: " }, { "kind": "Content", - "text": "unknown" - }, - { - "kind": "Content", - "text": "): " + "text": "typeof " }, { "kind": "Reference", - "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": ";" + "text": "_isAtom", + "canonicalReference": "@tldraw/state!~isAtom_2:function" } ], - "fileUrlPath": "packages/state/src/lib/core/Atom.ts", - "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 7 - }, + "fileUrlPath": "packages/state/src/lib/core/index.ts", + "isReadonly": true, "releaseTag": "Public", - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "value", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false - } - ], - "name": "isAtom" + "name": "isAtom", + "variableTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 + } }, { - "kind": "Function", - "canonicalReference": "@tldraw/state!isSignal:function(1)", + "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", "excerptTokens": [ { "kind": "Content", - "text": "export declare function isSignal(value: " + "text": "isSignal: " }, { "kind": "Content", - "text": "any" - }, - { - "kind": "Content", - "text": "): " + "text": "typeof " }, { "kind": "Reference", - "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": ";" + "text": "_isSignal", + "canonicalReference": "@tldraw/state!~isSignal_2:function" } ], - "fileUrlPath": "packages/state/src/lib/core/isSignal.ts", - "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 7 - }, + "fileUrlPath": "packages/state/src/lib/core/index.ts", + "isReadonly": true, "releaseTag": "Public", - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "value", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false - } - ], - "name": "isSignal" + "name": "isSignal", + "variableTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 + } }, { "kind": "Variable", @@ -1693,178 +1004,32 @@ } }, { - "kind": "Function", - "canonicalReference": "@tldraw/state!react:function(1)", + "kind": "Variable", + "canonicalReference": "@tldraw/state!react:var", "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": "export declare function react(name: " + "text": "react: " }, { "kind": "Content", - "text": "string" - }, - { - "kind": "Content", - "text": ", fn: " - }, - { - "kind": "Content", - "text": "(lastReactedEpoch: number) => any" - }, - { - "kind": "Content", - "text": ", options?: " + "text": "typeof " }, { "kind": "Reference", - "text": "EffectSchedulerOptions", - "canonicalReference": "@tldraw/state!~EffectSchedulerOptions:interface" - }, - { - "kind": "Content", - "text": "): " - }, - { - "kind": "Content", - "text": "() => void" - }, - { - "kind": "Content", - "text": ";" + "text": "_react", + "canonicalReference": "@tldraw/state!~react_2:function" } ], - "fileUrlPath": "packages/state/src/lib/core/EffectScheduler.ts", - "returnTypeTokenRange": { - "startIndex": 7, - "endIndex": 8 - }, + "fileUrlPath": "packages/state/src/lib/core/index.ts", + "isReadonly": true, "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 - } - ], - "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" + "name": "react", + "variableTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 + } }, { "kind": "Interface", @@ -1914,7 +1079,7 @@ { "kind": "Reference", "text": "EffectScheduler", - "canonicalReference": "@tldraw/state!EffectScheduler:class" + "canonicalReference": "@tldraw/state!~EffectScheduler_2:class" }, { "kind": "Content", @@ -2010,6 +1175,34 @@ ], "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", @@ -2391,178 +1584,88 @@ "name": "track" }, { - "kind": "Function", - "canonicalReference": "@tldraw/state!transact:function(1)", + "kind": "Variable", + "canonicalReference": "@tldraw/state!transact:var", "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": "export declare function transact(fn: " + "text": "transact: " }, { "kind": "Content", - "text": "() => T" + "text": "typeof " }, { - "kind": "Content", - "text": "): " - }, - { - "kind": "Content", - "text": "T" - }, - { - "kind": "Content", - "text": ";" + "kind": "Reference", + "text": "_transact", + "canonicalReference": "@tldraw/state!~transact_2:function" } ], - "fileUrlPath": "packages/state/src/lib/core/transactions.ts", - "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 - }, + "fileUrlPath": "packages/state/src/lib/core/index.ts", + "isReadonly": true, "releaseTag": "Public", - "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" + "name": "transact", + "variableTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 + } }, { - "kind": "Function", - "canonicalReference": "@tldraw/state!transaction:function(1)", + "kind": "Variable", + "canonicalReference": "@tldraw/state!transaction:var", "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": "export declare function transaction(fn: " + "text": "transaction: " }, { "kind": "Content", - "text": "(rollback: () => void) => T" + "text": "typeof " }, { - "kind": "Content", - "text": "): " - }, - { - "kind": "Content", - "text": "T" - }, - { - "kind": "Content", - "text": ";" + "kind": "Reference", + "text": "_transaction", + "canonicalReference": "@tldraw/state!~transaction_2:function" } ], - "fileUrlPath": "packages/state/src/lib/core/transactions.ts", - "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 - }, + "fileUrlPath": "packages/state/src/lib/core/index.ts", + "isReadonly": true, "releaseTag": "Public", - "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" + "name": "transaction", + "variableTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 + } }, { - "kind": "Function", - "canonicalReference": "@tldraw/state!unsafe__withoutCapture:function(1)", + "kind": "Variable", + "canonicalReference": "@tldraw/state!unsafe__withoutCapture:var", "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": "export declare function unsafe__withoutCapture(fn: " + "text": "unsafe__withoutCapture: " }, { "kind": "Content", - "text": "() => T" + "text": "typeof " }, { - "kind": "Content", - "text": "): " - }, - { - "kind": "Content", - "text": "T" - }, - { - "kind": "Content", - "text": ";" + "kind": "Reference", + "text": "_unsafe__withoutCapture", + "canonicalReference": "@tldraw/state!~unsafe__withoutCapture_2:function" } ], - "fileUrlPath": "packages/state/src/lib/core/capture.ts", - "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 - }, + "fileUrlPath": "packages/state/src/lib/core/index.ts", + "isReadonly": true, "releaseTag": "Public", - "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" + "name": "unsafe__withoutCapture", + "variableTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 + } }, { "kind": "Function", @@ -3221,122 +2324,60 @@ "name": "useValue" }, { - "kind": "Function", - "canonicalReference": "@tldraw/state!whyAmIRunning:function(1)", + "kind": "Variable", + "canonicalReference": "@tldraw/state!whyAmIRunning:var", "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": "export declare function whyAmIRunning(): " + "text": "whyAmIRunning: " }, { "kind": "Content", - "text": "void" + "text": "typeof " }, { - "kind": "Content", - "text": ";" + "kind": "Reference", + "text": "_whyAmIRunning", + "canonicalReference": "@tldraw/state!~whyAmIRunning_2:function" } ], - "fileUrlPath": "packages/state/src/lib/core/capture.ts", - "returnTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, + "fileUrlPath": "packages/state/src/lib/core/index.ts", + "isReadonly": true, "releaseTag": "Public", - "overloadIndex": 1, - "parameters": [], - "name": "whyAmIRunning" + "name": "whyAmIRunning", + "variableTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 + } }, { - "kind": "Function", - "canonicalReference": "@tldraw/state!withDiff:function(1)", + "kind": "Variable", + "canonicalReference": "@tldraw/state!withDiff:var", "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": "export declare function withDiff(value: " + "text": "withDiff: " }, { "kind": "Content", - "text": "Value" - }, - { - "kind": "Content", - "text": ", diff: " - }, - { - "kind": "Content", - "text": "Diff" - }, - { - "kind": "Content", - "text": "): " + "text": "typeof " }, { "kind": "Reference", - "text": "WithDiff", - "canonicalReference": "@tldraw/state!~WithDiff:class" - }, - { - "kind": "Content", - "text": "" - }, - { - "kind": "Content", - "text": ";" + "text": "_withDiff", + "canonicalReference": "@tldraw/state!~withDiff_2:function" } ], - "fileUrlPath": "packages/state/src/lib/core/Computed.ts", - "returnTypeTokenRange": { - "startIndex": 5, - "endIndex": 7 - }, + "fileUrlPath": "packages/state/src/lib/core/index.ts", + "isReadonly": true, "releaseTag": "Public", - "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" + "name": "withDiff", + "variableTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 + } } ] } diff --git a/packages/state/src/lib/core/Atom.ts b/packages/state/src/lib/core/Atom.ts index 3343569f0..5d1b99ad9 100644 --- a/packages/state/src/lib/core/Atom.ts +++ b/packages/state/src/lib/core/Atom.ts @@ -162,24 +162,6 @@ export class _Atom implements 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( /** * A name for the signal. This is used for debugging and profiling purposes, it does not need to be unique. @@ -197,10 +179,6 @@ export function atom( 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 d9afa02b2..68b08f246 100644 --- a/packages/state/src/lib/core/Computed.ts +++ b/packages/state/src/lib/core/Computed.ts @@ -8,14 +8,17 @@ import { globalEpoch } from './transactions' import { Child, ComputeDiff, RESET_VALUE, Signal } from './types' import { logComputedGetterWarning, logDotValueWarning } from './warnings' -const UNINITIALIZED = Symbol('UNINITIALIZED') +/** + * @public + */ +export const UNINITIALIZED = Symbol.for('com.tldraw.state/UNINITIALIZED') /** * The type of the first value passed to a computed signal function as the 'prevValue' parameter. * * @see [[isUninitialized]]. * @public */ -type UNINITIALIZED = typeof UNINITIALIZED +export type UNINITIALIZED = typeof UNINITIALIZED /** * Call this inside a computed signal function to determine whether it is the first time the function is being called. @@ -44,28 +47,6 @@ class WithDiff { constructor(public value: Value, public diff: Diff) {} } -/** - * When writing incrementally-computed signals it is convenient (and usually more performant) to incrementally compute the diff too. - * - * You can use this function to wrap the return value of a computed signal function to indicate that the diff should be used instead of calculating a new one with [[AtomOptions.computeDiff]]. - * - * @example - * ```ts - * const count = atom('count', 0) - * const double = computed('double', (prevValue) => { - * const nextValue = count.get() * 2 - * if (isUninitialized(prevValue)) { - * return nextValue - * } - * return withDiff(nextValue, nextValue - prevValue) - * }, { historyLength: 10 }) - * ``` - * - * - * @param value - The value. - * @param diff - The diff. - * @public - */ export function withDiff(value: Value, diff: Diff): WithDiff { return new WithDiff(value, diff) } @@ -347,51 +328,6 @@ export function getComputedInstance( 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 0aafc9eeb..d6d26a02b 100644 --- a/packages/state/src/lib/core/EffectScheduler.ts +++ b/packages/state/src/lib/core/EffectScheduler.ts @@ -37,23 +37,6 @@ interface EffectSchedulerOptions { scheduleEffect?: (execute: () => void) => void } -/** - * An EffectScheduler is responsible for executing side effects in response to changes in state. - * - * You probably don't need to use this directly unless you're integrating this library with a framework of some kind. - * - * Instead, use the [[react]] and [[reactor]] functions. - * - * @example - * ```ts - * const render = new EffectScheduler('render', drawToCanvas) - * - * render.attach() - * render.execute() - * ``` - * - * @public - */ export class EffectScheduler { private _isActivelyListening = false /** @@ -166,36 +149,6 @@ export class 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, @@ -241,11 +194,6 @@ 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/capture.ts b/packages/state/src/lib/core/capture.ts index 6504f3e37..af380d0f9 100644 --- a/packages/state/src/lib/core/capture.ts +++ b/packages/state/src/lib/core/capture.ts @@ -1,17 +1,6 @@ import { attach, detach } from './helpers' import { Child, Signal } from './types' -const tldrawStateGlobalKey = Symbol.for('__@tldraw/state__') -const tldrawStateGlobal = globalThis as { [tldrawStateGlobalKey]?: true } - -if (tldrawStateGlobal[tldrawStateGlobalKey]) { - console.error( - 'Multiple versions of @tldraw/state detected. This will cause unexpected behavior. Please add "resolutions" (yarn/pnpm) or "overrides" (npm) in your package.json to ensure only one version of @tldraw/state is loaded.' - ) -} else { - tldrawStateGlobal[tldrawStateGlobalKey] = true -} - class CaptureStackFrame { offset = 0 numNewParents = 0 @@ -23,29 +12,6 @@ class CaptureStackFrame { let stack: CaptureStackFrame | null = null -/** - * Executes the given function without capturing any parents in the current capture context. - * - * This is mainly useful if you want to run an effect only when certain signals change while also - * dereferencing other signals which should not cause the effect to rerun on their own. - * - * @example - * ```ts - * const name = atom('name', 'Sam') - * const time = atom('time', () => new Date().getTime()) - * - * setInterval(() => { - * time.set(new Date().getTime()) - * }) - * - * react('log name changes', () => { - * print(name.get(), 'was changed at', unsafe__withoutCapture(() => time.get())) - * }) - * - * ``` - * - * @public - */ export function unsafe__withoutCapture(fn: () => T): T { const oldStack = stack stack = null @@ -125,26 +91,6 @@ export function maybeCaptureParent(p: Signal) { } } -/** - * 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 if (!child) { diff --git a/packages/state/src/lib/core/index.ts b/packages/state/src/lib/core/index.ts index 6bc513659..f2a294fc6 100644 --- a/packages/state/src/lib/core/index.ts +++ b/packages/state/src/lib/core/index.ts @@ -1,12 +1,347 @@ -export { atom, isAtom } from './Atom' +import { atom as _atom, isAtom as _isAtom } from './Atom' +import { computed as _computed, withDiff as _withDiff } from './Computed' +import { + EffectScheduler as _EffectScheduler, + react as _react, + reactor as _reactor, +} from './EffectScheduler' +import { + unsafe__withoutCapture as _unsafe__withoutCapture, + whyAmIRunning as _whyAmIRunning, +} from './capture' +import { EMPTY_ARRAY as _EMPTY_ARRAY } from './helpers' +import { isSignal as _isSignal } from './isSignal' +import { transact as _transact, transaction as _transaction } from './transactions' + +const sym = Symbol.for('com.tldraw.state') +const glob = globalThis as any + +// This should be incremented any time an API change is made. i.e. for additions or removals. +// Bugfixes need not increment this. +const currentApiVersion = 1 + +function init() { + return { + apiVersion: currentApiVersion, + atom: _atom, + isAtom: _isAtom, + computed: _computed, + withDiff: _withDiff, + EffectScheduler: _EffectScheduler, + react: _react, + reactor: _reactor, + unsafe__withoutCapture: _unsafe__withoutCapture, + whyAmIRunning: _whyAmIRunning, + EMPTY_ARRAY: _EMPTY_ARRAY, + isSignal: _isSignal, + transact: _transact, + transaction: _transaction, + } +} + +const obj: ReturnType = 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) { + throw new Error( + '@tldraw/state: Multiple versions of @tldraw/state are being used. Please ensure that there is only one version of @tldraw/state in your dependency tree.' + ) +} + export type { Atom, AtomOptions } from './Atom' -export { computed, getComputedInstance, isUninitialized, withDiff } from './Computed' +export { getComputedInstance, isUninitialized } 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' +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 5263c5f4d..fa1406d05 100644 --- a/packages/state/src/lib/core/isSignal.ts +++ b/packages/state/src/lib/core/isSignal.ts @@ -2,10 +2,6 @@ import { _Atom } from './Atom' import { _Computed } from './Computed' import { Signal } from './types' -/** - * Returns true if the given value is a signal (either an Atom or a Computed). - * @public - */ export function isSignal(value: any): value is Signal { 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 99ab550dc..a81508082 100644 --- a/packages/state/src/lib/core/transactions.ts +++ b/packages/state/src/lib/core/transactions.ts @@ -135,78 +135,6 @@ export function atomDidChange(atom: _Atom, previousValue: any) { */ export let currentTransaction = null as Transaction | null -/** - * Batches state updates, deferring side effects until after the transaction completes. - * - * @example - * ```ts - * const firstName = atom('John') - * const lastName = atom('Doe') - * - * react('greet', () => { - * print(`Hello, ${firstName.get()} ${lastName.get()}!`) - * }) - * - * // Logs "Hello, John Doe!" - * - * transaction(() => { - * firstName.set('Jane') - * lastName.set('Smith') - * }) - * - * // Logs "Hello, Jane Smith!" - * ``` - * - * If the function throws, the transaction is aborted and any signals that were updated during the transaction revert to their state before the transaction began. - * - * @example - * ```ts - * const firstName = atom('John') - * const lastName = atom('Doe') - * - * react('greet', () => { - * print(`Hello, ${firstName.get()} ${lastName.get()}!`) - * }) - * - * // Logs "Hello, John Doe!" - * - * transaction(() => { - * firstName.set('Jane') - * throw new Error('oops') - * }) - * - * // Does not log - * // firstName.get() === 'John' - * ``` - * - * A `rollback` callback is passed into the function. - * Calling this will prevent the transaction from committing and will revert any signals that were updated during the transaction to their state before the transaction began. - * - * * @example - * ```ts - * const firstName = atom('John') - * const lastName = atom('Doe') - * - * react('greet', () => { - * print(`Hello, ${firstName.get()} ${lastName.get()}!`) - * }) - * - * // Logs "Hello, John Doe!" - * - * transaction((rollback) => { - * firstName.set('Jane') - * lastName.set('Smith') - * rollback() - * }) - * - * // Does not log - * // firstName.get() === 'John' - * // lastName.get() === 'Doe' - * ``` - * - * @param fn - The function to run in a transaction, called with a function to roll back the change. - * @public - */ export function transaction(fn: (rollback: () => void) => T) { const txn = new Transaction(currentTransaction) @@ -238,12 +166,6 @@ export function transaction(fn: (rollback: () => void) => T) { } } -/** - * Like [transaction](#transaction), but does not create a new transaction if there is already one in progress. - * - * @param fn - The function to run in a transaction. - * @public - */ export function transact(fn: () => T): T { if (currentTransaction) { return fn() diff --git a/packages/state/src/lib/core/types.ts b/packages/state/src/lib/core/types.ts index d95a3631c..89688973e 100644 --- a/packages/state/src/lib/core/types.ts +++ b/packages/state/src/lib/core/types.ts @@ -3,7 +3,7 @@ import { _Computed } from './Computed' import { EffectScheduler } from './EffectScheduler' /** @public */ -export const RESET_VALUE: unique symbol = Symbol('RESET_VALUE') +export const RESET_VALUE: unique symbol = Symbol.for('com.tldraw.state/RESET_VALUE') /** @public */ export type RESET_VALUE = typeof RESET_VALUE