@e280/strata 0.2.8 → 0.3.0-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.
Files changed (153) hide show
  1. package/README.md +11 -47
  2. package/package.json +3 -3
  3. package/s/index.ts +0 -1
  4. package/s/prism/prism.test.ts +2 -1
  5. package/s/prism/prism.ts +1 -1
  6. package/s/signals/derived/class.ts +34 -0
  7. package/s/signals/derived/fn.ts +36 -0
  8. package/s/signals/{tests/derived.test.ts → derived/test.ts} +13 -14
  9. package/s/signals/effect/effect.ts +7 -0
  10. package/s/signals/effect/test.ts +172 -0
  11. package/s/signals/effect/watch.ts +32 -0
  12. package/s/signals/index.ts +11 -7
  13. package/s/signals/lazy/class.ts +74 -0
  14. package/s/signals/lazy/fn.ts +22 -0
  15. package/s/signals/{tests/lazy.test.ts → lazy/test.ts} +23 -12
  16. package/s/signals/signal/class.ts +77 -0
  17. package/s/signals/signal/fn.ts +31 -0
  18. package/s/signals/{tests/signal.test.ts → signal/test.ts} +56 -59
  19. package/s/signals/signals.test.ts +8 -8
  20. package/s/signals/types.ts +4 -23
  21. package/s/signals/utils/default-compare.ts +1 -1
  22. package/s/signals/utils/symbols.ts +9 -0
  23. package/s/tests.test.ts +1 -57
  24. package/x/index.d.ts +0 -1
  25. package/x/index.js +0 -1
  26. package/x/index.js.map +1 -1
  27. package/x/prism/prism.js.map +1 -1
  28. package/x/prism/prism.test.js +1 -1
  29. package/x/prism/prism.test.js.map +1 -1
  30. package/x/signals/derived/class.d.ts +14 -0
  31. package/x/signals/derived/class.js +23 -0
  32. package/x/signals/derived/class.js.map +1 -0
  33. package/x/signals/derived/fn.d.ts +3 -0
  34. package/x/signals/derived/fn.js +28 -0
  35. package/x/signals/derived/fn.js.map +1 -0
  36. package/x/signals/{tests/derived.test.d.ts → derived/test.d.ts} +1 -3
  37. package/x/signals/{tests/derived.test.js → derived/test.js} +14 -15
  38. package/x/signals/derived/test.js.map +1 -0
  39. package/x/signals/effect/effect.d.ts +1 -0
  40. package/x/signals/effect/effect.js +5 -0
  41. package/x/signals/effect/effect.js.map +1 -0
  42. package/x/signals/{tests/effect.test.d.ts → effect/test.d.ts} +7 -0
  43. package/x/signals/effect/test.js +132 -0
  44. package/x/signals/effect/test.js.map +1 -0
  45. package/x/signals/effect/watch.d.ts +4 -0
  46. package/x/signals/effect/watch.js +25 -0
  47. package/x/signals/effect/watch.js.map +1 -0
  48. package/x/signals/index.d.ts +8 -7
  49. package/x/signals/index.js +8 -7
  50. package/x/signals/index.js.map +1 -1
  51. package/x/signals/lazy/class.d.ts +19 -0
  52. package/x/signals/lazy/class.js +59 -0
  53. package/x/signals/lazy/class.js.map +1 -0
  54. package/x/signals/lazy/fn.d.ts +3 -0
  55. package/x/signals/lazy/fn.js +17 -0
  56. package/x/signals/lazy/fn.js.map +1 -0
  57. package/x/signals/{tests/lazy.test.d.ts → lazy/test.d.ts} +2 -3
  58. package/x/signals/{tests/lazy.test.js → lazy/test.js} +23 -13
  59. package/x/signals/lazy/test.js.map +1 -0
  60. package/x/signals/signal/class.d.ts +20 -0
  61. package/x/signals/signal/class.js +57 -0
  62. package/x/signals/signal/class.js.map +1 -0
  63. package/x/signals/signal/fn.d.ts +7 -0
  64. package/x/signals/signal/fn.js +23 -0
  65. package/x/signals/signal/fn.js.map +1 -0
  66. package/x/signals/{tests/signal.test.d.ts → signal/test.d.ts} +8 -7
  67. package/x/signals/{tests/signal.test.js → signal/test.js} +50 -46
  68. package/x/signals/signal/test.js.map +1 -0
  69. package/x/signals/signals.test.d.ts +25 -20
  70. package/x/signals/signals.test.js +8 -8
  71. package/x/signals/signals.test.js.map +1 -1
  72. package/x/signals/types.d.ts +4 -19
  73. package/x/signals/utils/default-compare.js +1 -1
  74. package/x/signals/utils/default-compare.js.map +1 -1
  75. package/x/signals/utils/symbols.d.ts +7 -0
  76. package/x/signals/utils/symbols.js +8 -0
  77. package/x/signals/utils/symbols.js.map +1 -0
  78. package/x/tests.test.js +1 -45
  79. package/x/tests.test.js.map +1 -1
  80. package/s/signals/core/derived.ts +0 -65
  81. package/s/signals/core/effect.ts +0 -31
  82. package/s/signals/core/lazy.ts +0 -78
  83. package/s/signals/core/parts/reactive.ts +0 -12
  84. package/s/signals/core/parts/readable.ts +0 -16
  85. package/s/signals/core/signal.ts +0 -101
  86. package/s/signals/porcelain.ts +0 -30
  87. package/s/signals/tests/effect.test.ts +0 -89
  88. package/s/tree/index.ts +0 -7
  89. package/s/tree/parts/branch.ts +0 -55
  90. package/s/tree/parts/chronobranch.ts +0 -86
  91. package/s/tree/parts/persistence.ts +0 -31
  92. package/s/tree/parts/trunk.ts +0 -70
  93. package/s/tree/parts/types.ts +0 -70
  94. package/s/tree/parts/utils/immute.ts +0 -43
  95. package/s/tree/parts/utils/process-options.ts +0 -7
  96. package/s/tree/parts/utils/setup.ts +0 -40
  97. package/s/tree/tree.test.ts +0 -366
  98. package/x/signals/core/derived.d.ts +0 -10
  99. package/x/signals/core/derived.js +0 -52
  100. package/x/signals/core/derived.js.map +0 -1
  101. package/x/signals/core/effect.d.ts +0 -5
  102. package/x/signals/core/effect.js +0 -17
  103. package/x/signals/core/effect.js.map +0 -1
  104. package/x/signals/core/lazy.d.ts +0 -11
  105. package/x/signals/core/lazy.js +0 -60
  106. package/x/signals/core/lazy.js.map +0 -1
  107. package/x/signals/core/parts/reactive.d.ts +0 -5
  108. package/x/signals/core/parts/reactive.js +0 -9
  109. package/x/signals/core/parts/reactive.js.map +0 -1
  110. package/x/signals/core/parts/readable.d.ts +0 -6
  111. package/x/signals/core/parts/readable.js +0 -15
  112. package/x/signals/core/parts/readable.js.map +0 -1
  113. package/x/signals/core/signal.d.ts +0 -13
  114. package/x/signals/core/signal.js +0 -77
  115. package/x/signals/core/signal.js.map +0 -1
  116. package/x/signals/porcelain.d.ts +0 -8
  117. package/x/signals/porcelain.js +0 -15
  118. package/x/signals/porcelain.js.map +0 -1
  119. package/x/signals/tests/derived.test.js.map +0 -1
  120. package/x/signals/tests/effect.test.js +0 -72
  121. package/x/signals/tests/effect.test.js.map +0 -1
  122. package/x/signals/tests/lazy.test.js.map +0 -1
  123. package/x/signals/tests/signal.test.js.map +0 -1
  124. package/x/tree/index.d.ts +0 -5
  125. package/x/tree/index.js +0 -6
  126. package/x/tree/index.js.map +0 -1
  127. package/x/tree/parts/branch.d.ts +0 -14
  128. package/x/tree/parts/branch.js +0 -42
  129. package/x/tree/parts/branch.js.map +0 -1
  130. package/x/tree/parts/chronobranch.d.ts +0 -25
  131. package/x/tree/parts/chronobranch.js +0 -75
  132. package/x/tree/parts/chronobranch.js.map +0 -1
  133. package/x/tree/parts/persistence.d.ts +0 -2
  134. package/x/tree/parts/persistence.js +0 -23
  135. package/x/tree/parts/persistence.js.map +0 -1
  136. package/x/tree/parts/trunk.d.ts +0 -19
  137. package/x/tree/parts/trunk.js +0 -57
  138. package/x/tree/parts/trunk.js.map +0 -1
  139. package/x/tree/parts/types.d.ts +0 -28
  140. package/x/tree/parts/types.js +0 -2
  141. package/x/tree/parts/types.js.map +0 -1
  142. package/x/tree/parts/utils/immute.d.ts +0 -11
  143. package/x/tree/parts/utils/immute.js +0 -33
  144. package/x/tree/parts/utils/immute.js.map +0 -1
  145. package/x/tree/parts/utils/process-options.d.ts +0 -2
  146. package/x/tree/parts/utils/process-options.js +0 -4
  147. package/x/tree/parts/utils/process-options.js.map +0 -1
  148. package/x/tree/parts/utils/setup.d.ts +0 -8
  149. package/x/tree/parts/utils/setup.js +0 -24
  150. package/x/tree/parts/utils/setup.js.map +0 -1
  151. package/x/tree/tree.test.d.ts +0 -40
  152. package/x/tree/tree.test.js +0 -325
  153. package/x/tree/tree.test.js.map +0 -1
