Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Derived Stores #40

Merged
merged 108 commits into from
Dec 26, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
108 commits
Select commit Hold shift + click to select a range
d3be221
feat!: initial work on adding derived signals
crutchcorn Feb 5, 2024
5830fca
chore: add tests to derived state
crutchcorn Feb 5, 2024
ae74fa5
fix: add cleanup edgecase
crutchcorn Feb 5, 2024
605b520
fix: edgecase where using derived and store for base
crutchcorn Feb 5, 2024
9d4deb7
docs: update docs link
crutchcorn Feb 6, 2024
12d40a6
chore: refactor derived class
crutchcorn Feb 6, 2024
368db9d
chore: further refactors
crutchcorn Feb 6, 2024
759040d
chore: refactor work to make things a bit cleaner
crutchcorn Feb 6, 2024
d79b473
chore: minor renaming and cleanup
crutchcorn Feb 7, 2024
5700cc3
chore: add initial benchmarks
crutchcorn Feb 7, 2024
0f7c648
chore: add Angular
crutchcorn Feb 7, 2024
f0ec935
chore: update benchmark package versions
crutchcorn Feb 7, 2024
790303c
chore: fix ci
crutchcorn Feb 7, 2024
5a069c7
chore: move to proper derived store
crutchcorn Feb 9, 2024
157413f
fix: avoid issues with what signal is being written
crutchcorn Feb 9, 2024
192829c
chore: ignore benchmark file in test coverage
crutchcorn Feb 9, 2024
f4dc9da
chore: rename TSX to TS
crutchcorn Feb 9, 2024
b7987e8
feat: add Effect
crutchcorn Feb 9, 2024
aa447aa
Merge branch 'main' into derived-diamond-problem
lachlancollins Feb 17, 2024
87dd588
Fix eslint, sherif, and knip
lachlancollins Feb 17, 2024
de55814
chore: migrate from forEach to for of loop
crutchcorn Feb 26, 2024
2bb46e3
Merge branch 'main' into derived-diamond-problem
crutchcorn Aug 12, 2024
fa65a7e
ci: apply automated fixes
autofix-ci[bot] Aug 12, 2024
357a301
improve Vue performance (#122)
ZainW Sep 29, 2024
228db0e
feat: add lazyness to derived (#109)
crutchcorn Nov 25, 2024
925ecae
Merge branch 'main' into derived-diamond-problem
crutchcorn Nov 25, 2024
81ef72c
chore: migrate to options object over positional arguments
crutchcorn Nov 25, 2024
eac78fa
ci: apply automated fixes
autofix-ci[bot] Nov 25, 2024
a7e9cbe
chore: fix sherif
crutchcorn Nov 25, 2024
62c3664
Merge branch 'main' into derived-diamond-problem
crutchcorn Nov 25, 2024
bd900f7
docs: add autogenerated docs for effect and derived
crutchcorn Nov 25, 2024
3db6e6f
Merge branch 'main' into derived-diamond-problem
crutchcorn Nov 25, 2024
49e47ea
Merge branch 'main' into derived-diamond-problem
crutchcorn Nov 25, 2024
8105722
Merge branch 'main' into derived-diamond-problem
crutchcorn Nov 25, 2024
b952bce
chore: migrate derived and effect to have cleanup and mount as distin…
crutchcorn Nov 25, 2024
9958187
ci: apply automated fixes and generate docs
autofix-ci[bot] Nov 25, 2024
b53b821
feat: add previous and current value to the store
crutchcorn Nov 25, 2024
a46d778
feat: add ability to track previous and new values of deps
crutchcorn Nov 25, 2024
cd7bb0d
ci: apply automated fixes and generate docs
autofix-ci[bot] Nov 25, 2024
0fe4628
chore: fix build
crutchcorn Nov 25, 2024
cc48436
ci: apply automated fixes and generate docs
autofix-ci[bot] Nov 25, 2024
709f04b
feat: add ability to get previous value from derived fn
crutchcorn Nov 27, 2024
f8c9839
ci: apply automated fixes and generate docs
autofix-ci[bot] Nov 27, 2024
c3b3f94
feat: add proper typings to derived state
crutchcorn Nov 27, 2024
cef42eb
Merge branch 'derived-diamond-problem' of https://github.com/TanStack…
crutchcorn Nov 27, 2024
24a771b
ci: apply automated fixes and generate docs
autofix-ci[bot] Nov 27, 2024
ec4fcc1
chore: fix potentially faulty TState inferencing
crutchcorn Nov 27, 2024
555b7bb
Merge branch 'derived-diamond-problem' of https://github.com/TanStack…
crutchcorn Nov 27, 2024
15ce0d4
ci: apply automated fixes and generate docs
autofix-ci[bot] Nov 27, 2024
b1fa3ad
chore: fix usage for TypeScript 4.9
crutchcorn Nov 29, 2024
ab0889d
ci: apply automated fixes and generate docs
autofix-ci[bot] Nov 29, 2024
4be2a82
chore: fix knip
crutchcorn Nov 29, 2024
e96a781
Merge branch 'derived-diamond-problem' of https://github.com/TanStack…
crutchcorn Nov 29, 2024
bf39f70
chore: more work to support TS5
crutchcorn Nov 29, 2024
1e7fec0
chore: fix usage of import and require type
crutchcorn Nov 29, 2024
df0646d
chore!: drop TypeScript 4.9 support
crutchcorn Nov 29, 2024
7b0bdd2
ci: apply automated fixes and generate docs
autofix-ci[bot] Nov 29, 2024
d751b2d
fix: prevState now works as intended
crutchcorn Nov 29, 2024
e8ff103
chore!: remove store.batch, add in temporary scheduler, skip intentio…
crutchcorn Nov 29, 2024
5f11306
ci: apply automated fixes and generate docs
autofix-ci[bot] Nov 29, 2024
8fb38a8
chore: add working unregister from graph
crutchcorn Nov 29, 2024
ee55538
chore: hooman attempt
crutchcorn Nov 29, 2024
0be5257
chore: cursor attempt
crutchcorn Nov 29, 2024
7a41b27
chore: code cleanup and add back batch tests
crutchcorn Nov 29, 2024
dbbf664
chore: fix listener recieving old values
crutchcorn Nov 29, 2024
2b95688
chore: fix laziness
crutchcorn Nov 29, 2024
9661c17
ci: apply automated fixes and generate docs
autofix-ci[bot] Nov 29, 2024
acf03d7
chore!: fix timings and temporarily(?) remove lazy prop
crutchcorn Nov 30, 2024
7e9752f
ci: apply automated fixes and generate docs
autofix-ci[bot] Nov 30, 2024
13c472f
chore: migrate useStore in React to support Derived values stores as …
crutchcorn Nov 30, 2024
8c0e587
ci: apply automated fixes and generate docs
autofix-ci[bot] Nov 30, 2024
782e772
chore: fix CI tests
crutchcorn Nov 30, 2024
5d2e67f
chore: add Angular store Derived support
crutchcorn Nov 30, 2024
6e5156d
ci: apply automated fixes and generate docs
autofix-ci[bot] Nov 30, 2024
2baec1f
chore: fix issues with Solid store tests
crutchcorn Nov 30, 2024
ac58a5f
ci: apply automated fixes and generate docs
autofix-ci[bot] Nov 30, 2024
2c4129f
chore: fix useStore from Vue
crutchcorn Nov 30, 2024
861e8df
Merge branch 'derived-diamond-problem' of https://github.com/TanStack…
crutchcorn Nov 30, 2024
59ab2df
ci: apply automated fixes and generate docs
autofix-ci[bot] Nov 30, 2024
9a5d485
chore: svelte works with useStore now
crutchcorn Nov 30, 2024
697d15e
Merge branch 'derived-diamond-problem' of https://github.com/TanStack…
crutchcorn Nov 30, 2024
46e63ef
ci: apply automated fixes and generate docs
autofix-ci[bot] Nov 30, 2024
7dc64d5
chore: add demo of useStore working as-intended
crutchcorn Nov 30, 2024
34ae6b0
Merge branch 'derived-diamond-problem' of https://github.com/TanStack…
crutchcorn Nov 30, 2024
f8c01f6
chore: fix CI
crutchcorn Nov 30, 2024
e9c449d
fix: issues with unmounting and remounting derived values
crutchcorn Dec 1, 2024
ab4c1f3
ci: apply automated fixes and generate docs
autofix-ci[bot] Dec 1, 2024
2e0bfd3
chore: add failing tests
crutchcorn Dec 1, 2024
634cbf2
fix: issues with out-of-order mounting should be fixed
crutchcorn Dec 1, 2024
e16ced5
Merge branch 'derived-diamond-problem' of https://github.com/TanStack…
crutchcorn Dec 1, 2024
4ec258b
ci: apply automated fixes and generate docs
autofix-ci[bot] Dec 1, 2024
ce6f794
chore: add tests
crutchcorn Dec 1, 2024
38558db
ci: apply automated fixes and generate docs
autofix-ci[bot] Dec 1, 2024
29af493
chore: add another failing test
crutchcorn Dec 1, 2024
cc45169
ci: apply automated fixes and generate docs
autofix-ci[bot] Dec 1, 2024
7250813
chore: initial work to refactor away from Derived having an internal …
crutchcorn Dec 2, 2024
40dd505
chore: test refactor of Derived to not include a Store
crutchcorn Dec 2, 2024
5b2d4cd
fix: issues with recomputing should now be resolved
crutchcorn Dec 2, 2024
c8829b9
ci: apply automated fixes and generate docs
autofix-ci[bot] Dec 2, 2024
45abb51
chore: add failing test
crutchcorn Dec 2, 2024
cad759a
ci: apply automated fixes and generate docs
autofix-ci[bot] Dec 2, 2024
01a1422
fix: add fix to broken test
crutchcorn Dec 2, 2024
e6cdf83
ci: apply automated fixes and generate docs
autofix-ci[bot] Dec 2, 2024
54d221a
fix: batched values do not break prevDepVals and currDepVals anymore
crutchcorn Dec 26, 2024
51140c9
Merge branch 'main' into derived-diamond-problem
crutchcorn Dec 26, 2024
aa393a5
chore: fix CI
crutchcorn Dec 26, 2024
1aab40c
ci: apply automated fixes and generate docs
autofix-ci[bot] Dec 26, 2024
6099591
docs: finish vanilla JS quick-start guide
crutchcorn Dec 26, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/framework/solid/reference/useStore.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ title: Use Store
id: useStore
---

Please see [/packages/solid-store/src/index.ts](https://github.com/tanstack/store/tree/main/packages/solid-store/src/index.ts)
Please see [/packages/solid-store/src/store.ts](https://github.com/tanstack/store/tree/main/packages/solid-store/src/index.ts)
2 changes: 1 addition & 1 deletion docs/framework/vue/reference/useStore.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ title: Use Store
id: useStore
---

Please see [/packages/vue-store/src/index.ts](https://github.com/tanstack/store/tree/main/packages/vue-store/src/index.ts)
Please see [/packages/vue-store/src/store.ts](https://github.com/tanstack/store/tree/main/packages/vue-store/src/index.ts)
12 changes: 12 additions & 0 deletions knip.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
{
"$schema": "https://unpkg.com/knip@5/schema.json",
"workspaces": {
"packages/angular-store": {
"ignoreDependencies": ["@angular/compiler-cli"]
},
"packages/store": {
"ignore": ["src/tests/derived.bench.ts"],
"ignoreDependencies": [
"@angular/core",
"@preact/signals",
"solid-js",
"vue"
]
},
"packages/vue-store": {
"ignoreDependencies": ["vue2", "vue2.7"]
}
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,16 @@
"publint": "^0.2.9",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"rimraf": "^5.0.10",
"sherif": "^0.11.0",
"rimraf": "^5.0.5",
"sherif": "^0.7.0",
"solid-js": "^1.8.20",
"typescript": "5.4.2",
"typescript49": "npm:[email protected]",
"typescript50": "npm:[email protected]",
"typescript51": "npm:[email protected]",
"typescript52": "npm:[email protected]",
"typescript53": "npm:[email protected]",
"vite": "^5.4.0",
"vite": "^5.1.0",
"vitest": "^2.0.5",
"vue": "^3.4.37"
}
Expand Down
9 changes: 8 additions & 1 deletion packages/store/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"test:types:ts53": "node ../../node_modules/typescript53/lib/tsc.js",
"test:types:ts54": "tsc",
"test:lib": "vitest",
"test:bench": "vitest bench",
"test:lib:dev": "pnpm run test:lib --watch",
"test:build": "publint --strict",
"build": "vite build"
Expand All @@ -54,5 +55,11 @@
"files": [
"dist",
"src"
]
],
"devDependencies": {
"@angular/core": "^17.3.12",
"solid-js": "^1.8.20",
"@preact/signals": "^1.3.0",
"vue": "^3.4.37"
}
}
115 changes: 115 additions & 0 deletions packages/store/src/derived.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { Store } from './store'
import type { Listener } from './types'

interface DerivedOptions<TState> {
onSubscribe?: (listener: Listener, derived: Derived<TState>) => () => void
onUpdate?: () => void
}

export type Deps = Array<Derived<any> | Store<any>>

export class Derived<TState> {
_store!: Store<TState>
rootStores = new Set<Store<unknown>>()
deps: Deps

// Functions representing the subscriptions. Call a function to cleanup
_subscriptions: Array<() => void> = []

// What store called the current update, if any
_whatStoreIsCurrentlyInUse: Store<unknown> | null = null

constructor(deps: Deps, fn: () => TState, options?: DerivedOptions<TState>) {
crutchcorn marked this conversation as resolved.
Show resolved Hide resolved
this.deps = deps
this._store = new Store(fn(), {
onSubscribe: options?.onSubscribe?.bind(this) as never,
onUpdate: options?.onUpdate,
})
/**
* This is here to solve the pyramid dependency problem where:
* A
* / \
* B C
* \ /
* D
*
* Where we deeply traverse this tree, how do we avoid D being recomputed twice; once when B is updated, once when C is.
*
* To solve this, we create linkedDeps that allows us to sync avoid writes to the state until all of the deps have been
* resolved.
*
* This is a record of stores, because derived stores are not able to write values to, but stores are
*/
const storeToDerived = new Map<Store<unknown>, Set<Derived<unknown>>>()
const derivedToStore = new Map<Derived<unknown>, Set<Store<unknown>>>()

const updateStoreToDerived = (
store: Store<unknown>,
dep: Derived<unknown>,
) => {
const prevDerivesForStore = storeToDerived.get(store) || new Set()
prevDerivesForStore.add(dep)
storeToDerived.set(store, prevDerivesForStore)
}
for (const dep of deps) {
if (dep instanceof Derived) {
derivedToStore.set(dep, dep.rootStores)
for (const store of dep.rootStores) {
this.rootStores.add(store)
updateStoreToDerived(store, dep)
}
} else if (dep instanceof Store) {
this.rootStores.add(dep)
updateStoreToDerived(dep, this as Derived<unknown>)
}
}

let __depsThatHaveWrittenThisTick: Deps = []

for (const dep of deps) {
const isDepAStore = dep instanceof Store
let relatedLinkedDerivedVals: null | Set<Derived<unknown>> = null

const unsub = dep.subscribe(() => {
const store = isDepAStore ? dep : dep._whatStoreIsCurrentlyInUse
this._whatStoreIsCurrentlyInUse = store
if (store) {
relatedLinkedDerivedVals = storeToDerived.get(store) ?? null
}

__depsThatHaveWrittenThisTick.push(dep)
if (
!relatedLinkedDerivedVals ||
__depsThatHaveWrittenThisTick.length === relatedLinkedDerivedVals.size
) {
// Yay! All deps are resolved - write the value of this derived
this._store.setState(fn)
// Cleanup the deps that have written this tick
__depsThatHaveWrittenThisTick = []
this._whatStoreIsCurrentlyInUse = null
return
}
})

this._subscriptions.push(unsub)
}
}

get state() {
return this._store.state
}

cleanup = () => {
for (const cleanup of this._subscriptions) {
cleanup()
}
};

[(Symbol as never as { readonly dispose: unique symbol }).dispose]() {
this.cleanup()
}

subscribe = (listener: Listener) => {
return this._store.subscribe(listener)
}
}
22 changes: 22 additions & 0 deletions packages/store/src/effect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Derived } from './derived'
import type { Deps } from './derived'

export class Effect {
_derived: Derived<void>

constructor(items: Deps, effectFn: () => void) {
this._derived = new Derived(items, () => {}, {
onUpdate() {
effectFn()
},
})
}

cleanup() {
this._derived.cleanup()
}

[(Symbol as never as { readonly dispose: unique symbol }).dispose]() {
this.cleanup()
}
}
74 changes: 4 additions & 70 deletions packages/store/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,70 +1,4 @@
export type AnyUpdater = (...args: Array<any>) => any

export type Listener = () => void

export interface StoreOptions<
TState,
TUpdater extends AnyUpdater = (cb: TState) => TState,
> {
updateFn?: (previous: TState) => (updater: TUpdater) => TState
onSubscribe?: (
listener: Listener,
store: Store<TState, TUpdater>,
) => () => void
onUpdate?: () => void
}

export class Store<
TState,
TUpdater extends AnyUpdater = (cb: TState) => TState,
> {
listeners = new Set<Listener>()
state: TState
options?: StoreOptions<TState, TUpdater>
_batching = false
_flushing = 0

constructor(initialState: TState, options?: StoreOptions<TState, TUpdater>) {
this.state = initialState
this.options = options
}

subscribe = (listener: Listener) => {
this.listeners.add(listener)
const unsub = this.options?.onSubscribe?.(listener, this)
return () => {
this.listeners.delete(listener)
unsub?.()
}
}

setState = (updater: TUpdater) => {
const previous = this.state
this.state = this.options?.updateFn
? this.options.updateFn(previous)(updater)
: (updater as any)(previous)

// Always run onUpdate, regardless of batching
this.options?.onUpdate?.()

// Attempt to flush
this._flush()
}

_flush = () => {
if (this._batching) return
const flushId = ++this._flushing
this.listeners.forEach((listener) => {
if (this._flushing !== flushId) return
listener()
})
}

batch = (cb: () => void) => {
if (this._batching) return cb()
this._batching = true
cb()
this._batching = false
this._flush()
}
}
export * from './derived'
export * from './effect'
export * from './store'
export * from './types'
68 changes: 68 additions & 0 deletions packages/store/src/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { AnyUpdater, Listener } from './types'

interface StoreOptions<
TState,
TUpdater extends AnyUpdater = (cb: TState) => TState,
> {
updateFn?: (previous: TState) => (updater: TUpdater) => TState
onSubscribe?: (
listener: Listener,
store: Store<TState, TUpdater>,
) => () => void
onUpdate?: () => void
}

export class Store<
TState,
TUpdater extends AnyUpdater = (cb: TState) => TState,
> {
listeners = new Set<Listener>()
state: TState
options?: StoreOptions<TState, TUpdater>
_batching = false
_flushing = 0

constructor(initialState: TState, options?: StoreOptions<TState, TUpdater>) {
this.state = initialState
this.options = options
}

subscribe = (listener: Listener) => {
this.listeners.add(listener)
const unsub = this.options?.onSubscribe?.(listener, this)
return () => {
this.listeners.delete(listener)
unsub?.()
}
}

setState = (updater: TUpdater) => {
const previous = this.state
this.state = this.options?.updateFn
? this.options.updateFn(previous)(updater)
: (updater as any)(previous)

// Always run onUpdate, regardless of batching
this.options?.onUpdate?.()

// Attempt to flush
this._flush()
}

_flush = () => {
if (this._batching) return
const flushId = ++this._flushing
for (const listener of this.listeners) {
if (this._flushing !== flushId) continue
listener()
}
}

batch = (cb: () => void) => {
if (this._batching) return cb()
this._batching = true
cb()
this._batching = false
this._flush()
}
}
3 changes: 3 additions & 0 deletions packages/store/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type AnyUpdater = (...args: Array<any>) => any

export type Listener = () => void
Loading