@e280/strata 0.0.0-6 → 0.0.0-8

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 (67) hide show
  1. package/README.md +80 -56
  2. package/package.json +5 -5
  3. package/s/signals/index.ts +2 -1
  4. package/s/signals/parts/derive.ts +29 -0
  5. package/s/signals/parts/effect.ts +3 -3
  6. package/s/signals/parts/lazy.ts +27 -0
  7. package/s/signals/parts/signal.ts +21 -43
  8. package/s/signals/parts/types.ts +11 -0
  9. package/s/signals/parts/units.ts +150 -0
  10. package/s/signals/signals.test.ts +103 -8
  11. package/s/tests.test.ts +1 -1
  12. package/s/tree/index.ts +1 -1
  13. package/s/tree/parts/branch.ts +17 -40
  14. package/s/tree/parts/chronobranch.ts +13 -13
  15. package/s/tree/parts/trunk.ts +27 -29
  16. package/s/tree/parts/types.ts +18 -6
  17. package/s/tree/parts/utils/setup.ts +8 -8
  18. package/s/tree/tree.test.ts +77 -68
  19. package/x/signals/index.d.ts +2 -1
  20. package/x/signals/index.js +2 -1
  21. package/x/signals/index.js.map +1 -1
  22. package/x/signals/parts/derive.d.ts +12 -0
  23. package/x/signals/parts/derive.js +12 -0
  24. package/x/signals/parts/derive.js.map +1 -0
  25. package/x/signals/parts/effect.d.ts +2 -2
  26. package/x/signals/parts/effect.js +2 -2
  27. package/x/signals/parts/effect.js.map +1 -1
  28. package/x/signals/parts/lazy.d.ts +10 -0
  29. package/x/signals/parts/lazy.js +12 -0
  30. package/x/signals/parts/lazy.js.map +1 -0
  31. package/x/signals/parts/signal.d.ts +11 -8
  32. package/x/signals/parts/signal.js +8 -37
  33. package/x/signals/parts/signal.js.map +1 -1
  34. package/x/signals/parts/types.d.ts +7 -0
  35. package/x/signals/parts/types.js +2 -0
  36. package/x/signals/parts/types.js.map +1 -0
  37. package/x/signals/parts/units.d.ts +43 -0
  38. package/x/signals/parts/units.js +131 -0
  39. package/x/signals/parts/units.js.map +1 -0
  40. package/x/signals/signals.test.d.ts +8 -3
  41. package/x/signals/signals.test.js +87 -8
  42. package/x/signals/signals.test.js.map +1 -1
  43. package/x/tests.test.js +1 -1
  44. package/x/tests.test.js.map +1 -1
  45. package/x/tree/index.d.ts +1 -1
  46. package/x/tree/index.js +1 -1
  47. package/x/tree/index.js.map +1 -1
  48. package/x/tree/parts/branch.d.ts +4 -7
  49. package/x/tree/parts/branch.js +11 -30
  50. package/x/tree/parts/branch.js.map +1 -1
  51. package/x/tree/parts/chronobranch.d.ts +4 -4
  52. package/x/tree/parts/chronobranch.js +12 -12
  53. package/x/tree/parts/chronobranch.js.map +1 -1
  54. package/x/tree/parts/trunk.d.ts +5 -5
  55. package/x/tree/parts/trunk.js +18 -29
  56. package/x/tree/parts/trunk.js.map +1 -1
  57. package/x/tree/parts/types.d.ts +12 -6
  58. package/x/tree/parts/utils/setup.d.ts +3 -3
  59. package/x/tree/parts/utils/setup.js +6 -6
  60. package/x/tree/parts/utils/setup.js.map +1 -1
  61. package/x/tree/tree.test.d.ts +7 -7
  62. package/x/tree/tree.test.js +76 -68
  63. package/x/tree/tree.test.js.map +1 -1
  64. package/s/signals/parts/computed.ts +0 -53
  65. package/x/signals/parts/computed.d.ts +0 -14
  66. package/x/signals/parts/computed.js +0 -41
  67. package/x/signals/parts/computed.js.map +0 -1
