@tomorrowevening/theatre-dataverse 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/LICENSE +203 -0
  2. package/README.md +709 -0
  3. package/dist/Atom.d.ts +127 -0
  4. package/dist/Atom.d.ts.map +1 -0
  5. package/dist/PointerProxy.d.ts +33 -0
  6. package/dist/PointerProxy.d.ts.map +1 -0
  7. package/dist/Ticker.d.ts +105 -0
  8. package/dist/Ticker.d.ts.map +1 -0
  9. package/dist/atom.typeTest.d.ts +2 -0
  10. package/dist/atom.typeTest.d.ts.map +1 -0
  11. package/dist/index.d.ts +19 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +1555 -0
  14. package/dist/index.js.map +7 -0
  15. package/dist/pointer.d.ts +107 -0
  16. package/dist/pointer.d.ts.map +1 -0
  17. package/dist/pointer.typeTest.d.ts +2 -0
  18. package/dist/pointer.typeTest.d.ts.map +1 -0
  19. package/dist/pointerToPrism.d.ts +20 -0
  20. package/dist/pointerToPrism.d.ts.map +1 -0
  21. package/dist/prism/Interface.d.ts +33 -0
  22. package/dist/prism/Interface.d.ts.map +1 -0
  23. package/dist/prism/asyncIterateOver.d.ts +1 -0
  24. package/dist/prism/asyncIterateOver.d.ts.map +1 -0
  25. package/dist/prism/discoveryMechanism.d.ts +4 -0
  26. package/dist/prism/discoveryMechanism.d.ts.map +1 -0
  27. package/dist/prism/iterateAndCountTicks.d.ts +7 -0
  28. package/dist/prism/iterateAndCountTicks.d.ts.map +1 -0
  29. package/dist/prism/iterateOver.d.ts +4 -0
  30. package/dist/prism/iterateOver.d.ts.map +1 -0
  31. package/dist/prism/prism.d.ts +175 -0
  32. package/dist/prism/prism.d.ts.map +1 -0
  33. package/dist/setupTestEnv.d.ts +2 -0
  34. package/dist/setupTestEnv.d.ts.map +1 -0
  35. package/dist/tsdoc-metadata.json +11 -0
  36. package/dist/types.d.ts +6 -0
  37. package/dist/types.d.ts.map +1 -0
  38. package/dist/utils/Stack.d.ts +16 -0
  39. package/dist/utils/Stack.d.ts.map +1 -0
  40. package/dist/utils/typeTestUtils.d.ts +11 -0
  41. package/dist/utils/typeTestUtils.d.ts.map +1 -0
  42. package/dist/utils/updateDeep.d.ts +3 -0
  43. package/dist/utils/updateDeep.d.ts.map +1 -0
  44. package/dist/val.d.ts +14 -0
  45. package/dist/val.d.ts.map +1 -0
  46. package/package.json +50 -0
