@e280/strata 0.2.7 → 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/LICENSE +1 -1
- package/README.md +56 -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/s/tracker/bindings/react.ts +42 -0
- package/s/tracker/index.ts +1 -0
- 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/x/tracker/bindings/react.d.ts +10 -0
- package/x/tracker/bindings/react.js +29 -0
- package/x/tracker/bindings/react.js.map +1 -0
- package/x/tracker/index.d.ts +1 -0
- package/x/tracker/index.js +1 -0
- package/x/tracker/index.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/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
🚦 [**signals**](#signals) — ephemeral view-level state
|
|
18
18
|
🔮 [**prism**](#prism) — app-level state tree
|
|
19
19
|
🪄 [**tracker**](#tracker) — reactivity integration hub
|
|
20
|
+
⚛️ [**react**](#react) — optional bindings for react
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
|
|
@@ -28,7 +29,7 @@
|
|
|
28
29
|
> *ephemeral view-level state*
|
|
29
30
|
|
|
30
31
|
```ts
|
|
31
|
-
import {signal, effect} from "@e280/strata"
|
|
32
|
+
import {signal, effect, derived, lazy} from "@e280/strata"
|
|
32
33
|
```
|
|
33
34
|
|
|
34
35
|
### 🚦 each signal holds a value
|
|
@@ -45,7 +46,7 @@ import {signal, effect} from "@e280/strata"
|
|
|
45
46
|
```ts
|
|
46
47
|
$count(1)
|
|
47
48
|
```
|
|
48
|
-
- **
|
|
49
|
+
- 🤯 **await all downstream effects***
|
|
49
50
|
```ts
|
|
50
51
|
await $count(2)
|
|
51
52
|
```
|
|
@@ -85,13 +86,12 @@ import {signal, effect} from "@e280/strata"
|
|
|
85
86
|
// when $count is changed, the effect fn is run
|
|
86
87
|
```
|
|
87
88
|
|
|
88
|
-
### 🚦
|
|
89
|
-
- **
|
|
90
|
-
is for combining signals, like a formula
|
|
89
|
+
### 🚦 computed signals
|
|
90
|
+
- **derived,** for combining signals, like a formula
|
|
91
91
|
```ts
|
|
92
92
|
const $a = signal(1)
|
|
93
93
|
const $b = signal(10)
|
|
94
|
-
const $product =
|
|
94
|
+
const $product = derived(() => $a() * $b())
|
|
95
95
|
|
|
96
96
|
$product() // 10
|
|
97
97
|
|
|
@@ -101,52 +101,17 @@ import {signal, effect} from "@e280/strata"
|
|
|
101
101
|
|
|
102
102
|
$product() // 20
|
|
103
103
|
```
|
|
104
|
-
- **
|
|
105
|
-
is for making special optimizations.
|
|
104
|
+
- **lazy,** for making special optimizations.
|
|
106
105
|
it's like derived, except it cannot trigger effects,
|
|
107
106
|
because it's so damned lazy, it only computes the value on read, and only when necessary.
|
|
108
107
|
> *i repeat: lazy signals cannot trigger effects!*
|
|
109
108
|
|
|
110
|
-
### 🚦
|
|
111
|
-
-
|
|
112
|
-
- **you can instead use the core primitive classes**
|
|
113
|
-
```ts
|
|
114
|
-
const $count = new Signal(1)
|
|
115
|
-
```
|
|
116
|
-
core signals work mostly the same
|
|
117
|
-
```ts
|
|
118
|
-
// ✅ legal
|
|
119
|
-
$count.get()
|
|
120
|
-
$count.set(2)
|
|
121
|
-
```
|
|
122
|
-
except you cannot directly invoke them
|
|
123
|
-
```ts
|
|
124
|
-
// ⛔ illegal on core primitives
|
|
125
|
-
$count()
|
|
126
|
-
$count(2)
|
|
127
|
-
```
|
|
128
|
-
- **same thing for derived/lazy**
|
|
129
|
-
```ts
|
|
130
|
-
const $product = new Derived(() => $a() * $b())
|
|
131
|
-
```
|
|
132
|
-
```ts
|
|
133
|
-
const $product = new Lazy(() => $a() * $b())
|
|
134
|
-
```
|
|
135
|
-
- **conversions**
|
|
136
|
-
- all core primitives (signal/derived/lazy) have a convert-to-hipster-fn method
|
|
137
|
-
```ts
|
|
138
|
-
new Signal(1).fn() // SignalFn<number>, hipster-fn
|
|
139
|
-
```
|
|
140
|
-
- and all hipster fns (signal/derived/lazy) have a `.core` property to get the primitive
|
|
141
|
-
```ts
|
|
142
|
-
signal(0).core // Signal<number>, primitive instance
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
### 🚦 types
|
|
146
|
-
- **`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>`
|
|
147
111
|
- these are types for the core primitive classes
|
|
148
|
-
-
|
|
149
|
-
-
|
|
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.
|
|
150
115
|
|
|
151
116
|
|
|
152
117
|
|
|
@@ -363,6 +328,50 @@ note, the *items* that the tracker tracks can be any object, or symbol.. the tra
|
|
|
363
328
|
|
|
364
329
|
|
|
365
330
|
|
|
331
|
+
<br/><br/>
|
|
332
|
+
|
|
333
|
+
<a id="react"></a>
|
|
334
|
+
|
|
335
|
+
## 🍋 react bindings
|
|
336
|
+
> *easy peasy*
|
|
337
|
+
|
|
338
|
+
### ⚛️ react setup
|
|
339
|
+
|
|
340
|
+
1. setup your `strata.ts` module
|
|
341
|
+
```ts
|
|
342
|
+
import * as react from "react"
|
|
343
|
+
import {react as strata} from "@e280/strata"
|
|
344
|
+
|
|
345
|
+
export const {component, useStrata} = strata(react)
|
|
346
|
+
```
|
|
347
|
+
1. now you import `component` and `useStrata` from your module
|
|
348
|
+
```ts
|
|
349
|
+
import {component, useStrata} from "./strata.js"
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### ⚛️ `component` enables fully automatic reactive re-rendering
|
|
353
|
+
```ts
|
|
354
|
+
const $count = signal(0)
|
|
355
|
+
|
|
356
|
+
export const MyCounter = component(() => {
|
|
357
|
+
const add = () => $count.value++
|
|
358
|
+
return <button onClick={add}>{$count()}</button>
|
|
359
|
+
})
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### ⚛️ `useStrata` for a manual hands-on approach
|
|
363
|
+
```ts
|
|
364
|
+
const $count = signal(0)
|
|
365
|
+
|
|
366
|
+
export const MyCounter = () => {
|
|
367
|
+
const count = useStrata(() => $count())
|
|
368
|
+
const add = () => $count.value++
|
|
369
|
+
return <button onClick={add}>{count}</button>
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
|
|
366
375
|
<br/><br/>
|
|
367
376
|
|
|
368
377
|
## 🧑💻 strata is by e280
|
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
|
+
|