package/README.md CHANGED
@@ -29,7 +29,7 @@
29
29
  > *ephemeral view-level state*
30
30
 
31
31
  ```ts
32
- import {signal, effect} from "@e280/strata"
32
+ import {signal, effect, derived, lazy} from "@e280/strata"
33
33
  ```
34
34
 
35
35
  ### 🚦 each signal holds a value
@@ -46,7 +46,7 @@ import {signal, effect} from "@e280/strata"
46
46
  ```ts
47
47
  $count(1)
48
48
  ```
49
- - **write signal *(and await all downstream effects)***
49
+ - 🤯 **await all downstream effects***
50
50
  ```ts
51
51
  await $count(2)
52
52
  ```
@@ -86,13 +86,12 @@ import {signal, effect} from "@e280/strata"
86
86
  // when $count is changed, the effect fn is run
87
87
  ```
88
88
 
89
- ### 🚦 `signal.derived` and `signal.lazy` are computed signals
90
- - **signal.derived**
91
- is for combining signals, like a formula
89
+ ### 🚦 computed signals
90
+ - **derived,** for combining signals, like a formula
92
91
  ```ts
93
92
  const $a = signal(1)
94
93
  const $b = signal(10)
95
- const $product = signal.derived(() => $a() * $b())
94
+ const $product = derived(() => $a() * $b())
96
95
 
