@tanstack/store 0.5.5 → 0.7.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.
package/src/derived.ts ADDED
@@ -0,0 +1,198 @@
1
+ import { Store } from './store'
2
+ import { __derivedToStore, __storeToDerived } from './scheduler'
3
+ import type { Listener } from './types'
4
+
5
+ export type UnwrapDerivedOrStore<T> =
6
+ T extends Derived<infer InnerD>
7
+ ? InnerD
8
+ : T extends Store<infer InnerS>
9
+ ? InnerS
10
+ : never
11
+
12
+ type UnwrapReadonlyDerivedOrStoreArray<
13
+ TArr extends ReadonlyArray<Derived<any> | Store<any>>,
14
+ > = TArr extends readonly [infer Head, ...infer Tail]
15
+ ? Head extends Derived<any> | Store<any>
16
+ ? Tail extends ReadonlyArray<Derived<any> | Store<any>>
17
+ ? [UnwrapDerivedOrStore<Head>, ...UnwrapReadonlyDerivedOrStoreArray<Tail>]
18
+ : []
19
+ : []
20
+ : []
21
+
22
+ // Can't have currVal, as it's being evaluated from the current derived fn
23
+ export interface DerivedFnProps<
24
+ TArr extends ReadonlyArray<Derived<any> | Store<any>> = ReadonlyArray<any>,
25
+ TUnwrappedArr extends
26
+ UnwrapReadonlyDerivedOrStoreArray<TArr> = UnwrapReadonlyDerivedOrStoreArray<TArr>,
27
+ > {
28
+ // `undefined` if it's the first run
29
+ /**
30
+ * `undefined` if it's the first run
31
+ * @privateRemarks this also cannot be typed as TState, as it breaks the inferencing of the function's return type when an argument is used - even with `NoInfer` usage
32
+ */
33
+ prevVal: unknown | undefined
34
+ prevDepVals: TUnwrappedArr | undefined
35
+ currDepVals: TUnwrappedArr
36
+ }
37
+
38
+ export interface DerivedOptions<
39
+ TState,
40
+ TArr extends ReadonlyArray<Derived<any> | Store<any>> = ReadonlyArray<any>,
41
+ > {
42
+ onSubscribe?: (
43
+ listener: Listener<TState>,
44
+ derived: Derived<TState>,
45
+ ) => () => void
46
+ onUpdate?: () => void
47
+ deps: TArr
48
+ /**
49
+ * Values of the `deps` from before and after the current invocation of `fn`
50
+ */
51
+ fn: (props: DerivedFnProps<TArr>) => TState
52
+ }
53
+
54
+ export class Derived<
55
+ TState,
56
+ const TArr extends ReadonlyArray<
57
+ Derived<any> | Store<any>
58
+ > = ReadonlyArray<any>,
59
+ > {
60
+ listeners = new Set<Listener<TState>>()
61
+ state: TState
62
+ prevState: TState | undefined
63
+ options: DerivedOptions<TState, TArr>
64
+
65
+ /**
66
+ * Functions representing the subscriptions. Call a function to cleanup
67
+ * @private
68
+ */
69
+ _subscriptions: Array<() => void> = []
70
+
71
+ lastSeenDepValues: Array<unknown> = []
72
+ getDepVals = () => {
73
+ const prevDepVals = [] as Array<unknown>
74
+ const currDepVals = [] as Array<unknown>
75
+ for (const dep of this.options.deps) {
76
+ prevDepVals.push(dep.prevState)
77
+ currDepVals.push(dep.state)
78
+ }
79
+ this.lastSeenDepValues = currDepVals
80
+ return {
81
+ prevDepVals,
82
+ currDepVals,
83
+ prevVal: this.prevState ?? undefined,
84
+ }
85
+ }
86
+
87
+ constructor(options: DerivedOptions<TState, TArr>) {
88
+ this.options = options
89
+ this.state = options.fn({
90
+ prevDepVals: undefined,
91
+ prevVal: undefined,
92
+ currDepVals: this.getDepVals().currDepVals as never,
93
+ })
94
+ }
95
+
96
+ registerOnGraph(
97
+ deps: ReadonlyArray<Derived<any> | Store<any>> = this.options.deps,
98
+ ) {
99
+ for (const dep of deps) {
100
+ if (dep instanceof Derived) {
101
+ // First register the intermediate derived value if it's not already registered
102
+ dep.registerOnGraph()
103
+ // Then register this derived with the dep's underlying stores
104
+ this.registerOnGraph(dep.options.deps)
105
+ } else if (dep instanceof Store) {
106
+ // Register the derived as related derived to the store
107
+ let relatedLinkedDerivedVals = __storeToDerived.get(dep)
108
+ if (!relatedLinkedDerivedVals) {
109
+ relatedLinkedDerivedVals = new Set()
110
+ __storeToDerived.set(dep, relatedLinkedDerivedVals)
111
+ }
112
+ relatedLinkedDerivedVals.add(this as never)
113
+
114
+ // Register the store as a related store to this derived
115
+ let relatedStores = __derivedToStore.get(this as never)
116
+ if (!relatedStores) {
117
+ relatedStores = new Set()
118
+ __derivedToStore.set(this as never, relatedStores)
119
+ }
120
+ relatedStores.add(dep)
121
+ }
122
+ }
123
+ }
124
+
125
+ unregisterFromGraph(
126
+ deps: ReadonlyArray<Derived<any> | Store<any>> = this.options.deps,
127
+ ) {
128
+ for (const dep of deps) {
129
+ if (dep instanceof Derived) {
130
+ this.unregisterFromGraph(dep.options.deps)
131
+ } else if (dep instanceof Store) {
132
+ const relatedLinkedDerivedVals = __storeToDerived.get(dep)
133
+ if (relatedLinkedDerivedVals) {
134
+ relatedLinkedDerivedVals.delete(this as never)
135
+ }
136
+
137
+ const relatedStores = __derivedToStore.get(this as never)
138
+ if (relatedStores) {
139
+ relatedStores.delete(dep)
140
+ }
141
+ }
142
+ }
143
+ }
144
+
145
+ recompute = () => {
146
+ this.prevState = this.state
147
+ const { prevDepVals, currDepVals, prevVal } = this.getDepVals()
148
+ this.state = this.options.fn({
149
+ prevDepVals: prevDepVals as never,
150
+ currDepVals: currDepVals as never,
151
+ prevVal,
152
+ })
153
+
154
+ this.options.onUpdate?.()
155
+ }
156
+
157
+ checkIfRecalculationNeededDeeply = () => {
158
+ for (const dep of this.options.deps) {
159
+ if (dep instanceof Derived) {
160
+ dep.checkIfRecalculationNeededDeeply()
161
+ }
162
+ }
163
+ let shouldRecompute = false
164
+ const lastSeenDepValues = this.lastSeenDepValues
165
+ const { currDepVals } = this.getDepVals()
166
+ for (let i = 0; i < currDepVals.length; i++) {
167
+ if (currDepVals[i] !== lastSeenDepValues[i]) {
168
+ shouldRecompute = true
169
+ break
170
+ }
171
+ }
172
+
173
+ if (shouldRecompute) {
174
+ this.recompute()
175
+ }
176
+ }
177
+
178
+ mount = () => {
179
+ this.registerOnGraph()
180
+ this.checkIfRecalculationNeededDeeply()
181
+
182
+ return () => {
183
+ this.unregisterFromGraph()
184
+ for (const cleanup of this._subscriptions) {
185
+ cleanup()
186
+ }
187
+ }
188
+ }
189
+
190
+ subscribe = (listener: Listener<TState>) => {
191
+ this.listeners.add(listener)
192
+ const unsub = this.options.onSubscribe?.(listener, this)
193
+ return () => {
194
+ this.listeners.delete(listener)
195
+ unsub?.()
196
+ }
197
+ }
198
+ }
package/src/effect.ts ADDED
@@ -0,0 +1,42 @@
1
+ import { Derived } from './derived'
2
+ import type { DerivedOptions } from './derived'
3
+
4
+ interface EffectOptions
5
+ extends Omit<
6
+ DerivedOptions<unknown>,
7
+ 'onUpdate' | 'onSubscribe' | 'lazy' | 'fn'
8
+ > {
9
+ /**
10
+ * Should the effect trigger immediately?
11
+ * @default false
12
+ */
13
+ eager?: boolean
14
+ fn: () => void
15
+ }
16
+
17
+ export class Effect {
18
+ /**
19
+ * @private
20
+ */
21
+ _derived: Derived<void>
22
+
23
+ constructor(opts: EffectOptions) {
24
+ const { eager, fn, ...derivedProps } = opts
25
+
26
+ this._derived = new Derived({
27
+ ...derivedProps,
28
+ fn: () => {},
29
+ onUpdate() {
30
+ fn()
31
+ },
32
+ })
33
+
34
+ if (eager) {
35
+ fn()
36
+ }
37
+ }
38
+
39
+ mount() {
40
+ return this._derived.mount()
41
+ }
42
+ }
package/src/index.ts CHANGED
@@ -1,70 +1,5 @@
1
- export type AnyUpdater = (...args: Array<any>) => any
2
-
3
- export type Listener = () => void
4
-
5
- export interface StoreOptions<
6
- TState,
7
- TUpdater extends AnyUpdater = (cb: TState) => TState,
8
- > {
9
- updateFn?: (previous: TState) => (updater: TUpdater) => TState
10
- onSubscribe?: (
11
- listener: Listener,
12
- store: Store<TState, TUpdater>,
13
- ) => () => void
14
- onUpdate?: () => void
15
- }
16
-
17
- export class Store<
18
- TState,
19
- TUpdater extends AnyUpdater = (cb: TState) => TState,
20
- > {
21
- listeners = new Set<Listener>()
22
- state: TState
23
- options?: StoreOptions<TState, TUpdater>
24
- _batching = false
25
- _flushing = 0
26
-
27
- constructor(initialState: TState, options?: StoreOptions<TState, TUpdater>) {
28
- this.state = initialState
29
- this.options = options
30
- }
31
-
32
- subscribe = (listener: Listener) => {
33
- this.listeners.add(listener)
34
- const unsub = this.options?.onSubscribe?.(listener, this)
35
- return () => {
36
- this.listeners.delete(listener)
37
- unsub?.()
38
- }
39
- }
40
-
41
- setState = (updater: TUpdater) => {
42
- const previous = this.state
43
- this.state = this.options?.updateFn
44
- ? this.options.updateFn(previous)(updater)
45
- : (updater as any)(previous)
46
-
47
- // Always run onUpdate, regardless of batching
48
- this.options?.onUpdate?.()
49
-
50
- // Attempt to flush
51
- this._flush()
52
- }
53
-
54
- _flush = () => {
55
- if (this._batching) return
56
- const flushId = ++this._flushing
57
- this.listeners.forEach((listener) => {
58
- if (this._flushing !== flushId) return
59
- listener()
60
- })
61
- }
62
-
63
- batch = (cb: () => void) => {
64
- if (this._batching) return cb()
65
- this._batching = true
66
- cb()
67
- this._batching = false
68
- this._flush()
69
- }
70
- }
1
+ export * from './derived'
2
+ export * from './effect'
3
+ export * from './store'
4
+ export * from './types'
5
+ export * from './scheduler'
@@ -0,0 +1,155 @@
1
+ import { Derived } from './derived'
2
+ import type { Store } from './store'
3
+
4
+ /**
5
+ * This is here to solve the pyramid dependency problem where:
6
+ * A
7
+ * / \
8
+ * B C
9
+ * \ /
10
+ * D
11
+ *
12
+ * Where we deeply traverse this tree, how do we avoid D being recomputed twice; once when B is updated, once when C is.
13
+ *
14
+ * To solve this, we create linkedDeps that allows us to sync avoid writes to the state until all of the deps have been
15
+ * resolved.
16
+ *
17
+ * This is a record of stores, because derived stores are not able to write values to, but stores are
18
+ */
19
+ export const __storeToDerived = new WeakMap<
20
+ Store<unknown>,
21
+ Set<Derived<unknown>>
22
+ >()
23
+ export const __derivedToStore = new WeakMap<
24
+ Derived<unknown>,
25
+ Set<Store<unknown>>
26
+ >()
27
+
28
+ export const __depsThatHaveWrittenThisTick = {
29
+ current: [] as Array<Derived<unknown> | Store<unknown>>,
30
+ }
31
+
32
+ let __isFlushing = false
33
+ let __batchDepth = 0
34
+ const __pendingUpdates = new Set<Store<unknown>>()
35
+ // Add a map to store initial values before batch
36
+ const __initialBatchValues = new Map<Store<unknown>, unknown>()
37
+
38
+ function __flush_internals(relatedVals: Set<Derived<unknown>>) {
39
+ // First sort deriveds by dependency order
40
+ const sorted = Array.from(relatedVals).sort((a, b) => {
41
+ // If a depends on b, b should go first
42
+ if (a instanceof Derived && a.options.deps.includes(b)) return 1
43
+ // If b depends on a, a should go first
44
+ if (b instanceof Derived && b.options.deps.includes(a)) return -1
45
+ return 0
46
+ })
47
+
48
+ for (const derived of sorted) {
49
+ if (__depsThatHaveWrittenThisTick.current.includes(derived)) {
50
+ continue
51
+ }
52
+
53
+ __depsThatHaveWrittenThisTick.current.push(derived)
54
+ derived.recompute()
55
+
56
+ const stores = __derivedToStore.get(derived)
57
+ if (stores) {
58
+ for (const store of stores) {
59
+ const relatedLinkedDerivedVals = __storeToDerived.get(store)
60
+ if (!relatedLinkedDerivedVals) continue
61
+ __flush_internals(relatedLinkedDerivedVals)
62
+ }
63
+ }
64
+ }
65
+ }
66
+
67
+ function __notifyListeners(store: Store<unknown>) {
68
+ store.listeners.forEach((listener) =>
69
+ listener({
70
+ prevVal: store.prevState as never,
71
+ currentVal: store.state as never,
72
+ }),
73
+ )
74
+ }
75
+
76
+ function __notifyDerivedListeners(derived: Derived<unknown>) {
77
+ derived.listeners.forEach((listener) =>
78
+ listener({
79
+ prevVal: derived.prevState as never,
80
+ currentVal: derived.state as never,
81
+ }),
82
+ )
83
+ }
84
+
85
+ /**
86
+ * @private only to be called from `Store` on write
87
+ */
88
+ export function __flush(store: Store<unknown>) {
89
+ // If we're starting a batch, store the initial values
90
+ if (__batchDepth > 0 && !__initialBatchValues.has(store)) {
91
+ __initialBatchValues.set(store, store.prevState)
92
+ }
93
+
94
+ __pendingUpdates.add(store)
95
+
96
+ if (__batchDepth > 0) return
97
+ if (__isFlushing) return
98
+
99
+ try {
100
+ __isFlushing = true
101
+
102
+ while (__pendingUpdates.size > 0) {
103
+ const stores = Array.from(__pendingUpdates)
104
+ __pendingUpdates.clear()
105
+
106
+ // First notify listeners with updated values
107
+ for (const store of stores) {
108
+ // Use initial batch values for prevState if we have them
109
+ const prevState = __initialBatchValues.get(store) ?? store.prevState
110
+ store.prevState = prevState
111
+ __notifyListeners(store)
112
+ }
113
+
114
+ // Then update all derived values
115
+ for (const store of stores) {
116
+ const derivedVals = __storeToDerived.get(store)
117
+ if (!derivedVals) continue
118
+
119
+ __depsThatHaveWrittenThisTick.current.push(store)
120
+ __flush_internals(derivedVals)
121
+ }
122
+
123
+ // Notify derived listeners after recomputing
124
+ for (const store of stores) {
125
+ const derivedVals = __storeToDerived.get(store)
126
+ if (!derivedVals) continue
127
+
128
+ for (const derived of derivedVals) {
129
+ __notifyDerivedListeners(derived)
130
+ }
131
+ }
132
+ }
133
+ } finally {
134
+ __isFlushing = false
135
+ __depsThatHaveWrittenThisTick.current = []
136
+ __initialBatchValues.clear()
137
+ }
138
+ }
139
+
140
+ export function batch(fn: () => void) {
141
+ __batchDepth++
142
+ try {
143
+ fn()
144
+ } finally {
145
+ __batchDepth--
146
+ if (__batchDepth === 0) {
147
+ const pendingUpdateToFlush = Array.from(__pendingUpdates)[0] as
148
+ | Store<unknown>
149
+ | undefined
150
+ if (pendingUpdateToFlush) {
151
+ __flush(pendingUpdateToFlush) // Trigger flush of all pending updates
152
+ }
153
+ }
154
+ }
155
+ }
package/src/store.ts ADDED
@@ -0,0 +1,63 @@
1
+ import { __flush } from './scheduler'
2
+ import type { AnyUpdater, Listener } from './types'
3
+
4
+ export interface StoreOptions<
5
+ TState,
6
+ TUpdater extends AnyUpdater = (cb: TState) => TState,
7
+ > {
8
+ /**
9
+ * Replace the default update function with a custom one.
10
+ */
11
+ updateFn?: (previous: TState) => (updater: TUpdater) => TState
12
+ /**
13
+ * Called when a listener subscribes to the store.
14
+ *
15
+ * @return a function to unsubscribe the listener
16
+ */
17
+ onSubscribe?: (
18
+ listener: Listener<TState>,
19
+ store: Store<TState, TUpdater>,
20
+ ) => () => void
21
+ /**
22
+ * Called after the state has been updated, used to derive other state.
23
+ */
24
+ onUpdate?: () => void
25
+ }
26
+
27
+ export class Store<
28
+ TState,
29
+ TUpdater extends AnyUpdater = (cb: TState) => TState,
30
+ > {
31
+ listeners = new Set<Listener<TState>>()
32
+ state: TState
33
+ prevState: TState
34
+ options?: StoreOptions<TState, TUpdater>
35
+
36
+ constructor(initialState: TState, options?: StoreOptions<TState, TUpdater>) {
37
+ this.prevState = initialState
38
+ this.state = initialState
39
+ this.options = options
40
+ }
41
+
42
+ subscribe = (listener: Listener<TState>) => {
43
+ this.listeners.add(listener)
44
+ const unsub = this.options?.onSubscribe?.(listener, this)
45
+ return () => {
46
+ this.listeners.delete(listener)
47
+ unsub?.()
48
+ }
49
+ }
50
+
51
+ setState = (updater: TUpdater) => {
52
+ this.prevState = this.state
53
+ this.state = this.options?.updateFn
54
+ ? this.options.updateFn(this.prevState)(updater)
55
+ : (updater as any)(this.prevState)
56
+
57
+ // Always run onUpdate, regardless of batching
58
+ this.options?.onUpdate?.()
59
+
60
+ // Attempt to flush
61
+ __flush(this as never)
62
+ }
63
+ }
package/src/types.ts ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @private
3
+ */
4
+ export type AnyUpdater = (prev: any) => any
5
+
6
+ /**
7
+ * @private
8
+ */
9
+ export interface ListenerValue<T> {
10
+ prevVal: T
11
+ currentVal: T
12
+ }
13
+
14
+ /**
15
+ * @private
16
+ */
17
+ export type Listener<T> = (value: ListenerValue<T>) => void