diff --git a/packages/dataverse/docs/GET_STARTED.md b/packages/dataverse/docs/GET_STARTED.md index 4ab313d..1b6618d 100644 --- a/packages/dataverse/docs/GET_STARTED.md +++ b/packages/dataverse/docs/GET_STARTED.md @@ -1,725 +1,3 @@ # Into the dataverse - Get started with `@theatre/dataverse` -This guide will help you to get started with `dataverse`, the reactive dataflow -library that [Theatre.js](https://www.theatrejs.com) is built on. It is inspired -by ideas in -[functional reactive programming](https://en.wikipedia.org/wiki/Functional_reactive_programming) -and it is optimised for interactivity and animation. - -## Main concepts - -A good analogy for `dataverse` would be a spreadsheet editor application. In a -spreadsheet editor you have cells that store values, cells that store functions, -that manipulate the values of other cells. The cells have identifiers (e.g. -`A1`, `B3`, etc...) that are used to reference them in the functions. These are -similar to the set of tools that `dataverse` provides for manipulating data. -Here's a quick comparison: - -| `dataverse` | Spreadsheet editor analogy | role | -| ----------------------- | ----------------------------------- | ---------------------------------------------------------------------------------- | -| sources (`Box`, `Atom`) | a cell that holds a value | `Box`: holds a simple value, `Atom`: holds an object with (sub)props to be tracked | -| derivations | functions | changes recorded on the value of an `Box` or `Atom` | -| pointers | addresses of the cells (`A1`, `B3`) | they point to a (sub)prop of an `Atom` | - -Note that some concepts in `dataverse` go beyond the spreadsheet analogy. - -## Practical Introduction - -Here we collected a few examples that introduce the main concepts/tools in -`dataverse` through practical examples. We strongly recommend running the -examples on your local machine (see the [Setup](#setup) section to see how to -configure your local environment before running the examples). - -0. [Setup your local environment for running the examples](#setup) -1. [`Box`](#box-store-simple-values) -2. [Observe values](#observe-values) -3. [`map()`](#map) -4. [`prism()`](#prism) - - [A basic example](#a-basic-example) - - [`prism.state()` and `prism.effect()`](#prismstate-and-prismeffect) - - [Other methods of `prism()`](#other-methods-of-prism) -5. [`usePrism()` (from `@theatre/react`)](#useprism) -6. [`Atom`](#atom) - - [`Atom` vs `Box`](#atom-vs-box) - - [`Pointers`](#pointers) - - [Update the value of an `Atom`](#update-the-value-of-an-atom) -7. [`Ticker`](#ticker-and-studioticker) - -### Setup - -You are encouraged to follow the examples on your machine by cloning the -[`theatre-js/theatre`](https://github.com/theatre-js/theatre/) repo and creating -a new directory and file called `dataverse/index.tsx` in -`theatre/packages/playground/src/personal/` (this directory is already added to -`.gitignore`, so you don't have to worry about that). - -### `Box`: store simple values - -Let's start with creating a variable that holds a simple value, which we can -change and observe later: - -```typescript -import {Box} from '@theatre/dataverse' - -// `theatre/packages/playground/src/personal/dataverse/index.tsx` - -const variableB = new Box('some value') -console.log(variableB.get()) // prints 'some value' in the console -``` - -> As you can see there's a naming convention here for boxes (`variableB`), -> pointers (`variableP`), derivations (`variableD`), etc... - -Now we can change the value: - -```typescript -variableB.set('some new value') -console.log(variableB.get()) // prints 'some new value' in the console -``` - -### Observe values - -Let's say you want to watch the value of `variableB` for changes and execute a -callback when it does change. - -```typescript -import {Box} from '@theatre/dataverse' - -const variableB = new Box('some value') -// Change the value of variableB to a random number in every 1000 ms -const interval = setInterval(() => { - variableB.set(Math.random().toString()) - console.log('isHot?', variableB.derivation.isHot) -}, 1000) - -// Watch `variableB` changes and print a message to the console when the value of -// `variableB` changes -const untap = variableB.derivation.changesWithoutValues().tap(() => { - console.log('value of variableB changed', variableB.derivation.getValue()) -}) - -// Stop observing `variableB` after 5000 ms -setTimeout(untap, 5000) - -// Clear the interval after 7000 ms -setTimeout(() => { - clearInterval(interval) - console.log('Interval cleared.') -}, 7000) -``` - -A few notes about the example above: - -- `variableB.derivation.changesWithoutValues()` returns a derivation / tappable that we can - tap into (observe). -- The `tap()` method returns the `untap()` function which unsubscribes the observer function -- As long as `variableB` is tapped (observed) `variableB.derivation.isHot` will - bet set to `true` automatically - -## Hotness -As we saw above, derivations may or may not be "hot" -(the same concept as "hot" Observables[ref](https://medium.com/codingthesmartway-com-blog/getting-started-with-rxjs-part-3-hot-and-cold-observables-4713757c9a88)). A derivation -is hot if and only if it is being tapped. - -If you want to keep a derivation hot manually even if there's no tappable -attached to it anymore, you can use the `keepHot()` method. Why would you -want to keep a derivation hot? Check out this example: - - - - - - - - - - -
- -without `keepHot()` 🥶 - - - -with `keepHot()` 🥵 - -
- -```typescript -const variableD = prism(() => { - return performance.now() -}) -console.log(variableD.getValue()) // e.g. 113.5 -console.log(variableD.getValue()) // e.g. 114 -// Notice they give different values -``` - - - -```typescript -const variableD = prism(() => { - return performance.now() -}) -variableD.keepHot() -console.log(variableD.getValue()) // e.g. 113 -console.log(variableD.getValue()) // e.g. 113 -// Notice they give the same value! -``` - -
- -To see a full example of `keepHot`, check out this modified version -of the example from the section above: - -```typescript -variableB.set('some new value') -console.log(variableB.get()) // prints 'some new value' in the console - -// Change the value of variableB to a random number in every 1000 ms -const interval = setInterval(() => { - variableB.set(Math.random().toString()) - // This will print 'isHot? true' every time, since we kept - // the derivation hot by calling the 'keepHot()' method - console.log('isHot?', variableB.derivation.isHot) -}, 1000) - -// Watch `variableB` changes and print a message to the console when the value of -// `variableB` changes -const untap = variableB.derivation.changesWithoutValues().tap(() => { - console.log('value of variableB changed', variableB.derivation.getValue()) -}) - -// Stop observing `variableB` after 5000 ms -setTimeout(untap, 5000) - -// Keep the derivation hot -variableB.derivation.keepHot() - -// Clear the interval after 7000 ms -setTimeout(() => { - clearInterval(interval) - console.log('Interval cleared.') -}, 7000) -``` - -How does `keepHot` work? It's super simple, it just adds a tap to the derivation ([source](https://github.com/theatre-js/theatre/blob/aec6b2a25135e6264e7529e7d3800c4bc3badee6/packages/dataverse/src/derivations/AbstractDerivation.ts#L103-L105)). - -### `map()` - -It is also possible to create a derivation based on an existing derivation: - -```typescript -const niceNumberB = new Box(5) -const isNiceNumberEvenD = niceNumberB.derivation.map((v) => v % 2 === 0) - -// the following line will print '5, false' to the console -console.log(niceNumberB.get(), isNiceNumberEvenD.getValue()) -``` - -The new derivation will be always up to date with the value of the original -derivation: - -```typescript -import {Box} from '@theatre/dataverse' - -const niceNumberB = new Box(5) -const isNiceNumberEvenD = niceNumberB.derivation.map((v) => - v % 2 === 0 ? 'even' : 'odd', -) - -const untap = isNiceNumberEvenD.changesWithoutValues().tap(() => {}) - -const interval1 = setInterval(untap, 5000) -const interval2 = setInterval(() => { - niceNumberB.set(niceNumberB.get() + 1) - console.log( - `${niceNumberB.get()} is an ${isNiceNumberEvenD.getValue()} number.`, - ) -}, 1000) - -// clear the intervals -setTimeout(() => { - clearInterval(interval1) - console.log('interval1 is cleared.') -}, 7000) - -setTimeout(() => { - clearInterval(interval2) - console.log('interval2 is cleared.') -}, 7000) -``` - -### `prism()` - -At this point we can make derivations that track the value of an other -derivation with [the `.map()` method](#map), but what if we want to track the -value of multiple derivations at once for the new derivation? This is where the -`prism()` function comes into play. - -#### A basic example - -Let's say that we have two derivations and we want to create a derivation that -returns the product of their values. In the spreadsheet analogy it would be like -having two cells with two functions and third cell that contains a function that -calculates the product of the previous two cells. Whenever the first two cells -recalculate their value, the third cell will also do the same. - -Here's how we would solve this problem in `dataverse`: - -```typescript -import {Box, prism} from '@theatre/dataverse' - -const widthB = new Box(1) -const heightB = new Box(2) -const padding = 5 - -const widthWithPaddingD = widthB.derivation.map((w) => w + padding) -const heightWidthPaddingD = heightB.derivation.map((h) => h + padding) - -const areaD = prism(() => { - return widthWithPaddingD.getValue() * heightWidthPaddingD.getValue() -}) - -console.log('area: ', areaD.getValue()) -widthB.set(10) -console.log('new area: ', areaD.getValue()) -``` - -#### `prism.state()` and `prism.effect()` - -Prisms don't always follow the rules of functional programming: they can have -internal states and perform side effects using the `prism.state()` and -`prism.effect()` methods. Their concept and API is very similar to React's -`useState()` and `useEffect()` hooks. - -The important thing to know about them is that: - -- `prism.state()` returns a state variable and a function that updates it. -- `prism.effect()` receives two arguments: - 1. The first one is a key (a string), which should be unique to this effect - inside the prism - 2. The second one is a callback function as an argument that gets executed - when the derivation is created (or the dependencies in the dependency array - change). The callback function may return a clean up function that runs - when the derivation gets updated or removed. - -Let's say you want to create a derivation that tracks the position of the mouse. -This would require the derivation to do the following steps: - -1. Create an internal state where the position of the mouse is stored -2. Attach an event listener that listens to `mousemove` events to the `document` -3. Update the internal state of the position whenever the `mousemove` event is - fired -4. Remove the event listener once the derivation is gone (clean up) - -This is how this derivation would look like in code: - -```typescript -import {prism} from '@theatre/dataverse' - -const mousePositionD = prism(() => { - // Create an internal state (`pos`) where the position of the mouse - // will be stored, and a function that updates it (`setPos`) - const [pos, setPos] = prism.state('pos', {x: 0, y: 0}) - - // Create a side effect that attaches the `mousemove` event listeners - // to the `document` - prism.effect( - 'setupListeners', - () => { - const handleMouseMove = (e: MouseEvent) => { - setPos({x: e.screenX, y: e.screenY}) - } - document.addEventListener('mousemove', handleMouseMove) - - // Clean up after the derivation is gone (remove the event - // listener) - return () => { - document.removeEventListener('mousemove', handleMouseMove) - } - }, - [], - ) - - return pos -}) - -// Display the current position of the mouse using a `h2` element -const p = document.createElement('h2') -const {x, y} = mousePositionD.getValue() -p.textContent = `Position of the cursor: [${x}, ${y}]` -document.querySelector('body')?.append(p) - -// Update the element's content when the position of the mouse -// changes -mousePositionD.changesWithoutValues().tap(() => { - const {x, y} = mousePositionD.getValue() - p.textContent = `Position of the cursor: [${x}, ${y}]` -}) -``` - -#### Other methods of `prism` - -Prism has other methods (`prism.memo()`, `prism.scope()`, `prism.ref()`, etc) -inspired by React hooks, but they aren't used that much in `@theatre/core` and -`@theatre/studio`. You can check out the -[tests](../src/derivations/prism/prism.test.ts) or the -[source code](../src/derivations/prism/prism.ts) to get more familiar with them. - -### `usePrism()` - -You can also use derivations inside of React components with the `usePrism()` -hook from the `@theatre/react` package, which accepts a dependency array for the -second argument. If the prism uses a value that is not a derivation (such as a -simple number, or a pointer), then you need to provide that value to the -dependency array. - -#### A simple example - -Here's a simple example: we have a Box that contains the width and height of a -div (let's call it `panel`). Imagine that we want to have a button that changes -the width of the `panel` to a random number when clicked. - -```typescript -import {Box} from '@theatre/dataverse' -import {usePrism} from '@theatre/react' -import React from 'react' -import ReactDOM from 'react-dom' - -// Set the original width and height -const panelB = new Box({ - dims: {width: 200, height: 100}, -}) - -function changePanelWidth() { - const oldValue = panelB.get() - // Change `width` to a random number between 0 and 200 - panelB.set({dims: {...oldValue.dims, width: Math.round(Math.random() * 200)}}) -} - -const Comp = () => { - const render = usePrism(() => { - const {dims} = panelB.derivation.getValue() - return ( - <> - -
- - ) - }, [panelB]) // Note that `panelB` is in the dependency array - - return render -} - -ReactDOM.render( -
- -
, - document.querySelector('body'), -) -``` - -#### The dependency array - -If you remove `panelB` from the dependency array in the previous example you -might see that there's no change in the functionality of the `Change the width` -button. It surprisingly still works: - -```typescript -// ... -const Comp = () => { - const render = usePrism(() => { - // ... - }, []) // Here we removed `panelB` from the dependency array - - return render -} -// ... -``` - -The reason behind this behavior is that even though the value of `panelB` - the -`Box` instance - is cached, the cached `Box` instance's value is still tracked -inside the callback function (which uses `prism()` under the hood, and handles -every derivation inside as its dependency). However, if you change the value of -the `panelB` variable to another `Box` instance, then that change won't be -recognized inside the callback function if `panelB` is not included in the -dependency array of `usePrism()`. Let's look at another example to make things a -bit more clear: - -```typescript -// ... - -// Set the original width and height -const panelB = new Box({ - dims: {width: 200, height: 100}, -}) - -// Create two new `Box` instances -const theme1B = new Box({backgroundColor: '#bd6888', opacity: 1}) -const theme2B = new Box({backgroundColor: '#5ac777', opacity: 1}) - -function changePanelWidthAndThemeOpacity() { - const oldValue = panelB.get() - // Change `width` to a random number between 0 and 200 - const width = Math.round(Math.random() * 200) - panelB.set({dims: {...oldValue.dims, width}}) - // Change opacity in the themes: - const opacity = width > 100 ? width / 200 : width / 100 - theme1B.set({...theme1B.get(), opacity}) - theme2B.set({...theme2B.get(), opacity}) -} - -// DEPENDENCY ARRAYS DEMO -const Comp = () => { - // Get the width of the panel - const {width} = panelB.derivation.getValue().dims - // If the width of the panel is greater than 100, then - // set the value of the `theme` variable to `theme1B`, - // otherwise use `theme2B` - const theme = width > 100 ? theme1B : theme2B - - const render = usePrism(() => { - const {dims} = panelB.derivation.getValue() - const {backgroundColor, opacity} = theme.get() - return ( - <> - -
- - ) - // Note that if the `theme` variable weren't included in the - // dependency array, then the background color of the div - // wouldn't be updated (the opacity still would). - // (Feel free to try it out.) - }, [theme]) - - return render -} - -// ... -``` - -If you omit the `theme` variable from the previous example, then the background -color of the `div` element will not be updated when the value of the `theme` -variable does, while the opacity would track the changes of the width. This -happens, because in that case the callback function in `usePrism()` caches the -value of `theme`, which is `theme1B` when `usePrims()` is called for the first -time, and updates whenever `theme1B` changes. If you pass down `theme` as a -dependency to `usePrism()`, then the callback function will always use new new -value of `theme` (which is set to `theme2B` if the `div`'s width is smaller than -or equal to `100`), whenever it changes. - -### `Atom` - -Remember how we compared `Box`-es to cells in the spreadsheet-analogy? `Atom`-s -are also like cells in the sense that they also hold a value (although they only -work with objects), but there's a huge difference in how their value gets -updated. - -#### `Atom` vs `Box` - -`Box` uses strict equality for comparing new and old values, while `Atom` tracks -the individual properties and nested properties of an object. The following -example illustrates this difference between the two pretty well: - -```typescript -import {Atom, Box, val, valueDerivation} from '@theatre/dataverse' - -const originalValue = {width: 200, height: 100} - -// Create a `Box` that holds an object -const panelB = new Box(originalValue) - -console.log('old value (Box): ', panelB.derivation.getValue()) -// Print the new value of `panelB` to the console -// every time it changes -panelB.derivation - .changesWithoutValues() - .tap(() => console.log('new value: (Box) ', panelB.derivation.getValue())) - -// Set the value of the `panelB` to a new object that has -// the same properties with the same values as `panelB`. -// Note that this will get recognized as a change, since -// the two objects are not strictly equal. -panelB.set({...panelB.get()}) - -// Create an `Atom` that holds an object -const panelA = new Atom({width: 200, height: 100}) - -console.log('old value (Atom):', val(panelA.pointer)) - -// Create a derivation to track the value of `panelA` -// There are a lot of new information here, we'll come back -// to this line later. -const panelFromAtomD = valueDerivation(panelA.pointer) - -// Print the new value of `panelA` to the console -// every time it changes -panelFromAtomD - .changesWithoutValues() - .tap(() => console.log('new value (Atom):', val(panelA.pointer))) - -// Since the next line sets changes the value of `panelA` to what it -// already holds, it does not get recognized as a change. -// The `.setIn()` method is also new, we'll cover it later. -panelA.setIn(['width'], 200) - -// The next line will trigger a change as expected -panelA.setIn(['width'], 400) -``` - -#### Pointers - -You might have wondered what `val(panelA.pointer)` meant when you read this -line: - -```typescript -console.log('old value (Atom):', val(panelA.pointer)) -``` - -`dataverse` uses pointers that point to the properties and nested properties of -the object that the `Atom` instance holds as its value. - -You can use the pointers to get the value of the property they point to, or to -convert them to a derivation using the `val()` and `useDerivation()` functions: - -```typescript -const panelA = new Atom({width: 200, height: 100}) - -// Create a derivation -const panelFromAtomD = valueDerivation(panelA.pointer) - -// Print the value of the property that belongs to the pointer -// Note that `panelA.pointer` and `panelA.pointer.width` are both -// pointers. -console.log(val(panelA.pointer)) // prints `{width: 200, height: 100}` -console.log(val(panelA.pointer.width)) // prints `100` -console.log(val(panelA.pointer.height)) // prints `200` -``` - -#### Update the value of an `Atom` - -If you want to update the value of an `Atom`, you have first choose the -property/nested property that you want to update. Then you can use the names of -its ancestor properties in an array to define the path to the property for the -`setIn()` method: - -```typescript -const panelA = new Atom({dims: {width: 200, height: 100}}) - -// Sets the value of panelA to `{dims: {width: 400, height: 100}}` -panelA.setIn(['dims', 'width'], 400) -``` - -### `Ticker` and `studioTicker` - -The `Ticker` class helps us to schedule callbacks using a strategy. One such -strategy could by synchronizing the execution of certain actions with the -browser's repaint schedule to avoid changes that are invisible for the user and -would only worsen the performance. This could be implemented using the -`studioTicker` from the `@theatre/studio` package. - -Here's an example: we want to increase the value of a counter variable by 1 in -every 10 ms and print the current value on every repaint for 1 s: - -```typescript -import {Box} from '@theatre/dataverse' -import studioTicker from '@theatre/studio/studioTicker' - -// Clear the console to make tracking the relevant logs easier -console.clear() - -// Create a counter variable -const counterB = new Box(0) -// Create a variable to track the number of repaints -let numberOfRepaints = 0 - -// Increase the value of the counter variable by 1 -// in every 10 ms -const interval = setInterval(() => { - counterB.set(counterB.get() + 1) - console.log(counterB.get()) -}, 10) - -// Increase the number of repaints by one every time -// a repaint happens -const untap = counterB.derivation - .changes(studioTicker) - .tap((newCounterValue) => { - ++numberOfRepaints - console.log(`VALUE ON REPAINT: ${newCounterValue}`) - }) - -// Clean up everything after 1 s -setTimeout(() => { - clearInterval(interval) - untap() - console.log('interval is cleared.') - console.log(`Number of times when the counter got updated: ${counterB.get()}`) - console.log(`Number of repaints: ${numberOfRepaints}`) -}, 1000) -``` - -When I run the example above using a 60 Hz refresh rate monitor (or when the -browser itself limits the repaints to 60 times per second), I see something like -this in the console: - -``` -Number of times when the counter got updated: 98 -Number of repaints: 60 -``` - -What happens is that the counter gets updated about `1000ms / 10ms = 100` times, -but only 60 of these changes can be shown on screen due to the refresh rate of -my monitor. The values of the counter when the repaints happen are also printed -to the console: - -``` -... -94 -VALUE ON REPAINT: 94 -95 -96 -VALUE ON REPAINT: 96 -97 -98 -... -``` - -## Summary - -We only covered the basics, there are much more to `Box`-es, `Atom`-s and -everything else in `dataverse`. You can always check the source code for more -information. +> TODO: Dataverse has changed quite a bit since this guide was written, so let's write a new one. \ No newline at end of file diff --git a/packages/dataverse/src/derivations/IDerivation.ts b/packages/dataverse/src/derivations/IDerivation.ts index dfa72d6..ac10668 100644 --- a/packages/dataverse/src/derivations/IDerivation.ts +++ b/packages/dataverse/src/derivations/IDerivation.ts @@ -23,13 +23,7 @@ export interface IDerivation { */ changes(ticker: Ticker): Tappable - /** - * Like {@link changes} but with a different performance model. `changesWithoutValues` returns a {@link Tappable} that - * updates every time the derivation is updated, even if the value didn't change, and the callback is called without - * the value. The advantage of this is that you have control over when the derivation is freshened, it won't - * automatically be kept fresh. - */ - changesWithoutValues(): Tappable + onStale(cb: () => void): VoidFn /** * Keep the derivation hot, even if there are no tappers (subscribers). diff --git a/packages/dataverse/src/derivations/flatMap.ts b/packages/dataverse/src/derivations/flatMap.ts deleted file mode 100644 index d4fa2ba..0000000 --- a/packages/dataverse/src/derivations/flatMap.ts +++ /dev/null @@ -1,129 +0,0 @@ -import type {$FixMe} from '../types' -import AbstractDerivation from './AbstractDerivation' -import type {IDerivation} from './IDerivation' -import {isDerivation} from './IDerivation' - -enum UPDATE_NEEDED_FROM { - none = 0, - dep = 1, - inner = 2, -} - -const makeFlatMapDerivationClass = () => { - // TODO once prism and AbstractDerivation are merged into one, we should delete this file - class FlatMapDerivation extends AbstractDerivation { - private _innerDerivation: undefined | null | IDerivation - private _staleDependency: UPDATE_NEEDED_FROM - - static displayName = 'flatMap' - - constructor( - readonly _depDerivation: IDerivation, - readonly _fn: (v: DepType) => IDerivation | V, - ) { - super() - this._innerDerivation = undefined - this._staleDependency = UPDATE_NEEDED_FROM.dep - - this._addDependency(_depDerivation) - - return this - } - - _recalculateHot() { - const updateNeededFrom = this._staleDependency - this._staleDependency = UPDATE_NEEDED_FROM.none - - if (updateNeededFrom === UPDATE_NEEDED_FROM.inner) { - // @ts-ignore - return this._innerDerivation.getValue() - } - - const possibleInnerDerivation = this._fn(this._depDerivation.getValue()) - - if (isDerivation(possibleInnerDerivation)) { - this._innerDerivation = possibleInnerDerivation - this._addDependency(possibleInnerDerivation) - return possibleInnerDerivation.getValue() - } else { - return possibleInnerDerivation - } - } - - protected _recalculateCold() { - const possibleInnerDerivation = this._fn(this._depDerivation.getValue()) - - if (isDerivation(possibleInnerDerivation)) { - return possibleInnerDerivation.getValue() - } else { - return possibleInnerDerivation - } - } - - protected _recalculate() { - return this.isHot ? this._recalculateHot() : this._recalculateCold() - } - - protected _reactToDependencyBecomingStale( - msgComingFrom: IDerivation, - ) { - const updateNeededFrom = - msgComingFrom === this._depDerivation - ? UPDATE_NEEDED_FROM.dep - : UPDATE_NEEDED_FROM.inner - - if ( - updateNeededFrom === UPDATE_NEEDED_FROM.inner && - msgComingFrom !== this._innerDerivation - ) { - throw Error( - `got a _reactToDependencyBecomingStale() from neither the dep nor the inner derivation`, - ) - } - - if (this._staleDependency === UPDATE_NEEDED_FROM.none) { - this._staleDependency = updateNeededFrom - - if (updateNeededFrom === UPDATE_NEEDED_FROM.dep) { - this._removeInnerDerivation() - } - } else if (this._staleDependency === UPDATE_NEEDED_FROM.dep) { - } else { - if (updateNeededFrom === UPDATE_NEEDED_FROM.dep) { - this._staleDependency = UPDATE_NEEDED_FROM.dep - this._removeInnerDerivation() - } - } - } - - private _removeInnerDerivation() { - if (this._innerDerivation) { - this._removeDependency(this._innerDerivation) - this._innerDerivation = undefined - } - } - - protected _keepHot() { - this._staleDependency = UPDATE_NEEDED_FROM.dep - this.getValue() - } - - protected _becomeCold() { - this._staleDependency = UPDATE_NEEDED_FROM.dep - this._removeInnerDerivation() - } - } - return FlatMapDerivation -} - -let cls: ReturnType | undefined = undefined - -export default function flatMap( - dep: IDerivation, - fn: (v: V) => R, -): IDerivation ? T : R> { - if (!cls) { - cls = makeFlatMapDerivationClass() - } - return new cls(dep, fn) as $FixMe -} diff --git a/packages/dataverse/src/derivations/iterateAndCountTicks.ts b/packages/dataverse/src/derivations/iterateAndCountTicks.ts index 37030fb..6e8e54c 100644 --- a/packages/dataverse/src/derivations/iterateAndCountTicks.ts +++ b/packages/dataverse/src/derivations/iterateAndCountTicks.ts @@ -17,7 +17,7 @@ export default function* iterateAndCountTicks( } let ticksCountedSinceLastYield = 0 - const untap = d.changesWithoutValues().tap(() => { + const untap = d.onStale(() => { ticksCountedSinceLastYield++ }) diff --git a/packages/dataverse/src/derivations/map.ts b/packages/dataverse/src/derivations/map.ts deleted file mode 100644 index 78c661e..0000000 --- a/packages/dataverse/src/derivations/map.ts +++ /dev/null @@ -1,33 +0,0 @@ -import AbstractDerivation from './AbstractDerivation' -import type {IDerivation} from './IDerivation' - -// Exporting from a function because of the circular dependency with AbstractDerivation -const makeMapDerivationClass = () => - // TODO once prism and AbstractDerivation are merged into one, we should delete this file - class MapDerivation extends AbstractDerivation { - constructor( - private readonly _dep: IDerivation, - private readonly _fn: (t: T) => V, - ) { - super() - this._addDependency(_dep) - } - - _recalculate() { - return this._fn(this._dep.getValue()) - } - - _reactToDependencyBecomingStale() {} - } - -let cls: ReturnType | undefined = undefined - -export default function map( - dep: IDerivation, - fn: (v: V) => R, -): IDerivation { - if (!cls) { - cls = makeMapDerivationClass() - } - return new cls(dep, fn) -} diff --git a/packages/dataverse/src/derivations/prism/prism.ts b/packages/dataverse/src/derivations/prism/prism.ts index 89f3d7b..d3cd2fe 100644 --- a/packages/dataverse/src/derivations/prism/prism.ts +++ b/packages/dataverse/src/derivations/prism/prism.ts @@ -231,25 +231,18 @@ class PrismDerivation implements IDerivation { return new DerivationEmitter(this, ticker).tappable() } - /** - * @deprecated This is renamed to {@link PrismDerivation.onStale}. - */ - changesWithoutValues(): Tappable { - return this.onStale() - } - /** * Returns a tappable that fires every time the prism's state goes from `fresh-\>stale.` */ - onStale(): Tappable { - return new DerivationValuelessEmitter(this).tappable() + onStale(callback: () => void): VoidFn { + return new DerivationValuelessEmitter(this).tappable().tap(callback) } /** * Keep the derivation hot, even if there are no tappers (subscribers). */ keepHot() { - return this.onStale().tap(() => {}) + return this.onStale(() => {}) } /** diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 1eb8877..86777fb 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -132,9 +132,9 @@ type QueueItem = { */ | `queueUpdate()` /** - * `cb` in `item.der.changesWithoutValues(cb)` was called + * `cb` in `item.der.onStale(cb)` was called */ - | `changesWithoutValues(cb)` + | `onStale(cb)` /** * Item was rendered */ @@ -158,7 +158,7 @@ type QueueItem = { */ queueUpdate: () => void /** - * Untaps from `this.der.changesWithoutValues()` + * Untaps from `this.der.unStale()` */ untap: () => void } @@ -327,9 +327,9 @@ export function useDerivation(der: IDerivation, debugLabel?: string): T { } pushToQueue(ref.current) }, - untap: der.changesWithoutValues().tap(() => { + untap: der.onStale(() => { if (TRACE) { - ref.current.debug!.history.push(`changesWithoutValues(cb)`) + ref.current.debug!.history.push(`onStale(cb)`) } ref.current!.queueUpdate() }), diff --git a/theatre/core/src/sequences/playbackControllers/AudioPlaybackController.ts b/theatre/core/src/sequences/playbackControllers/AudioPlaybackController.ts index 771abfc..53f6138 100644 --- a/theatre/core/src/sequences/playbackControllers/AudioPlaybackController.ts +++ b/theatre/core/src/sequences/playbackControllers/AudioPlaybackController.ts @@ -49,7 +49,7 @@ export default class AudioPlaybackController implements IPlaybackController { // We're keeping the rangeD hot, so we can read from it on every tick without // causing unnecessary recalculations - const untapFromRangeD = rangeD.changesWithoutValues().tap(play) + const untapFromRangeD = rangeD.onStale(play) play() this._stopPlayCallback = () => { diff --git a/theatre/core/src/sheetObjects/SheetObject.ts b/theatre/core/src/sheetObjects/SheetObject.ts index 750204d..74b5c93 100644 --- a/theatre/core/src/sheetObjects/SheetObject.ts +++ b/theatre/core/src/sheetObjects/SheetObject.ts @@ -283,9 +283,9 @@ export default class SheetObject implements IdentityDerivationProvider { interpolate(left, right, triple.progression), ) } - const untap = derivation - .changesWithoutValues() - .tap(updateSequenceValueFromItsDerivation) + const untap = derivation.onStale( + updateSequenceValueFromItsDerivation, + ) updateSequenceValueFromItsDerivation() untaps.push(untap) diff --git a/theatre/studio/src/PaneManager.ts b/theatre/studio/src/PaneManager.ts index af7071f..86f4478 100644 --- a/theatre/studio/src/PaneManager.ts +++ b/theatre/studio/src/PaneManager.ts @@ -15,7 +15,7 @@ export default class PaneManager { private _instantiatePanesAsTheyComeIn() { const allPanesD = this._getAllPanes() - allPanesD.changesWithoutValues().tap(() => { + allPanesD.onStale(() => { allPanesD.getValue() }) } diff --git a/theatre/studio/src/Studio.ts b/theatre/studio/src/Studio.ts index 05ff8c0..6a4d3b5 100644 --- a/theatre/studio/src/Studio.ts +++ b/theatre/studio/src/Studio.ts @@ -180,7 +180,7 @@ export class Studio { } } } - projectsD.changesWithoutValues().tap(() => { + projectsD.onStale(() => { attachToProjects(projectsD.getValue()) }) attachToProjects(projectsD.getValue()) diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/HorizontallyScrollableArea.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/HorizontallyScrollableArea.tsx index 73c105d..c770bb8 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/HorizontallyScrollableArea.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/HorizontallyScrollableArea.tsx @@ -311,7 +311,7 @@ function useUpdateScrollFromClippedSpaceRange( const rangeStartInScaledSpace = d.getValue() node.scrollLeft = rangeStartInScaledSpace } - const untap = d.changesWithoutValues().tap(update) + const untap = d.onStale(update) update() const timeout = setTimeout(update, 100) diff --git a/theatre/studio/src/uiComponents/Popover/TooltipContext.tsx b/theatre/studio/src/uiComponents/Popover/TooltipContext.tsx index 8f0330e..1fc2450 100644 --- a/theatre/studio/src/uiComponents/Popover/TooltipContext.tsx +++ b/theatre/studio/src/uiComponents/Popover/TooltipContext.tsx @@ -29,7 +29,7 @@ export const useTooltipOpenState = (): [ }, []) useEffect(() => { - return cur.changesWithoutValues().tap(() => { + return cur.onStale(() => { const flag = cur.getValue() === id if (isOpenRef.current !== flag) isOpenRef.current = flag