package/README.md CHANGED
@@ -4,12 +4,12 @@
4
4
  <br/>
5
5
 
6
6
  # ⛏️ strata
7
- 📦 `npm install @e280/strata`
8
- 🫩 probably my tenth state management library, lol
9
-
10
- <br/>
11
7
 
12
8
  ### get in loser, we're managing state
9
+ 📦 `npm install @e280/strata`
10
+ 🧙‍♂️ probably my tenth state management library, lol
11
+ 💁 it's all about rerendering ui when data changes
12
+
13
13
  🚦 **signals** — ephemeral view-level state
14
14
  🌳 **tree** — persistent app-level state
15
15
  🪄 **tracker** — reactivity integration hub
@@ -23,70 +23,91 @@
23
23
  <br/>
24
24
 
25
25
  ## 🚦 signals — *ephemeral view-level state*
26
- - `@e280/strata/signals`
26
+ ```ts
27
+ import {signal, effect, computed} from "@e280/strata"
28
+ ```
29
+
30
+ ### each signal holds a value
31
+ - **create a signal**
27
32
  ```ts
28
- import {signal, effect, computed} from "@e280/strata"
33
+ const count = signal(0)
29
34
  ```
30
- - **signals are little bundles of joy**
35
+ - **read a signal**
31
36
  ```ts
32
- const count = signal(0)
33
-
34
- console.log(count.get())
35
- // 0
36
-
37
- count.set(1)
37
+ count() // 0
38
+ ```
39
+ - **set a signal**
40
+ ```ts
41
+ count(1)
42
+ ```
43
+ - **set a signal, and await effect propagation**
44
+ ```ts
45
+ await count(2)
46
+ ```
38
47
 
39
- console.log(count.value)
40
- // 1
48
+ ### pick your poison
49
+ - **signals hipster fn syntax**
50
+ ```ts
51
+ count() // get
52
+ await count(2) // set
53
+ ```
54
+ > to achieve this hipster syntax i had to make the implementation so damn cursed, lol 💀
55
+ - **signals get/set syntax**
56
+ ```ts
57
+ count.get() // get
58
+ await count.set(2) // set
59
+ ```
60
+ - **signals .value accessor syntax**
61
+ ```ts
62
+ count.value // get
63
+ count.value = 2 // set
41
64
  ```
42
- - components/views will auto rerender when relevant signals change
43
- (if your component/view library is cool an integrates with `tracker`)
44
- - three ways to get it
45
- ```ts
46
- count() // 1
47
- count.get() // 1
48
- count.value // 1
49
- ```
50
- - three ways to set it
51
- ```ts
52
- await count(2)
53
- await count.set(2)
54
- count.value = 2
55
- ```
56
- - using `await` here allows you to wait for downstream effects to finish
57
- - **effects are run when the relevant signals change**
65
+ value pattern is nice for this vibe
66
+ ```ts
67
+ count.value++
68
+ count.value += 1
69
+ ```
70
+
71
+ ### effects
72
+ - **effects run when the relevant signals change**
58
73
  ```ts
59
74
  effect(() => console.log(count()))
60
- // 2
75
+ // 1
76
+ // the system detects 'count' is relevant
61
77
 
62
78
  count.value++
63
- // 3
79
+ // 2
80
+ // when count is changed, the effect fn is run
64
81
  ```
65
- - **computed signals are super lazy**
82
+
83
+ ### `signal.derive` and `signal.lazy` are computed signals
84
+ - **signal.derive**
85
+ is for combining signals
66
86
  ```ts
67
- const tenly = computed(() => {
68
- console.log("recomputed!")
69
- return count() * 10
70
- })
87
+ const a = signal(1)
88
+ const b = signal(10)
89
+ const product = signal.derive(() => a() * b())
71
90
 
72
- console.log(tenly())
73
- // "recomputed!"
74
- // 30
91
+ product() // 10
75
92
 
76
- await count(4)
93
+ // change a dependency,
94
+ // and the derived signal is automatically updated
95
+ await a.set(2)
77
96
 
78
- console.log(tenly.value)
79
- // "recomputed!"
80
- // 40
97
+ product() // 20
81
98
  ```
