[d24f17c] | 1 | // Original autotracking implementation source:
|
---|
| 2 | // - https://gist.github.com/pzuraq/79bf862e0f8cd9521b79c4b6eccdc4f9
|
---|
| 3 | // Additional references:
|
---|
| 4 | // - https://www.pzuraq.com/blog/how-autotracking-works
|
---|
| 5 | // - https://v5.chriskrycho.com/journal/autotracking-elegant-dx-via-cutting-edge-cs/
|
---|
| 6 | import type { EqualityFn } from '../types'
|
---|
| 7 | import { assertIsFunction } from '../utils'
|
---|
| 8 |
|
---|
| 9 | // The global revision clock. Every time state changes, the clock increments.
|
---|
| 10 | export let $REVISION = 0
|
---|
| 11 |
|
---|
| 12 | // The current dependency tracker. Whenever we compute a cache, we create a Set
|
---|
| 13 | // to track any dependencies that are used while computing. If no cache is
|
---|
| 14 | // computing, then the tracker is null.
|
---|
| 15 | let CURRENT_TRACKER: Set<Cell<any> | TrackingCache> | null = null
|
---|
| 16 |
|
---|
| 17 | // Storage represents a root value in the system - the actual state of our app.
|
---|
| 18 | export class Cell<T> {
|
---|
| 19 | revision = $REVISION
|
---|
| 20 |
|
---|
| 21 | _value: T
|
---|
| 22 | _lastValue: T
|
---|
| 23 | _isEqual: EqualityFn = tripleEq
|
---|
| 24 |
|
---|
| 25 | constructor(initialValue: T, isEqual: EqualityFn = tripleEq) {
|
---|
| 26 | this._value = this._lastValue = initialValue
|
---|
| 27 | this._isEqual = isEqual
|
---|
| 28 | }
|
---|
| 29 |
|
---|
| 30 | // Whenever a storage value is read, it'll add itself to the current tracker if
|
---|
| 31 | // one exists, entangling its state with that cache.
|
---|
| 32 | get value() {
|
---|
| 33 | CURRENT_TRACKER?.add(this)
|
---|
| 34 |
|
---|
| 35 | return this._value
|
---|
| 36 | }
|
---|
| 37 |
|
---|
| 38 | // Whenever a storage value is updated, we bump the global revision clock,
|
---|
| 39 | // assign the revision for this storage to the new value, _and_ we schedule a
|
---|
| 40 | // rerender. This is important, and it's what makes autotracking _pull_
|
---|
| 41 | // based. We don't actively tell the caches which depend on the storage that
|
---|
| 42 | // anything has happened. Instead, we recompute the caches when needed.
|
---|
| 43 | set value(newValue) {
|
---|
| 44 | if (this.value === newValue) return
|
---|
| 45 |
|
---|
| 46 | this._value = newValue
|
---|
| 47 | this.revision = ++$REVISION
|
---|
| 48 | }
|
---|
| 49 | }
|
---|
| 50 |
|
---|
| 51 | function tripleEq(a: unknown, b: unknown) {
|
---|
| 52 | return a === b
|
---|
| 53 | }
|
---|
| 54 |
|
---|
| 55 | // Caches represent derived state in the system. They are ultimately functions
|
---|
| 56 | // that are memoized based on what state they use to produce their output,
|
---|
| 57 | // meaning they will only rerun IFF a storage value that could affect the output
|
---|
| 58 | // has changed. Otherwise, they'll return the cached value.
|
---|
| 59 | export class TrackingCache {
|
---|
| 60 | _cachedValue: any
|
---|
| 61 | _cachedRevision = -1
|
---|
| 62 | _deps: any[] = []
|
---|
| 63 | hits = 0
|
---|
| 64 |
|
---|
| 65 | fn: () => any
|
---|
| 66 |
|
---|
| 67 | constructor(fn: () => any) {
|
---|
| 68 | this.fn = fn
|
---|
| 69 | }
|
---|
| 70 |
|
---|
| 71 | clear() {
|
---|
| 72 | this._cachedValue = undefined
|
---|
| 73 | this._cachedRevision = -1
|
---|
| 74 | this._deps = []
|
---|
| 75 | this.hits = 0
|
---|
| 76 | }
|
---|
| 77 |
|
---|
| 78 | get value() {
|
---|
| 79 | // When getting the value for a Cache, first we check all the dependencies of
|
---|
| 80 | // the cache to see what their current revision is. If the current revision is
|
---|
| 81 | // greater than the cached revision, then something has changed.
|
---|
| 82 | if (this.revision > this._cachedRevision) {
|
---|
| 83 | const { fn } = this
|
---|
| 84 |
|
---|
| 85 | // We create a new dependency tracker for this cache. As the cache runs
|
---|
| 86 | // its function, any Storage or Cache instances which are used while
|
---|
| 87 | // computing will be added to this tracker. In the end, it will be the
|
---|
| 88 | // full list of dependencies that this Cache depends on.
|
---|
| 89 | const currentTracker = new Set<Cell<any>>()
|
---|
| 90 | const prevTracker = CURRENT_TRACKER
|
---|
| 91 |
|
---|
| 92 | CURRENT_TRACKER = currentTracker
|
---|
| 93 |
|
---|
| 94 | // try {
|
---|
| 95 | this._cachedValue = fn()
|
---|
| 96 | // } finally {
|
---|
| 97 | CURRENT_TRACKER = prevTracker
|
---|
| 98 | this.hits++
|
---|
| 99 | this._deps = Array.from(currentTracker)
|
---|
| 100 |
|
---|
| 101 | // Set the cached revision. This is the current clock count of all the
|
---|
| 102 | // dependencies. If any dependency changes, this number will be less
|
---|
| 103 | // than the new revision.
|
---|
| 104 | this._cachedRevision = this.revision
|
---|
| 105 | // }
|
---|
| 106 | }
|
---|
| 107 |
|
---|
| 108 | // If there is a current tracker, it means another Cache is computing and
|
---|
| 109 | // using this one, so we add this one to the tracker.
|
---|
| 110 | CURRENT_TRACKER?.add(this)
|
---|
| 111 |
|
---|
| 112 | // Always return the cached value.
|
---|
| 113 | return this._cachedValue
|
---|
| 114 | }
|
---|
| 115 |
|
---|
| 116 | get revision() {
|
---|
| 117 | // The current revision is the max of all the dependencies' revisions.
|
---|
| 118 | return Math.max(...this._deps.map(d => d.revision), 0)
|
---|
| 119 | }
|
---|
| 120 | }
|
---|
| 121 |
|
---|
| 122 | export function getValue<T>(cell: Cell<T>): T {
|
---|
| 123 | if (!(cell instanceof Cell)) {
|
---|
| 124 | console.warn('Not a valid cell! ', cell)
|
---|
| 125 | }
|
---|
| 126 |
|
---|
| 127 | return cell.value
|
---|
| 128 | }
|
---|
| 129 |
|
---|
| 130 | type CellValue<T extends Cell<unknown>> = T extends Cell<infer U> ? U : never
|
---|
| 131 |
|
---|
| 132 | export function setValue<T extends Cell<unknown>>(
|
---|
| 133 | storage: T,
|
---|
| 134 | value: CellValue<T>
|
---|
| 135 | ): void {
|
---|
| 136 | if (!(storage instanceof Cell)) {
|
---|
| 137 | throw new TypeError(
|
---|
| 138 | 'setValue must be passed a tracked store created with `createStorage`.'
|
---|
| 139 | )
|
---|
| 140 | }
|
---|
| 141 |
|
---|
| 142 | storage.value = storage._lastValue = value
|
---|
| 143 | }
|
---|
| 144 |
|
---|
| 145 | export function createCell<T = unknown>(
|
---|
| 146 | initialValue: T,
|
---|
| 147 | isEqual: EqualityFn = tripleEq
|
---|
| 148 | ): Cell<T> {
|
---|
| 149 | return new Cell(initialValue, isEqual)
|
---|
| 150 | }
|
---|
| 151 |
|
---|
| 152 | export function createCache<T = unknown>(fn: () => T): TrackingCache {
|
---|
| 153 | assertIsFunction(
|
---|
| 154 | fn,
|
---|
| 155 | 'the first parameter to `createCache` must be a function'
|
---|
| 156 | )
|
---|
| 157 |
|
---|
| 158 | return new TrackingCache(fn)
|
---|
| 159 | }
|
---|