@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.
- package/README.md +11 -47
- package/package.json +3 -3
- package/s/index.ts +0 -1
- package/s/prism/prism.test.ts +2 -1
- package/s/prism/prism.ts +1 -1
- package/s/signals/derived/class.ts +34 -0
- package/s/signals/derived/fn.ts +36 -0
- package/s/signals/{tests/derived.test.ts → derived/test.ts} +13 -14
- package/s/signals/effect/effect.ts +7 -0
- package/s/signals/effect/test.ts +172 -0
- package/s/signals/effect/watch.ts +32 -0
- package/s/signals/index.ts +11 -7
- package/s/signals/lazy/class.ts +74 -0
- package/s/signals/lazy/fn.ts +22 -0
- package/s/signals/{tests/lazy.test.ts → lazy/test.ts} +23 -12
- package/s/signals/signal/class.ts +77 -0
- package/s/signals/signal/fn.ts +31 -0
- package/s/signals/{tests/signal.test.ts → signal/test.ts} +56 -59
- package/s/signals/signals.test.ts +8 -8
- package/s/signals/types.ts +4 -23
- package/s/signals/utils/default-compare.ts +1 -1
- package/s/signals/utils/symbols.ts +9 -0
- package/s/tests.test.ts +1 -57
- package/x/index.d.ts +0 -1
- package/x/index.js +0 -1
- package/x/index.js.map +1 -1
- package/x/prism/prism.js.map +1 -1
- package/x/prism/prism.test.js +1 -1
- package/x/prism/prism.test.js.map +1 -1
- package/x/signals/derived/class.d.ts +14 -0
- package/x/signals/derived/class.js +23 -0
- package/x/signals/derived/class.js.map +1 -0
- package/x/signals/derived/fn.d.ts +3 -0
- package/x/signals/derived/fn.js +28 -0
- package/x/signals/derived/fn.js.map +1 -0
- package/x/signals/{tests/derived.test.d.ts → derived/test.d.ts} +1 -3
- package/x/signals/{tests/derived.test.js → derived/test.js} +14 -15
- package/x/signals/derived/test.js.map +1 -0
- package/x/signals/effect/effect.d.ts +1 -0
- package/x/signals/effect/effect.js +5 -0
- package/x/signals/effect/effect.js.map +1 -0
- package/x/signals/{tests/effect.test.d.ts → effect/test.d.ts} +7 -0
- package/x/signals/effect/test.js +132 -0
- package/x/signals/effect/test.js.map +1 -0
- package/x/signals/effect/watch.d.ts +4 -0
- package/x/signals/effect/watch.js +25 -0
- package/x/signals/effect/watch.js.map +1 -0
- package/x/signals/index.d.ts +8 -7
- package/x/signals/index.js +8 -7
- package/x/signals/index.js.map +1 -1
- package/x/signals/lazy/class.d.ts +19 -0
- package/x/signals/lazy/class.js +59 -0
- package/x/signals/lazy/class.js.map +1 -0
- package/x/signals/lazy/fn.d.ts +3 -0
- package/x/signals/lazy/fn.js +17 -0
- package/x/signals/lazy/fn.js.map +1 -0
- package/x/signals/{tests/lazy.test.d.ts → lazy/test.d.ts} +2 -3
- package/x/signals/{tests/lazy.test.js → lazy/test.js} +23 -13
- package/x/signals/lazy/test.js.map +1 -0
- package/x/signals/signal/class.d.ts +20 -0
- package/x/signals/signal/class.js +57 -0
- package/x/signals/signal/class.js.map +1 -0
- package/x/signals/signal/fn.d.ts +7 -0
- package/x/signals/signal/fn.js +23 -0
- package/x/signals/signal/fn.js.map +1 -0
- package/x/signals/{tests/signal.test.d.ts → signal/test.d.ts} +8 -7
- package/x/signals/{tests/signal.test.js → signal/test.js} +50 -46
- package/x/signals/signal/test.js.map +1 -0
- package/x/signals/signals.test.d.ts +25 -20
- package/x/signals/signals.test.js +8 -8
- package/x/signals/signals.test.js.map +1 -1
- package/x/signals/types.d.ts +4 -19
- package/x/signals/utils/default-compare.js +1 -1
- package/x/signals/utils/default-compare.js.map +1 -1
- package/x/signals/utils/symbols.d.ts +7 -0
- package/x/signals/utils/symbols.js +8 -0
- package/x/signals/utils/symbols.js.map +1 -0
- package/x/tests.test.js +1 -45
- package/x/tests.test.js.map +1 -1
- package/s/signals/core/derived.ts +0 -65
- package/s/signals/core/effect.ts +0 -31
- package/s/signals/core/lazy.ts +0 -78
- package/s/signals/core/parts/reactive.ts +0 -12
- package/s/signals/core/parts/readable.ts +0 -16
- package/s/signals/core/signal.ts +0 -101
- package/s/signals/porcelain.ts +0 -30
- package/s/signals/tests/effect.test.ts +0 -89
- package/s/tree/index.ts +0 -7
- package/s/tree/parts/branch.ts +0 -55
- package/s/tree/parts/chronobranch.ts +0 -86
- package/s/tree/parts/persistence.ts +0 -31
- package/s/tree/parts/trunk.ts +0 -70
- package/s/tree/parts/types.ts +0 -70
- package/s/tree/parts/utils/immute.ts +0 -43
- package/s/tree/parts/utils/process-options.ts +0 -7
- package/s/tree/parts/utils/setup.ts +0 -40
- package/s/tree/tree.test.ts +0 -366
- package/x/signals/core/derived.d.ts +0 -10
- package/x/signals/core/derived.js +0 -52
- package/x/signals/core/derived.js.map +0 -1
- package/x/signals/core/effect.d.ts +0 -5
- package/x/signals/core/effect.js +0 -17
- package/x/signals/core/effect.js.map +0 -1
- package/x/signals/core/lazy.d.ts +0 -11
- package/x/signals/core/lazy.js +0 -60
- package/x/signals/core/lazy.js.map +0 -1
- package/x/signals/core/parts/reactive.d.ts +0 -5
- package/x/signals/core/parts/reactive.js +0 -9
- package/x/signals/core/parts/reactive.js.map +0 -1
- package/x/signals/core/parts/readable.d.ts +0 -6
- package/x/signals/core/parts/readable.js +0 -15
- package/x/signals/core/parts/readable.js.map +0 -1
- package/x/signals/core/signal.d.ts +0 -13
- package/x/signals/core/signal.js +0 -77
- package/x/signals/core/signal.js.map +0 -1
- package/x/signals/porcelain.d.ts +0 -8
- package/x/signals/porcelain.js +0 -15
- package/x/signals/porcelain.js.map +0 -1
- package/x/signals/tests/derived.test.js.map +0 -1
- package/x/signals/tests/effect.test.js +0 -72
- package/x/signals/tests/effect.test.js.map +0 -1
- package/x/signals/tests/lazy.test.js.map +0 -1
- package/x/signals/tests/signal.test.js.map +0 -1
- package/x/tree/index.d.ts +0 -5
- package/x/tree/index.js +0 -6
- package/x/tree/index.js.map +0 -1
- package/x/tree/parts/branch.d.ts +0 -14
- package/x/tree/parts/branch.js +0 -42
- package/x/tree/parts/branch.js.map +0 -1
- package/x/tree/parts/chronobranch.d.ts +0 -25
- package/x/tree/parts/chronobranch.js +0 -75
- package/x/tree/parts/chronobranch.js.map +0 -1
- package/x/tree/parts/persistence.d.ts +0 -2
- package/x/tree/parts/persistence.js +0 -23
- package/x/tree/parts/persistence.js.map +0 -1
- package/x/tree/parts/trunk.d.ts +0 -19
- package/x/tree/parts/trunk.js +0 -57
- package/x/tree/parts/trunk.js.map +0 -1
- package/x/tree/parts/types.d.ts +0 -28
- package/x/tree/parts/types.js +0 -2
- package/x/tree/parts/types.js.map +0 -1
- package/x/tree/parts/utils/immute.d.ts +0 -11
- package/x/tree/parts/utils/immute.js +0 -33
- package/x/tree/parts/utils/immute.js.map +0 -1
- package/x/tree/parts/utils/process-options.d.ts +0 -2
- package/x/tree/parts/utils/process-options.js +0 -4
- package/x/tree/parts/utils/process-options.js.map +0 -1
- package/x/tree/parts/utils/setup.d.ts +0 -8
- package/x/tree/parts/utils/setup.js +0 -24
- package/x/tree/parts/utils/setup.js.map +0 -1
- package/x/tree/tree.test.d.ts +0 -40
- package/x/tree/tree.test.js +0 -325
- 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
|
-
- **
|
|
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
|
-
### 🚦
|
|
90
|
-
- **
|
|
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 =
|
|
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
|
-
- **
|
|
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
|
-
### 🚦
|
|
112
|
-
-
|
|
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
|
-
-
|
|
150
|
-
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
package/s/prism/prism.test.ts
CHANGED
|
@@ -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/
|
|
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
|
@@ -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 {
|
|
4
|
-
import {
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
}),
|
|
106
|
+
await a(2)
|
|
107
|
+
expect(product()).is(20)
|
|
109
108
|
}),
|
|
110
109
|
})
|
|
111
110
|
|
|
@@ -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
|
+
|
package/s/signals/index.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
|
|
2
|
-
export * from "./
|
|
3
|
-
export * from "./
|
|
4
|
-
|
|
5
|
-
export * from "./
|
|
6
|
-
export * from "./
|
|
7
|
-
|
|
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 "./
|
|
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
|
|
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
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
57
|
-
|
|
68
|
+
await a(5)
|
|
69
|
+
expect(sum()).is(8)
|
|
58
70
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}),
|
|
71
|
+
await b(7)
|
|
72
|
+
expect(sum()).is(12)
|
|
62
73
|
}),
|
|
63
74
|
})
|
|
64
75
|
|