99
+ - **signal.lazy**
100
+ is for making special optimizations.
101
+ it's like derive, except it cannot trigger effects,
102
+ because it's so lazy it only computes the value on read, and only when necessary.
103
+ > ⚠️ *i repeat: lazy signals cannot trigger effects!*
82
104
 
83
105
  <br/>
84
106
 
85
107
  ## 🌳 tree — *persistent app-level state*
86
- - `@e280/strata/tree`
87
108
  - single-source-of-truth state tree
88
109
  - immutable except for `mutate(fn)` calls
89
- - undo/redo history, cross-tab sync, localStorage persistence
110
+ - localStorage persistence, cross-tab sync, undo/redo history
90
111
  - no spooky-dookie proxy magic — just god's honest javascript
91
112
 
92
113
  #### `Trunk` is your app's state tree root
@@ -131,28 +152,29 @@
131
152
  ```
132
153
  - you can branch a branch
133
154
 
134
- #### watch for mutations
155
+ #### `on` to watch for mutations
135
156
  - on the trunk, we can listen deeply for mutations within the whole tree
136
157
  ```ts
137
- trunk.watch(s => console.log(s.count))
158
+ trunk.on(s => console.log(s.count))
138
159
  ```
139
160
  - whereas branch listeners don't care about changes outside their scope
140
161
  ```ts
141
- snacks.watch(s => console.log(s.peanuts))
162
+ snacks.on(s => console.log(s.peanuts))
142
163
  ```
143
- - watch returns a fn to stop listening
164
+ - on returns a fn to stop listening
144
165
  ```ts
145
- const stop = trunk.watch(s => console.log(s.count))
166
+ const stop = trunk.on(s => console.log(s.count))
146
167
  stop() // stop listening
147
168
  ```
148
169
 
149
170
  ### only discerning high-class aristocrats are permitted beyond this point
150
171
 
151
172
  #### `Trunk.setup` for localStorage persistence etc
173
+ - it automatically handles persistence to localStorage and cross-tab synchronization
152
174
  - simple setup
153
175
  ```ts
154
176
  const {trunk} = await Trunk.setup({
155
- version: 1, // 👈 bump whenever your change state schema!
177
+ version: 1, // 👈 bump whenever you change state schema!
156
178
  initialState: {count: 0},
157
179
  })
158
180
  ```
@@ -186,7 +208,7 @@
186
208
  })
187
209
  ```
188
210
  - *big-brain moment:* the whole chronicle *itself* is stored in the state.. serializable.. think persistence — user can close their project, reopen, and their undo/redo history is still chillin' — *brat girl summer*
189
- - second, make a `Chronobranch` which is like a branch
211
+ - second, make a `Chronobranch` which is like a branch, but is concerned with history
190
212
  ```ts
191
213
  const snacks = trunk.chronobranch(64, s => s.snacks)
192
214
  // \
@@ -207,13 +229,15 @@
207
229
  snacks.undoable // 2
208
230
  snacks.redoable // 1
209
231
  ```
210
- - chronobranch can have its own branch — all their mutations advance history
232
+ - chronobranch can have its own branches — all their mutations advance history
211
233
  - plz pinky-swear right now, that you won't create a chronobranch under a branch under another chronobranch 💀
212
234
 
213
235
  <br/>
214
236
 
215
237
  ## 🪄 tracker — integrations
216
- - `@e280/strata/tracker`
238
+ - ```ts
239
+ import {tracker} from "@e280/strata/tracker"
240
+ ```
217
241
  - all reactivity is orchestrated by the `tracker`