package/README.md ADDED
@@ -0,0 +1,709 @@
1
+ # @theatre/dataverse
2
+
3
+ Dataverse is the reactive dataflow library
4
+ [Theatre.js](https://www.theatrejs.com) is built on. It is inspired by ideas in
5
+ [functional reactive programming](https://en.wikipedia.org/wiki/Functional_reactive_programming)
6
+ and it is optimised for interactivity and animation.
7
+
8
+ ## Installation
9
+
10
+ ```sh
11
+ $ npm install @theatre/dataverse
12
+ # and the react bindings
13
+ $ npm install @theatre/react
14
+ ```
15
+
16
+ ## Usage with React
17
+
18
+ ```tsx
19
+ import {Atom} from '@theatre/dataverse'
20
+ import {useVal} from '@theatre/react'
21
+ import {useEffect} from 'react'
22
+
23
+ // Atoms hold state
24
+ const atom = new Atom({count: 0, ready: false})
25
+
26
+ const increaseCount = () =>
27
+ atom.setByPointer(atom.pointer.count, (count) => count + 1)
28
+
29
+ function Component() {
30
+ // useVal is a hook that subscribes to changes in a specific path inside the atom
31
+ const ready = useVal(
32
+ // atom.pointer is a type-safe way to refer to a path inside the atom
33
+ atom.pointer.ready,
34
+ )
35
+
36
+ if (!ready) {
37
+ return <div>Loading...</div>
38
+ } else {
39
+ return <button onClick={increaseCount}>Increase count</button>
40
+ }
41
+ }
42
+ ```
43
+
44
+ Alternatively, we could have defined our atom inside the component, making its
45
+ state local to that component instance:
46
+
47
+ ```tsx
48
+ import {useAtom} form '@theatre/react'
49
+
50
+ function Component() {
51
+ const atom = useAtom({count: 0, ready: false})
52
+ const ready = useVal(atom.pointer.ready)
53
+
54
+ // ...
55
+ }
56
+ ```
57
+
58
+ ## Quick tour
59
+
60
+ There 4 main concepts in dataverse:
61
+
62
+ - [Atoms](#atoms), hold the state of your application.
63
+ - [Pointers](#pointers) are a type-safe way to refer to specific properties of
64
+ atoms.
65
+ - [Prisms](#prisms) are functions that derive a value from an atom or from
66
+ another prism.
67
+ - [Tickers](#tickers) are a way to schedule and synchronise computations.
68
+
69
+ ### Atoms
70
+
71
+ Atoms are state holders. They can be used to manage either component state or
72
+ the global state of your application.
73
+
74
+ ```ts
75
+ import {Atom} from '@theatre/dataverse'
76
+
77
+ const atom = new Atom({intensity: 1, position: {x: 0, y: 0}})
78
+ ```
79
+
80
+ #### Changing the state of an atom
81
+
82
+ ```ts
83
+ // replace the whole stae
84
+ atom.set({intensity: 1, position: {x: 0, y: 0}})
85
+
86
+ // or using an update function
87
+ atom.reduce((state) => ({...state, intensity: state.intensity + 1}))
88
+
89
+ // or much easier, using a pointer
90
+ atom.setByPointer(atom.pointer.intensity, 3)
91
+
92
+ atom.reduceByPointer(atom.pointer.intensity, (intensity) => intensity + 1)
93
+ ```
94
+
95
+ #### Reading the state of an atom _None-reactively_
96
+
97
+ ```ts
98
+ // get the whole state
99
+ atom.get() // {intensity: 4, position: {x: 0, y: 0}}
100
+
101
+ // or get a specific property using a pointer
102
+ atom.getByPointer(atom.pointer.intensity) // 4
103
+ ```
104
+
105
+ #### Reading the state of an atom _reactively, in React_
106
+
107
+ ```ts
108
+ import {useVal} from '@theatre/react'
109
+
110
+ function Component() {
111
+ const intensity = useVal(atom.pointer.intensity) // 4
112
+ // ...
113
+ }
114
+ ```
115
+
116
+ Atoms can also be subscribed to outside of React. We'll cover that in a bit when
117
+ we talk about [prisms](#prisms).
118
+
119
+ ### Pointers
120
+
121
+ Pointers are a type-safe way to refer to specific properties of atoms.
122
+
123
+ ```ts
124
+ import {Atom} from '@theatre/dataverse'
125
+
126
+ const atom = new Atom({intensity: 1, position: {x: 0, y: 0}})
127
+
128
+ atom.setByPointer(atom.pointer.intensity, 3) // will set the intensity to 3
129
+
130
+ // referring to a non-existing property is a typescript error, but it'll work at runtime
131
+ atom.setByPointer(atom.pointer.nonExistingProperty, 3)
132
+
133
+ atom.get() // {intensity: 3, position: {x: 0, y: 0}, nonExistingProperty: 3}
134
+ ```
135
+
136
+ Pointers are referrentially stable
137
+
138
+ ```ts
139
+ assert.equal(atom.pointer.intensity, atom.pointer.intensity)
140
+ ```
141
+
142
+ #### Pointers and React
143
+
144
+ Pointers are a great way to pass data down the component tree while keeping
145
+ re-renders only to the components that actually need to re-render.
146
+
147
+ ```tsx
148
+ import {useVal, useAtom} from '@theatre/react'
149
+ import type {Pointer} from '@theatre/dataverse'
150
+
151
+ function ParentComponent() {
152
+ const atom = useAtom({
153
+ light: {intensity: 1, position: {x: 0, y: 0}},
154
+ ready: true,
155
+ })
156
+
157
+ const ready = useVal(atom.pointer.ready)
158
+
159
+ if (!ready) return <div>loading...</div>
160
+
161
+ return (
162
+ <>
163
+ {/* <Group> will only re-render when the position of the light changes */}
164
+ <Group positionP={atom.pointer.light.position}>
165
+ {/* <Light> will only re-render when the intensity of the light changes */}
166
+ <Light intensityP={atom.pointer.intensity} />
167
+ </Group>
168
+ </>
169
+ )
170
+ }
171
+
172
+ function Group({positionP, children}) {
173
+ const {x, y} = useVal(positionP)
174
+ return <div style={{position: `${x}px ${y}px`}}>{children}</div>
175
+ }
176
+
177
+ function Light({intensityP}) {
178
+ const intensity = useVal(intensityP)
179
+ return <div style={{opacity: intensity}} className="light" />
180
+ }
181
+ ```
182
+
183
+ ### Prisms
184
+
185
+ Prisms are functions that derive a value from an atom or from another prism.
186
+
187
+ ```ts
188
+ import {Atom, prism, val} from '@theatre/dataverse'
189
+
190
+ const atom = new Atom({a: 1, b: 2, foo: 10})
191
+
192
+ // the value of this prism will always be equal to the sum of `a` and `b`
193
+ const sum = prism(() => {
194
+ const a = val(atom.pointer.a)
195
+ const b = val(atom.pointer.b)
196
+ return a + b
197
+ })
198
+ ```
199
+
200
+ Prisms can also refer to other prisms.
201
+
202
+ ```ts
203
+ const double = prism(() => {
204
+ return 2 * val(sum)
205
+ })
206
+
207
+ console.log(val(double)) // 6
208
+ ```
209
+
210
+ #### Reading the value of a prism, _None-reactively_
211
+
212
+ ```ts
213
+ console.log(val(prism)) // 3
214
+
215
+ atom.setByPointer(atom.pointer.a, 2)
216
+ console.log(val(prism)) // 4
217
+ ```
218
+
219
+ #### Reading the value of a prism, _reactively, in React_
220
+
221
+ Just like atoms, prisms can be subscribed to via `useVal()`
222
+
223
+ ```tsx
224
+ function Component() {
225
+ return (
226
+ <div>
227
+ {useVal(atom.pointer.a)} + {useVal(atom.pointer.b)} = {useVal(prism)}
228
+ </div>
229
+ )
230
+ }
231
+ ```
232
+
233
+ #### Reading the value of a prism, _reactively, outside of React_
234
+
235
+ Prisms can also be subscribed to, outside of React's renderloop. This requires
236
+ the use of a Ticker, which we'll cover in the next section.
237
+
238
+ ### Tickers
239
+
240
+ Tickers are a way to schedule and synchronise computations. They're useful when
241
+ reacting to changes in atoms or prisms _outside of React's renderloop_.
242
+
243
+ ```ts
244
+ import {Ticker, onChange} from '@theatre/dataverse'
245
+
246
+ const ticker = new Ticker()
247
+
248
+ // advance the ticker roughly 60 times per second (note that it's better to use requestAnimationFrame)
249
+ setInterval(ticker.tick, 1000 / 60)
250
+
251
+ onChange(atom.pointer.intensity, (newIntensity) => {
252
+ console.log('intensity changed to', newIntensity)
253
+ })
254
+
255
+ atom.setByPointer(atom.pointer.intensity, 3)
256
+
257
+ // After a few milliseconds, logs 'intensity changed to 3'
258
+
259
+ setTimeout(() => {
260
+ atom.setByPointer(atom.pointer.intensity, 4)
261
+ atom.setByPointer(atom.pointer.intensity, 5)
262
+ // updates are batched because our ticker advances every 16ms, so we
263
+ // will only get one log for 'intensity changed to 5', even though we changed the intensity twice
264
+ }, 1000)
265
+ ```
266
+
267
+ Tickers should normally be advanced using `requestAnimationFrame` to make sure
268
+ all the computations are done in sync with the browser's refresh rate.
269
+
270
+ ```ts
271
+ const frame = () => {
272
+ ticker.tick()
273
+ requestAnimationFrame(frame)
274
+ }
275
+
276
+ requestAnimationFrame(frame)
277
+ ```
278
+
279
+ #### Benefits of using Tickers
280
+
281
+ Tickers make sure that our computations are batched and only advance atomically.
282
+ They also make sure that we don't recompute the same value twice in the same
283
+ frame.
284
+
285
+ Most importantly, Tickers allow us to align our computations to the browser's
286
+ (or the XR-device's) refresh rate.
287
+
288
+ ### Prism hooks
289
+
290
+ Prism hooks are inspired by
291
+ [React hooks](https://reactjs.org/docs/hooks-intro.html). They are a convenient
292
+ way to cache, memoize, batch, and run effects inside prisms, while ensuring that
293
+ the prism can be used in a declarative, encapsulated way.
294
+
295
+ #### `prism.source()`
296
+
297
+ The `prism.source()` hook allows a prism to read to and react to changes in
298
+ values that reside outside of an atom or another prism, for example, the value
299
+ of an `<input type="text" />` element.
300
+
301
+ ```ts
302
+ function prismFromInputElement(input: HTMLInputElement): Prism<string> {
303
+ function subscribe(cb: (value: string) => void) {
304
+ const listener = () => {
305
+ cb(input.value)
306
+ }
307
+ input.addEventListener('input', listener)
308
+ return () => {
309
+ input.removeEventListener('input', listener)
310
+ }
311
+ }
312
+
313
+ function get() {
314
+ return input.value
315
+ }
316
+ return prism(() => prism.source(subscribe, get))
317
+ }
318
+
319
+ const p = prismFromInputElement(document.querySelector('input'))
320
+
321
+ p.onChange(ticker, (value) => {
322
+ console.log('input value changed to', value)
323
+ })
324
+ ```
325
+
326
+ #### `prism.ref()`
327
+
328
+ Just like React's `useRef()`, `prism.ref()` allows us to create a prism that
329
+ holds a reference to some value. The only difference is that `prism.ref()`
330
+ requires a key to be passed into it, whlie `useRef()` doesn't. This means that
331
+ we can call `prism.ref()` in any order, and we can call it multiple times with
332
+ the same key.
333
+
334
+ ```ts
335
+ const p = prism(() => {
336
+ const inputRef = prism.ref('some-unique-key')
337
+ if (!inputRef.current) {
338
+ inputRef.current = document.$('input.username')
339
+ }
340
+
341
+ // this prism will always reflect the value of <input class="username">
342
+ return val(prismFromInputElement(inputRef.current))
343
+ })
344
+
345
+ p.onChange(ticker, (value) => {
346
+ console.log('username changed to', value)
347
+ })
348
+ ```
349
+
350
+ #### `prism.memo()`
351
+
352
+ `prism.memo()` works just like React's `useMemo()` hook. It's a way to cache the
353
+ result of a function call. The only difference is that `prism.memo()` requires a
354
+ key to be passed into it, whlie `useMemo()` doesn't. This means that we can call
355
+ `prism.memo()` in any order, and we can call it multiple times with the same
356
+ key.
357
+
358
+ ```ts
359
+ import {Atom, prism, val} from '@theatre/dataverse'
360
+
361
+ const atom = new Atom(0)
362
+
363
+ function factorial(n: number): number {
364
+ if (n === 0) return 1
365
+ return n * factorial(n - 1)
366
+ }
367
+
368
+ const p = prism(() => {
369
+ // num will be between 0 and 9. This is so we can test what happens when the atom's value changes, but
370
+ // the memoized value doesn't change.
371
+ const num = val(atom.pointer)
372
+ const numMod10 = num % 10
373
+ const value = prism.memo(
374
+ // we need a string key to identify the hook. This allows us to call `prism.memo()` in any order, or even conditionally.
375
+ 'factorial',
376
+ // the function to memoize
377
+ () => {
378
+ console.log('Calculating factorial')
379
+ factorial(numMod10)
380
+ },
381
+ // the dependencies of the function. If any of the dependencies change, the function will be called again.
382
+ [numMod10],
383
+ )
384
+
385
+ return `number is ${num}, num % 10 is ${numMod10} and its factorial is ${value}`
386
+ })
387
+
388
+ p.onChange(ticker, (value) => {
389
+ console.log('=>', value)
390
+ })
391
+
392
+ atom.set(1)
393
+ // Calculating factorial
394
+ // => number is 1, num % 10 is 1 and its factorial is 1
395
+
396
+ atom.set(2)
397
+ // Calculating factorial
398
+ // => number is 2, num % 10 is 2 and its factorial is 2
399
+
400
+ atom.set(12) // won't recalculate the factorial
401
+ // => number is 12, num % 10 is 2 and its factorial is 2
402
+ ```
403
+
404
+ #### `prism.effect()` and `prism.state()`
405
+
406
+ These are two more hooks that are similar to React's `useEffect()` and
407
+ `useState()` hooks.
408
+
409
+ `prism.effect()` is similar to React's `useEffect()` hook. It allows us to run
410
+ side-effects when the prism is calculated. Note that prisms are supposed to be
411
+ "virtually" pure functions. That means they either should not have side-effects
412
+ (and thus, no calls for `prism.effect()`), or their side-effects should clean
413
+ themselves up when the prism goes cold.
414
+
415
+ `prism.state()` is similar to React's `useState()` hook. It allows us to create
416
+ a stateful value that is scoped to the prism.
417
+
418
+ We'll defer to React's documentation for
419
+ [a more detailed explanation of how `useEffect()`](https://reactjs.org/docs/hooks-effect.html)
420
+ and how [`useState()`](https://reactjs.org/docs/hooks-state.html) work. But
421
+ here's a quick example:
422
+
423
+ ```tsx
424
+ import {prism} from '@theatre/dataverse'
425
+ import {useVal} from '@theatre/react'
426
+
427
+ // This prism holds the current mouse position and updates when the mouse moves
428
+ const mousePositionPr = prism(() => {
429
+ const [pos, setPos] = prism.state<[x: number, y: number]>('pos', [0, 0])
430
+
431
+ prism.effect(
432
+ 'setupListeners',
433
+ () => {
434
+ const handleMouseMove = (e: MouseEvent) => {
435
+ setPos([e.screenX, e.screenY])
436
+ }
437
+ document.addEventListener('mousemove', handleMouseMove)
438
+
439
+ return () => {
440
+ document.removeEventListener('mousemove', handleMouseMove)
441
+ }
442
+ },
443
+ [],
444
+ )
445
+
446
+ return pos
447
+ })
448
+
449
+ function Component() {
450
+ const [x, y] = useVal(mousePositionPr)
451
+ return (
452
+ <div>
453
+ Mouse position: {x}, {y}
454
+ </div>
455
+ )
456
+ }
457
+ ```
458
+
459
+ #### `prism.sub()`
460
+
461
+ `prism.sub()` is a shortcut for creating a prism inside another prism. It's
462
+ equivalent to calling `prism.memo(key, () => prism(fn), deps).getValue()`.
463
+ `prism.sub()` is useful when you want to divide your prism into smaller prisms,
464
+ each of which would _only_ recalculate when _certain_ dependencies change. In
465
+ other words, it's an optimization tool.
466
+
467
+ ```ts
468
+ function factorial(num: number): number {
469
+ if (num === 0) return 1
470
+ return num * factorial(num - 1)
471
+ }
472
+
473
+ const events: Array<'foo-calculated' | 'bar-calculated'> = []
474
+
475
+ // example:
476
+ const state = new Atom({foo: 0, bar: 0})
477
+ const pr = prism(() => {
478
+ const resultOfFoo = prism.sub(
479
+ 'foo',
480
+ () => {
481
+ events.push('foo-calculated')
482
+ const foo = val(state.pointer.foo) % 10
483
+ // Note how `prism.sub()` is more powerful than `prism.memo()` because it allows us to use `prism.memo()` and other hooks inside of it:
484
+ return prism.memo('factorial', () => factorial(foo), [foo])
485
+ },
486
+ [],
487
+ )
488
+ const resultOfBar = prism.sub(
489
+ 'bar',
490
+ () => {
491
+ events.push('bar-calculated')
492
+ const bar = val(state.pointer.bar) % 10
493
+
494
+ return prism.memo('factorial', () => factorial(bar), [bar])
495
+ },
496
+ [],
497
+ )
498
+
499
+ return `result of foo is ${resultOfFoo}, result of bar is ${resultOfBar}`
500
+ })
501
+
502
+ const unsub = pr.onChange(ticker, () => {})
503
+ // on the first run, both subs should be calculated:
504
+ console.log(events) // ['foo-calculated', 'bar-calculated']
505
+ events.length = 0 // clear the events array
506
+
507
+ // now if we change the value of `bar`, only `bar` should be recalculated:
508
+ state.setByPointer(state.pointer.bar, 2)
509
+ pr.getValue()
510
+ console.log(events) // ['bar-calculated']
511
+
512
+ unsub()
513
+ ```
514
+
515
+ since prism hooks are keyed (as opposed to React hooks where they're identified
516
+ by their order), it's possible to have multiple hooks with the same key in the
517
+ same prism. To avoid this, we can use `prism.scope()` to create a "scope" for
518
+ our hooks. Example:
519
+
520
+ ```ts
521
+ const pr = prism(() => {
522
+ prism.scope('a', () => {
523
+ prism.memo('foo', () => 1, [])
524
+ })
525
+
526
+ prism.scope('b', () => {
527
+ prism.memo('foo', () => 1, [])
528
+ })
529
+ })
530
+ ```
531
+
532
+ ### `usePrism()`
533
+
534
+ `usePrism()` is a _React_ hook that allows us to create a prism inside a React
535
+ component. This way, we can optimize our React components in a fine-grained way
536
+ by moving their computations outside of React's render loop.
537
+
538
+ ```tsx
539
+ import {usePrism} from '@theatre/react'
540
+
541
+ function Component() {
542
+ const value = usePrism(() => {
543
+ // [insert heavy calculation here]
544
+ }, [])
545
+ }
546
+ ```
547
+
548
+ ### Hot and cold prisms
549
+
550
+ Prisms can have three states:
551
+
552
+ - 🧊 Cold: The prism was just created. It does not have dependents, or its
553
+ dependents are also 🧊 cold.
554
+ - 🔥 Hot: The prism is either being subscribed to (via `useVal()`,
555
+ `prism.onChange()`, `prism.onStale()`, etc). Or, one of its dependents is 🔥
556
+ hot.
557
+ - A 🔥 Hot prism itself has two states:
558
+ - 🪵 Stale: The prism is hot, but its value is stale. This happens when one
559
+ or more of its dependencies have changed, but the value of the prism
560
+ hasn't been read since that change. Reading the value of a 🪵 Stale prism
561
+ will cause it to recalculate, and make it 🌲 Fresh.
562
+ - 🌲 Fresh: The prism is hot, and its value is fresh. This happens when the
563
+ prism's value has been read since the last change in its dependencies.
564
+ Re-reading the value of a 🌲 Fresh prism will _not_ cause it to
565
+ recalculate.
566
+
567
+ Or, as a typescript annotation:
568
+
569
+ ```ts
570
+ type PrismState =
571
+ | {isHot: false} // 🧊
572
+ | {isHot: true; isFresh: false} // 🔥🪵
573
+ | {isHot: true; isFresh: true} // 🔥🌲
574
+ ```
575
+
576
+ Let's demonstrate this with an example of a prism, and its `onStale()` method.
577
+
578
+ ```ts
579
+ const atom = new Atom(0)
580
+ const a = prism(() => val(atom.pointer)) // 🧊
581
+
582
+ // onStale(cb) calls `cb` when the prism goes from 🌲 to 🪵
583
+ a.onStale(() => {
584
+ console.log('a is stale')
585
+ })
586
+ // a from 🧊 to 🔥
587
+ // console: a is stale
588
+
589
+ // reading the value of `a` will cause it to recalculate, and make it 🌲 fresh.
590
+ console.log(val(a)) // 1
591
+ // a from 🔥🪵 to 🔥🌲
592
+
593
+ atom.set(1)
594
+ // a from 🔥🌲 to 🔥🪵
595
+ // console: a is stale
596
+
597
+ // reading the value of `a` will cause it to recalculate, and make it 🌲 fresh.
598
+ console.log(val(a)) // 2
599
+ ```
600
+
601
+ Prism states propogate through the prism dependency graph. Let's look at an
602
+ example:
603
+
604
+ ```ts
605
+ const atom = new Atom({a: 0, b: 0})
606
+ const a = prism(() => val(atom.pointer.a))
607
+ const b = prism(() => val(atom.pointer.b))
608
+ const sum = prism(() => val(a) + val(b))
609
+
610
+ // a | b | sum |
611
+ // 🧊 | 🧊 | 🧊 |
612
+
613
+ let unsub = a.onStale(() => {})
614
+
615
+ // there is now a subscription to `a`, so it's 🔥 hot
616
+ // a | b | sum |
617
+ // 🔥🪵 | 🧊 | 🧊 |
618
+
619
+ unsub()
620
+ // there are no subscriptions to `a`, so it's 🧊 cold again
621
+ // a | b | sum |
622
+ // 🧊 | 🧊 | 🧊 |
623
+
624
+ unsub = sum.onStale(() => {})
625
+ // there is now a subscription to `sum`, so it goes 🔥 hot, and so do its dependencies
626
+ // a | b | sum |
627
+ // 🔥🪵 | 🔥🪵 | 🔥🪵 |
628
+
629
+ val(sum)
630
+ // reading the value of `sum` will cause it to recalculate, and make it 🌲 fresh.
631
+ // a | b | sum |
632
+ // 🔥🌲 | 🔥🌲 | 🔥🌲 |
633
+
634
+ atom.setByPointer(atom.pointer.a, 1)
635
+ // `a` is now stale, which will cause `sum` to become stale as well
636
+ // a | b | sum |
637
+ // 🔥🪵 | 🔥🌲 | 🔥🪵 |
638
+
639
+ val(a)
640
+ // reading the value of `a` will cause it to recalculate, and make it 🌲 fresh. But notice that `sum` is still 🪵 stale.
641
+ // a | b | sum |
642
+ // 🔥🌲 | 🔥🌲 | 🔥🪵 |
643
+
644
+ atom.setByPointer(atom.pointer.b, 1)
645
+ // `b` now goes stale. Since sum was already stale, it will remain so
646
+ // a | b | sum |
647
+ // 🔥🌲 | 🔥🪵 | 🔥🪵 |
648
+
649
+ val(sum)
650
+ // reading the value of `sum` will cause it to recalculate and go 🌲 fresh.
651
+ // a | b | sum |
652
+ // 🔥🌲 | 🔥🌲 | 🔥🌲 |
653
+
654
+ unsub()
655
+ // there are no subscriptions to `sum`, so it goes 🧊 cold again, and so do its dependencies, since they don't have any other hot dependents
656
+ // a | b | sum |
657
+ // 🧊 | 🧊 | 🧊 |
658
+ ```
659
+
660
+ The state transitions propogate in topological order. Let's demonstrate this by
661
+ adding one more prism to our dependency graph:
662
+
663
+ ```ts
664
+ // continued from the previous example
665
+
666
+ const double = prism(() => val(sum) * 2)
667
+
668
+ // Initially, all prisms are 🧊 cold
669
+ // a | b | sum | double |
670
+ // 🧊 | 🧊 | 🧊 | 🧊 |
671
+
672
+ let unsub = double.onStale(() => {})
673
+ // here is how the state transitions will happen, step by step:
674
+ // (step) | a | b | sum | double |
675
+ // 1 | 🧊 | 🧊 | 🧊 | 🔥🪵 |
676
+ // 2 | 🧊 | 🧊 | 🔥🪵 | 🔥🪵 |
677
+ // 3 | 🔥🪵 | 🔥🪵 | 🔥🪵 | 🔥🪵 |
678
+
679
+ val(double)
680
+ // freshening happens in the reverse order
681
+ // (step) | a | b | sum | double |
682
+ // 0 | 🔥🪵 | 🔥🪵 | 🔥🪵 | 🔥🪵 |
683
+ // --------------------------------------------------|
684
+ // 1 â–² â–¼ | double reads the value of sum
685
+ // └────◄────┘ |
686
+ // --------------------------------------------------|
687
+ // 2 â–² â–² â–¼ | sum reads the value of a and b
688
+ // │ │ │ |
689
+ // └────◄───┴────◄─────┘ |
690
+ // --------------------------------------------------|
691
+ // 3 | 🔥🌲 | 🔥🌲 | 🔥🪵 | 🔥🪵 | a and b go fresh
692
+ // --------------------------------------------------|
693
+ // 4 | 🔥🌲 | 🔥🌲 | 🔥🌲 | 🔥🪵 | sum goes fresh
694
+ // --------------------------------------------------|
695
+ // 5 | 🔥🌲 | 🔥🌲 | 🔥🌲 | 🔥🌲 | double goes fresh
696
+ // --------------------------------------------------|
697
+ ```
698
+
699
+ ## Links
700
+
701
+ - [API Reference](./api/README.md)
702
+ - [The exhaustive guide to dataverse](./src/dataverse.test.ts)
703
+ - It's also fun to
704
+ [open the monorepo](https://github1s.com/theatre-js/theatre/blob/main/packages/dataverse/src/index.ts)
705
+ in VSCode and look up references to `Atom`, `prism()` and other dataverse
706
+ methods. Since dataverse is used internally in Theatre.js, there are a lot of
707
+ examples of how to use it.
708
+ - Also see [`@theatre/react`](../react/README.md) to learn more about the React
709
+ bindings.