97
96
  $product() // 10
98
97
 
@@ -102,52 +101,17 @@ import {signal, effect} from "@e280/strata"
102
101
 
103
102
  $product() // 20
104
103
  ```
105
- - **signal.lazy**
106
- is for making special optimizations.
104
+ - **lazy,** for making special optimizations.
107
105
  it's like derived, except it cannot trigger effects,
108
106
  because it's so damned lazy, it only computes the value on read, and only when necessary.
109
107
  > *i repeat: lazy signals cannot trigger effects!*
110
108
 
111
- ### 🚦 core primitive classes
112
- - **the hipster-fn syntax has a slight performance cost**
113
- - **you can instead use the core primitive classes**
114
- ```ts
115
- const $count = new Signal(1)
116
- ```
117
- core signals work mostly the same
118
- ```ts
119
- // ✅ legal
120
- $count.get()
121
- $count.set(2)
122
- ```
123
- except you cannot directly invoke them
124
- ```ts
125
- // ⛔ illegal on core primitives
126
- $count()
127
- $count(2)
128
- ```
129
- - **same thing for derived/lazy**
130
- ```ts
131
- const $product = new Derived(() => $a() * $b())
132
- ```
133
- ```ts
134
- const $product = new Lazy(() => $a() * $b())
135
- ```
136
- - **conversions**
137
- - all core primitives (signal/derived/lazy) have a convert-to-hipster-fn method
138
- ```ts
139
- new Signal(1).fn() // SignalFn<number>, hipster-fn
140
- ```
141
- - and all hipster fns (signal/derived/lazy) have a `.core` property to get the primitive
142
- ```ts
143
- signal(0).core // Signal<number>, primitive instance
144
- ```
145
-
146
- ### 🚦 types
147
- - **`Signaly<V>`** — can be `Signal<V>` or `Derived<V>` or `Lazy<V>`
109
+ ### 🚦 types and such
110
+ - **`Signaly<Value>`** can be `Signal<Value>` or `Derived<Value>` or `Lazy<Value>`
148
111
  - these are types for the core primitive classes
149
- - **`SignalyFn<V>`** can be `SignalFn<V>` or `DerivedFn<V>` or `LazyFn<V>`
150
- - these `*Fn` types are for the hipster-fn-syntax enabled variants
112
+ - **the classes are funky**
113
+ - Signal, Derived, and Lazy classes cannot be subclassed or extended, due to spooky magic we've done to make the instances callable as functions (hipster syntax).
114
+ - however, at least `$count instanceof Signal` works, so at least that's working.
151
115
 
152
116
 
153
117
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@e280/strata",
3
- "version": "0.2.8",
3
+ "version": "0.3.0-0",
4
4
  "description": "state management",
5
5
  "license": "MIT",
6
6
  "author": "Chase Moskal <chasemoskal@gmail.com>",
@@ -30,12 +30,12 @@
30
30
  "_tscw": "tsc -w"
31
31
  },
32
32
  "dependencies": {
33
- "@e280/stz": "^0.2.21"
33
+ "@e280/stz": "^0.2.22"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@e280/science": "^0.1.8",
37
37
  "@e280/scute": "^0.2.2",
38
- "@types/node": "^25.3.2",
38
+ "@types/node": "^25.3.5",
39
39
  "npm-run-all": "^4.1.5",
40
40
  "typescript": "^5.9.3"
41
41
  },
package/s/index.ts CHANGED
@@ -2,5 +2,4 @@
2
2
  export * from "./prism/index.js"
3
3
  export * from "./signals/index.js"
4
4
  export * from "./tracker/index.js"
5
- export * from "./tree/index.js"
6
5
 
@@ -4,7 +4,7 @@ import {suite, test, expect} from "@e280/science"
4
4
  import {Prism} from "./prism.js"
5
5
  import {Chrono} from "./chrono/chrono.js"
6
6
  import {chronicle} from "./chrono/chronicle.js"
7
- import {effect} from "../signals/core/effect.js"
7
+ import {effect} from "../signals/effect/effect.js"
8
8
 
9
9
  export default suite({
10
10
  "prism": suite({
@@ -14,6 +14,7 @@ export default suite({
14
14
  await prism.set({count: 2})
15
15
  expect(prism.get().count).is(2)
16
16
  }),
17
+
17
18
  "get/set state can trigger effects": test(async() => {
18
19
  const prism = new Prism({count: 1})
19
20
  let triggered = 0
package/s/prism/prism.ts CHANGED
@@ -1,7 +1,7 @@
1
1
 
2
2
  import {microbounce, sub} from "@e280/stz"
3
3
  import {Lens} from "./lens.js"
4
- import { tracker } from "../tracker/tracker.js"
4
+ import {tracker} from "../tracker/tracker.js"
5
5
 
6
6
  /** state mangagement source-of-truth */
7
7
  export class Prism<State> {
@@ -0,0 +1,34 @@
1
+
2
+ import {Sub} from "@e280/stz"
3
+ import {derived} from "./fn.js"
4
+ import {SignalOptions} from "../types.js"
5
+ import {tracker} from "../../tracker/tracker.js"
6
+
7
+ export interface Derived<Value> {
8
+ (): Value
9
+ }
10
+
11
+ export class Derived<Value> {
12
+ sneak!: Value
13
+ on!: Sub<[Value]>
14
+ dispose!: () => void
15
+
16
+ constructor(formula: () => Value, options?: Partial<SignalOptions>) {
17
+ if (new.target !== Derived) throw new Error("Derived cannot be subclassed")
18
+ return derived(formula, options)
19
+ }
20
+
21
+ get value() {
22
+ return (this as Derived<any>).get()
23
+ }
24
+
25
+ get() {
26
+ tracker.notifyRead(this)
27
+ return this.sneak
28
+ }
29
+
30
+ toString() {
31
+ return `(derived "${String(this.get())}")`
32
+ }
33
+ }
34
+
@@ -0,0 +1,36 @@
1
+
2
+ import {sub} from "@e280/stz"
3
+ import {Derived} from "./class.js"
4
+ import {watch} from "../effect/watch.js"
5
+ import {SignalOptions} from "../types.js"
6
+ import {tracker} from "../../tracker/tracker.js"
7
+ import {defaultCompare} from "../utils/default-compare.js"
8
+
9
+ export function derived<Value>(formula: () => Value, options?: Partial<SignalOptions>) {
10
+ function fn(): Value {
11
+ return (fn as Derived<Value>).get()
12
+ }
13
+
14
+ const compare = options?.compare ?? defaultCompare
15
+
16
+ Object.setPrototypeOf(fn, Derived.prototype)
17
+ fn.on = sub<[Value]>()
18
+
19
+ const {result, dispose} = watch(formula, async() => {
20
+ const value = formula()
21
+ const isChanged = !compare(fn.sneak, value)
22
+ if (isChanged) {
23
+ fn.sneak = value
24
+ await Promise.all([
25
+ tracker.notifyWrite(fn),
26
+ fn.on.pub(value),
27
+ ])
28
+ }
29
+ })
30
+
31
+ fn.sneak = result
32
+ fn.dispose = dispose
33
+
34
+ return fn as Derived<Value>
35
+ }
36
+
@@ -1,7 +1,8 @@
1
1
 
2
2
  import {Science, test, expect, spy} from "@e280/science"
3
- import {effect} from "../core/effect.js"
4
- import {derived, signal} from "../porcelain.js"
3
+ import {derived} from "./fn.js"
4
+ import {signal} from "../signal/fn.js"
5
+ import {effect} from "../effect/effect.js"
5
6
 
6
7
  export default Science.suite({
7
8
  "basic": test(async() => {
@@ -39,7 +40,7 @@ export default Science.suite({
39
40
  "effect doesn't overreact to derived": test(async() => {
40
41
  const a = signal(1)
41
42
  const b = signal(10)
42
- const product = signal.derived(() => a.value * b.value)
43
+ const product = derived(() => a.value * b.value)
43
44
 
44
45
  const derivedSpy = spy(() => {})
45
46
  product.on(derivedSpy)
@@ -63,7 +64,7 @@ export default Science.suite({
63
64
  "derived.on": test(async() => {
64
65
  const a = signal(1)
65
66
  const b = signal(10)
66
- const product = signal.derived(() => a.value * b.value)
67
+ const product = derived(() => a.value * b.value)
67
68
  expect(product.value).is(10)
68
69
 
69
70
  const mole = spy((_v: number) => {})
@@ -79,7 +80,7 @@ export default Science.suite({
79
80
  "derived.on not called if result doesn't change": test(async() => {
80
81
  const a = signal(1)
81
82
  const b = signal(10)
82
- const product = signal.derived(() => a.value * b.value)
83
+ const product = derived(() => a.value * b.value)
83
84
  expect(product.value).is(10)
84
85
 
85
86
  const mole = spy((_v: number) => {})
@@ -96,16 +97,14 @@ export default Science.suite({
96
97
  expect(mole.spy.calls.length).is(1)
97
98
  }),
98
99
 
99
- "hipster fns": Science.suite({
100
- "basic": test(async() => {
101
- const a = signal(1)
102
- const b = signal(10)
103
- const product = derived(() => a() * b())
104
- expect(product()).is(10)
100
+ "hipster syntax": test(async() => {
101
+ const a = signal(1)
102
+ const b = signal(10)
103
+ const product = derived(() => a() * b())
104
+ expect(product()).is(10)
105
105
 
106
- await a(2)
107
- expect(product()).is(20)
108
- }),
106
+ await a(2)
107
+ expect(product()).is(20)
109
108
  }),
110
109
  })
111
110
 
@@ -0,0 +1,7 @@
1
+
2
+ import {watch} from "./watch.js"
3
+
4
+ export function effect(collector: () => void, responder?: () => void) {
5
+ return watch(collector, responder).dispose
6
+ }
7
+
@@ -0,0 +1,172 @@
1
+
2
+ import {Science, test, expect} from "@e280/science"
3
+ import {watch} from "./watch.js"
4
+ import {effect} from "./effect.js"
5
+ import {signal} from "../signal/fn.js"
6
+
7
+ export default Science.suite({
8
+ "watch": Science.suite({
9
+ "responder gets value": test(async() => {
10
+ const count = signal(1)
11
+ let collected = 0
12
+ watch(
13
+ () => count(),
14
+ x => { collected = x }
15
+ )
16
+ expect(collected).is(0)
17
+ await count(2)
18
+ expect(collected).is(2)
19
+ }),
20
+
21
+ "responder not called until change": test(async() => {
22
+ const count = signal(1)
23
+ let calls = 0
24
+ watch(
25
+ () => count(),
26
+ () => { calls++ }
27
+ )
28
+ expect(calls).is(0)
29
+ await count(2)
30
+ expect(calls).is(1)
31
+ }),
32
+
33
+ "watch updates dynamic dependencies": test(async() => {
34
+ const toggle = signal(true)
35
+ const a = signal(1)
36
+ const b = signal(10)
37
+
38
+ let collected = 0
39
+
40
+ watch(
41
+ () => toggle() ? a() : b(),
42
+ x => { collected = x }
43
+ )
44
+
45
+ await a(2)
46
+ expect(collected).is(2)
47
+
48
+ await toggle(false)
49
+ expect(collected).is(10)
50
+
51
+ collected = 0
52
+ await a(3)
53
+ expect(collected).is(0)
54
+
55
+ await b(11)
56
+ expect(collected).is(11)
57
+ })
58
+ }),
59
+
60
+ "tracks signal changes": test(async() => {
61
+ const count = signal(1)
62
+ let doubled = 0
63
+
64
+ effect(() => doubled = count.value * 2)
65
+ expect(doubled).is(2)
66
+
67
+ await count.set(3)
68
+ expect(doubled).is(6)
69
+ }),
70
+
71
+ "correct signal effect order": test(async() => {
72
+ let order: string[] = []
73
+ const count = signal(0)
74
+
75
+ effect(() => {
76
+ if (count.value)
77
+ order.push("effect")
78
+ })
79
+
80
+ order.push("before")
81
+ await count.set(1)
82
+ order.push("after")
83
+
84
+ expect(order.length).is(3)
85
+ expect(order[0]).is("before")
86
+ expect(order[1]).is("effect")
87
+ expect(order[2]).is("after")
88
+ }),
89
+
90
+ "simple effect called the correct number of times": test(async() => {
91
+ const count = signal(0)
92
+ let runs = 0
93
+ effect(() => { count(); runs++ })
94
+ expect(runs).is(1)
95
+ await count(1)
96
+ expect(runs).is(2)
97
+ await count(2)
98
+ expect(runs).is(3)
99
+ }),
100
+
101
+ "is only called when signal actually changes": test(async() => {
102
+ const count = signal(1)
103
+ let runs = 0
104
+ effect(() => {
105
+ count.get()
106
+ runs++
107
+ })
108
+ expect(runs).is(1)
109
+ await count.set(999)
110
+ expect(runs).is(2)
111
+ await count.set(999)
112
+ expect(runs).is(2)
113
+ }),
114
+
115
+ "debounced": test(async() => {
116
+ const count = signal(1)
117
+ let runs = 0
118
+ effect(() => {
119
+ count.get()
120
+ runs++
121
+ })
122
+ expect(runs).is(1)
123
+ count.value++
124
+ count.value++
125
+ await count.set(count.get() + 1)
126
+ expect(runs).is(2)
127
+ }),
128
+
129
+ "can be disposed": test(async() => {
130
+ const count = signal(1)
131
+ let doubled = 0
132
+
133
+ const dispose = effect(() => doubled = count.value * 2)
134
+ expect(doubled).is(2)
135
+
136
+ await count.set(3)
137
+ expect(doubled).is(6)
138
+
139
+ dispose()
140
+ await count.set(4)
141
+ expect(doubled).is(6) // old value
142
+ }),
143
+
144
+ "signal set promise waits for effects": test(async() => {
145
+ const count = signal(1)
146
+ let doubled = 0
147
+
148
+ effect(() => doubled = count.value * 2)
149
+ expect(doubled).is(2)
150
+
151
+ await count.set(3)
152
+ expect(doubled).is(6)
153
+ }),
154
+
155
+ "only runs on change": test(async() => {
156
+ const sig = signal("a")
157
+ let runs = 0
158
+
159
+ effect(() => {
160
+ sig.value
161
+ runs++
162
+ })
163
+ expect(runs).is(1)
164
+
165
+ await sig.set("a")
166
+ expect(runs).is(1)
167
+
168
+ await sig.set("b")
169
+ expect(runs).is(2)
170
+ }),
171
+ })
172
+
@@ -0,0 +1,32 @@
1
+
2
+ import {microbounce} from "@e280/stz"
3
+ import {tracker} from "../../tracker/tracker.js"
4
+
5
+ export function watch<Value>(
6
+ collector: () => Value,
7
+ responder?: (value: Value) => void,
8
+ ) {
9
+
10
+ let disposers: (() => void)[] = []
11
+
12
+ const dispose = () => {
13
+ for (const d of disposers) d()
14
+ disposers = []
15
+ }
16
+
17
+ const run = () => {
18
+ const {seen, result} = tracker.observe(collector)
19
+ for (const saw of seen)
20
+ disposers.push(tracker.subscribe(saw, reset))
21
+ return result
22
+ }
23
+
24
+ const reset = microbounce(() => {
25
+ dispose()
26
+ if (responder) responder(run())
27
+ else run()
28
+ })
29
+
30
+ return {result: run(), dispose}
31
+ }
32
+
@@ -1,14 +1,18 @@
1
1
 
2
- export * from "./core/parts/reactive.js"
3
- export * from "./core/parts/readable.js"
4
- export * from "./core/derived.js"
5
- export * from "./core/effect.js"
6
- export * from "./core/lazy.js"
7
- export * from "./core/signal.js"
2
+ export * from "./derived/fn.js"
3
+ export * from "./derived/class.js"
4
+
5
+ export * from "./effect/effect.js"
6
+ export * from "./effect/watch.js"
7
+
8
+ export * from "./lazy/fn.js"
9
+ export * from "./lazy/class.js"
8
10
 
9
11
  export * from "./r/map.js"
10
12
  export * from "./r/set.js"
11
13
 
12
- export * from "./porcelain.js"
14
+ export * from "./signal/fn.js"
15
+ export * from "./signal/class.js"
16
+
13
17
  export * from "./types.js"
14
18
 
@@ -0,0 +1,74 @@
1
+
2
+ import {lazy} from "./fn.js"
3
+ import {SignalOptions} from "../types.js"
4
+ import {tracker} from "../../tracker/tracker.js"
5
+ import {_collect, _compare, _dirty, _disposers, _effect, _formula} from "../utils/symbols.js"
6
+
7
+ export interface Lazy<Value> {
8
+ (): Value
9
+ }
10
+
11
+ export class Lazy<Value> {
12
+ sneak!: Value
13
+ ;[_formula]!: () => Value
14
+ ;[_dirty]!: boolean
15
+ ;[_disposers]!: (() => void)[]
16
+ ;[_effect]!: (() => void) | undefined
17
+ ;[_compare]!: (a: any, b: any) => boolean
18
+
19
+ constructor(formula: () => Value, options?: Partial<SignalOptions>) {
20
+ if (new.target !== Lazy) throw new Error("Lazy cannot be subclassed")
21
+ return lazy(formula, options)
22
+ }
23
+
24
+ get value() {
25
+ return this.get()
26
+ }
27
+
28
+ [_collect]() {
29
+ for (const d of this[_disposers]) d()
30
+ this[_disposers] = []
31
+
32
+ const {seen, result} = tracker.observe(this[_formula])
33
+
34
+ const markDirty = async() => { this[_dirty] = true }
35
+ for (const saw of seen)
36
+ this[_disposers].push(tracker.subscribe(saw, markDirty))
37
+
38
+ this[_effect] = () => {
39
+ for (const d of this[_disposers]) d()
40
+ this[_disposers] = []
41
+ }
42
+
43
+ return result
44
+ }
45
+
46
+ get() {
47
+ if (!this[_effect]) {
48
+ this.sneak = this[_collect]()
49
+ this[_dirty] = false
50
+ }
51
+ else if (this[_dirty]) {
52
+ this[_dirty] = false
53
+ const value = this[_collect]()
54
+ const isChanged = !this[_compare](this.sneak, value)
55
+ if (isChanged) {
56
+ this.sneak = value
57
+ tracker.notifyWrite(this)
58
+ }
59
+ }
60
+
61
+ tracker.notifyRead(this)
62
+ return this.sneak
63
+ }
64
+
65
+ dispose() {
66
+ if (this[_effect])
67
+ this[_effect]()
68
+ }
69
+
70
+ toString() {
71
+ return `($lazy "${String(this.get())}")`
72
+ }
73
+ }
74
+
@@ -0,0 +1,22 @@
1
+
2
+ import {Lazy} from "./class.js"
3
+ import {SignalOptions} from "../types.js"
4
+ import {defaultCompare} from "../utils/default-compare.js"
5
+ import {_compare, _dirty, _disposers, _effect, _formula} from "../utils/symbols.js"
6
+
7
+ export function lazy<Value>(formula: () => Value, options?: Partial<SignalOptions>) {
8
+ function fn(): Value {
9
+ return (fn as Lazy<Value>).get()
10
+ }
11
+
12
+ Object.setPrototypeOf(fn, Lazy.prototype)
13
+ fn.sneak = undefined
14
+ fn[_formula] = formula
15
+ fn[_dirty] = false
16
+ fn[_effect] = undefined
17
+ fn[_disposers] = [] as any
18
+ fn[_compare] = options?.compare ?? defaultCompare
19
+
20
+ return fn as Lazy<Value>
21
+ }
22
+
@@ -1,6 +1,7 @@
1
1
 
2
2
  import {Science, test, expect} from "@e280/science"
3
- import {lazy, signal} from "../porcelain.js"
3
+ import {lazy} from "./fn.js"
4
+ import {signal} from "../signal/fn.js"
4
5
 
5
6
  export default Science.suite({
6
7
  "lazy values": test(async() => {
@@ -35,6 +36,18 @@ export default Science.suite({
35
36
  expect(runs).is(2)
36
37
  }),
37
38
 
39
+ "lazy handles changing deps": test(async() => {
40
+ const toggle = signal(true)
41
+ const a = signal(1)
42
+ const b = signal(2)
43
+ const comp = lazy(() => toggle() ? a() : b())
44
+ expect(comp()).is(1)
45
+ await toggle(false)
46
+ expect(comp()).is(2)
47
+ await b(3)
48
+ expect(comp()).is(3)
49
+ }),
50
+
38
51
  "lazy syntax": test(async() => {
39
52
  const a = signal(2)
40
53
  const b = signal(3)
@@ -46,19 +59,17 @@ export default Science.suite({
46
59
  expect(sum.get()).is(8)
47
60
  }),
48
61
 
49
- "hipster fns": Science.suite({
50
- "lazy values": test(async() => {
51
- const a = signal(2)
52
- const b = signal(3)
53
- const sum = lazy(() => a() + b())
54
- expect(sum()).is(5)
62
+ "lazy hipster syntax": test(async() => {
63
+ const a = signal(2)
64
+ const b = signal(3)
65
+ const sum = lazy(() => a() + b())
66
+ expect(sum()).is(5)
55
67
 
56
- await a(5)
57
- expect(sum()).is(8)
68
+ await a(5)
69
+ expect(sum()).is(8)
58
70
 
59
- await b(7)
60
- expect(sum()).is(12)
61
- }),
71
+ await b(7)
72
+ expect(sum()).is(12)
62
73
  }),
63
74
  })
64
75