218
242
  - if you are integrating a new state object, or a new view layer that needs to react to state changes, just read [tracker.ts](./s/tracker/tracker.ts)
219
243
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@e280/strata",
3
- "version": "0.0.0-6",
3
+ "version": "0.0.0-8",
4
4
  "description": "state management",
5
5
  "license": "MIT",
6
6
  "author": "Chase Moskal <chasemoskal@gmail.com>",
@@ -29,9 +29,12 @@
29
29
  "_tsc": "tsc",
30
30
  "_tscw": "tsc -w"
31
31
  },
32
+ "dependencies": {
33
+ "@e280/stz": "^0.0.0-28"
34
+ },
32
35
  "devDependencies": {
33
36
  "@e280/science": "^0.0.5",
34
- "@types/node": "^24.0.7",
37
+ "@types/node": "^24.0.10",
35
38
  "npm-run-all": "^4.1.5",
36
39
  "typescript": "^5.8.3"
37
40
  },
@@ -46,8 +49,5 @@
46
49
  },
47
50
  "bugs": {
48
51
  "url": "https://github.com/e280/strata/issues"
49
- },
50
- "dependencies": {
51
- "@e280/stz": "^0.0.0-27"
52
52
  }
53
53
  }
@@ -1,5 +1,6 @@
1
1
 
2
- export * from "./parts/computed.js"
2
+ export * from "./parts/lazy.js"
3
3
  export * from "./parts/effect.js"
4
4
  export * from "./parts/signal.js"
5
+ export * from "./parts/types.js"
5
6
 
@@ -0,0 +1,29 @@
1
+
2
+ import {Sub} from "@e280/stz"
3
+ import {SignalOptions} from "./types.js"
4
+ import {DerivedCore, processSignalOptions} from "./units.js"
5
+
6
+ export type DerivedSignal<V> = {
7
+ (): V
8
+ kind: "derived"
9
+
10
+ sneak: V
11
+ on: Sub<[V]>
12
+ get(): V
13
+ get value(): V
14
+ dispose(): void
15
+ }
16
+
17
+ export function derive<V>(formula: () => V, options: Partial<SignalOptions> = {}) {
18
+ function fn(): V {
19
+ return (fn as any).value
20
+ }
21
+
22
+ const o = processSignalOptions(options)
23
+ const core = DerivedCore.make<V>(fn as any, formula, o)
24
+ Object.setPrototypeOf(fn, DerivedCore.prototype)
25
+ Object.assign(fn, core)
26
+
27
+ return fn as DerivedSignal<V>
28
+ }
29
+
@@ -2,11 +2,11 @@
2
2
  import {debounce} from "@e280/stz"
3
3
  import {tracker} from "../../tracker/tracker.js"
4
4
 
