source: node_modules/reselect/src/autotrackMemoize/autotracking.ts

main
Last change on this file was d24f17c, checked in by Aleksandar Panovski <apano77@…>, 15 months ago

Initial commit

  • Property mode set to 100644
File size: 4.8 KB
Line 
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/
6import type { EqualityFn } from '../types'
7import { assertIsFunction } from '../utils'
8
9// The global revision clock. Every time state changes, the clock increments.
10export 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.
15let CURRENT_TRACKER: Set<Cell<any> | TrackingCache> | null = null
16
17// Storage represents a root value in the system - the actual state of our app.
18export 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
51function 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.
59export 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
122export 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
130type CellValue<T extends Cell<unknown>> = T extends Cell<infer U> ? U : never
131
132export 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
145export function createCell<T = unknown>(
146 initialValue: T,
147 isEqual: EqualityFn = tripleEq
148): Cell<T> {
149 return new Cell(initialValue, isEqual)
150}
151
152export 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}
Note: See TracBrowser for help on using the repository browser.