5
- export function effect<C = void>(collector: () => C, responder: () => void = collector) {
6
- return initEffect<C>(collector, responder).dispose
5
+ export function effect(collector: () => void, responder: () => void = collector) {
6
+ return collectorEffect(collector, responder).dispose
7
7
  }
8
8
 
9
- export function initEffect<C = void>(collector: () => C, responder: () => void = collector) {
9
+ export function collectorEffect<C = void>(collector: () => C, responder: () => void = collector) {
10
10
  const {seen, result} = tracker.seen(collector)
11
11
  const fn = debounce(0, responder)
12
12
 
@@ -0,0 +1,27 @@
1
+
2
+ import {SignalOptions} from "./types.js"
3
+ import {LazyCore, processSignalOptions} from "./units.js"
4
+
5
+ export type LazySignal<V> = {
6
+ (): V
7
+ kind: "lazy"
8
+
9
+ sneak: V
10
+ get(): V
11
+ get value(): V
12
+ dispose(): void
13
+ }
14
+
15
+ export function lazy<V>(formula: () => V, options: Partial<SignalOptions> = {}) {
16
+ function fn(): V {
17
+ return (fn as any).value
18
+ }
19
+
20
+ const o = processSignalOptions(options)
21
+ const core = new LazyCore<V>(formula, o)
22
+ Object.setPrototypeOf(fn, LazyCore.prototype)
23
+ Object.assign(fn, core)
24
+
25
+ return fn as LazySignal<V>
26
+ }
27
+
@@ -1,16 +1,28 @@
1
1
 
2
- import {sub} from "@e280/stz"
3
- import {tracker} from "../../tracker/tracker.js"
2
+ import {Sub} from "@e280/stz"
3
+
4
+ import {lazy} from "./lazy.js"
5
+ import {derive} from "./derive.js"
6
+ import {SignalOptions} from "./types.js"
7
+ import {processSignalOptions, SignalCore} from "./units.js"
4
8
 
5
9
  export type Signal<V> = {
6
10
  (): V
7
11
  (v: V): Promise<void>
8
12
  (v?: V): V | Promise<void>
9
- } & SignalCore<V>
10
13
 
11
- export function signal<V>(value: V) {
12
- const core = new SignalCore(value)
14
+ kind: "signal"
13
15
 
16
+ sneak: V
17
+ value: V
18
+ on: Sub<[V]>
19
+ get(): V
20
+ set(v: V): Promise<void>
21
+ publish(v?: V): Promise<void>
22
+ dispose(): void
23
+ } & SignalCore<V>
24
+
25
+ export function signal<V>(value: V, options: Partial<SignalOptions> = {}) {
14
26
  function fn(): V
15
27
  function fn(v: V): Promise<void>
16
28
  function fn(v?: V): V | Promise<void> {
@@ -19,48 +31,14 @@ export function signal<V>(value: V) {
19
31
  : (fn as any).get()
20
32
  }
21
33
 
34
+ const o = processSignalOptions(options)
35
+ const core = new SignalCore(value, o)
22
36
  Object.setPrototypeOf(fn, SignalCore.prototype)
23
37
  Object.assign(fn, core)
24
38
 
25
39
  return fn as Signal<V>
26
40
  }
27
41
 
28
- export class SignalCore<V> {
29
- on = sub<[V]>()
30
- published: Promise<V>
31
-
32
- constructor(public sneak: V) {
33
- this.published = Promise.resolve(sneak)
34
- }
35
-
36
- get() {
37
- tracker.see(this)
38
- return this.sneak
39
- }
40
-
41
- async set(v: V) {
42
- if (v !== this.sneak)
43
- await this.publish(v)
44
- }
45
-
46
- get value() {
47
- return this.get()
48
- }
49
-
50
- set value(v: V) {
51
- this.set(v)
52
- }
53
-
54
- async publish(v = this.get()) {
55
- this.sneak = v
56
- await Promise.all([
57
- tracker.change(this),
58
- this.published = this.on.pub(v).then(() => v),
59
- ])
60
- }
61
-
62
- dispose() {
63
- this.on.clear()
64
- }
65
- }
42
+ signal.lazy = lazy
43
+ signal.derive = derive
66
44
 
@@ -0,0 +1,11 @@
1
+
2
+ import {Signal} from "./signal.js"
3
+ import {LazySignal} from "./lazy.js"
4
+ import {DerivedSignal} from "./derive.js"
5
+
6
+ export type Signaloid<V> = Signal<V> | DerivedSignal<V> | LazySignal<V>
7
+
8
+ export type SignalOptions = {
9
+ compare: (a: any, b: any) => boolean
10
+ }
11
+
@@ -0,0 +1,150 @@
1
+
2
+ import {sub} from "@e280/stz"
3
+ import {SignalOptions} from "./types.js"
4
+ import {collectorEffect} from "./effect.js"
5
+ import {tracker} from "../../tracker/tracker.js"
6
+
7
+ const defaultSignalOptions: SignalOptions = {
8
+ compare: (a, b) => a === b
9
+ }
10
+
11
+ export function processSignalOptions(options: Partial<SignalOptions> = {}) {
12
+ return {...defaultSignalOptions, ...options}
13
+ }
14
+
15
+ export class ReadableSignal<V> {
16
+ constructor(public sneak: V) {}
17
+
18
+ get() {
19
+ tracker.see(this)
20
+ return this.sneak
21
+ }
22
+
23
+ get value() {
24
+ return this.get()
25
+ }
26
+ }
27
+
28
+ export class ReactiveSignal<V> extends ReadableSignal<V> {
29
+ on = sub<[V]>()
30
+
31
+ dispose() {
32
+ this.on.clear()
33
+ }
34
+ }
35
+
36
+ export class SignalCore<V> extends ReactiveSignal<V> {
37
+ kind: "signal" = "signal"
38
+ _lock = false
39
+
40
+ constructor(sneak: V, public _options: SignalOptions) {
41
+ super(sneak)
42
+ }
43
+
44
+ async set(v: V) {
45
+ const isChanged = !this._options.compare(this.sneak, v)
46
+ if (isChanged)
47
+ await this.publish(v)
48
+ }
49
+
50
+ get value() {
51
+ return this.get()
52
+ }
53
+
54
+ set value(v: V) {
55
+ this.set(v)
56
+ }
57
+
58
+ async publish(v = this.get()) {
59
+ if (this._lock)
60
+ throw new Error("forbid circularity")
61
+
62
+ let promise = Promise.resolve()
63
+
64
+ try {
65
+ this._lock = true
66
+ this.sneak = v
67
+ promise = Promise.all([
68
+ tracker.change(this),
69
+ this.on.pub(v),
70
+ ]) as any
71
+ }
72
+ finally {
73
+ this._lock = false
74
+ }
75
+
76
+ return promise
77
+ }
78
+ }
79
+
80
+ export class LazyCore<V> extends ReadableSignal<V> {
81
+ kind: "lazy" = "lazy"
82
+
83
+ _dirty = false
84
+ _effect: (() => void) | undefined
85
+
86
+ constructor(public _formula: () => V, public _options: SignalOptions) {
87
+ super(undefined as any)
88
+ }
89
+
90
+ get() {
91
+ if (!this._effect) {
92
+ const {result, dispose} = collectorEffect(this._formula, () => this._dirty = true)
93
+ this._effect = dispose
94
+ this.sneak = result
95
+ }
96
+ if (this._dirty) {
97
+ this._dirty = false
98
+
99
+ const v = this._formula()
100
+ const isChanged = !this._options.compare(this.sneak, v)
101
+ if (isChanged) {
102
+ this.sneak = v
103
+ tracker.change(this)
104
+ }
105
+ }
106
+ return super.get()
107
+ }
108
+
109
+ get value() {
110
+ return this.get()
111
+ }
112
+
113
+ dispose() {
114
+ if (this._effect)
115
+ this._effect()
116
+ }
117
+ }
118
+
119
+ export class DerivedCore<V> extends ReactiveSignal<V> {
120
+ static make<V>(that: DerivedCore<V>, formula: () => V, options: SignalOptions) {
121
+ const {result, dispose} = collectorEffect(formula, async() => {
122
+ const value = formula()
123
+ const isChanged = !options.compare(that.sneak, value)
124
+ if (isChanged) {
125
+ that.sneak = value
126
+ await Promise.all([
127
+ tracker.change(that),
128
+ that.on.pub(value),
129
+ ])
130
+ }
131
+ })
132
+ return new this(result, dispose)
133
+ }
134
+
135
+ kind: "derived" = "derived"
136
+
137
+ constructor(initialValue: V, public _effect: () => void) {
138
+ super(initialValue)
139
+ }
140
+
141
+ get value() {
142
+ return this.get()
143
+ }
144
+
145
+ dispose() {
146
+ super.dispose()
147
+ this._effect()
148
+ }
149
+ }
150
+