@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.
Files changed (162) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +56 -47
  3. package/package.json +3 -3
  4. package/s/index.ts +0 -1
  5. package/s/prism/prism.test.ts +2 -1
  6. package/s/prism/prism.ts +1 -1
  7. package/s/signals/derived/class.ts +34 -0
  8. package/s/signals/derived/fn.ts +36 -0
  9. package/s/signals/{tests/derived.test.ts → derived/test.ts} +13 -14
  10. package/s/signals/effect/effect.ts +7 -0
  11. package/s/signals/effect/test.ts +172 -0
  12. package/s/signals/effect/watch.ts +32 -0
  13. package/s/signals/index.ts +11 -7
  14. package/s/signals/lazy/class.ts +74 -0
  15. package/s/signals/lazy/fn.ts +22 -0
  16. package/s/signals/{tests/lazy.test.ts → lazy/test.ts} +23 -12
  17. package/s/signals/signal/class.ts +77 -0
  18. package/s/signals/signal/fn.ts +31 -0
  19. package/s/signals/{tests/signal.test.ts → signal/test.ts} +56 -59
  20. package/s/signals/signals.test.ts +8 -8
  21. package/s/signals/types.ts +4 -23
  22. package/s/signals/utils/default-compare.ts +1 -1
  23. package/s/signals/utils/symbols.ts +9 -0
  24. package/s/tests.test.ts +1 -57
  25. package/s/tracker/bindings/react.ts +42 -0
  26. package/s/tracker/index.ts +1 -0
  27. package/x/index.d.ts +0 -1
  28. package/x/index.js +0 -1
  29. package/x/index.js.map +1 -1
  30. package/x/prism/prism.js.map +1 -1
  31. package/x/prism/prism.test.js +1 -1
  32. package/x/prism/prism.test.js.map +1 -1
  33. package/x/signals/derived/class.d.ts +14 -0
  34. package/x/signals/derived/class.js +23 -0
  35. package/x/signals/derived/class.js.map +1 -0
  36. package/x/signals/derived/fn.d.ts +3 -0
  37. package/x/signals/derived/fn.js +28 -0
  38. package/x/signals/derived/fn.js.map +1 -0
  39. package/x/signals/{tests/derived.test.d.ts → derived/test.d.ts} +1 -3
  40. package/x/signals/{tests/derived.test.js → derived/test.js} +14 -15
  41. package/x/signals/derived/test.js.map +1 -0
  42. package/x/signals/effect/effect.d.ts +1 -0
  43. package/x/signals/effect/effect.js +5 -0
  44. package/x/signals/effect/effect.js.map +1 -0
  45. package/x/signals/{tests/effect.test.d.ts → effect/test.d.ts} +7 -0
  46. package/x/signals/effect/test.js +132 -0
  47. package/x/signals/effect/test.js.map +1 -0
  48. package/x/signals/effect/watch.d.ts +4 -0
  49. package/x/signals/effect/watch.js +25 -0
  50. package/x/signals/effect/watch.js.map +1 -0
  51. package/x/signals/index.d.ts +8 -7
  52. package/x/signals/index.js +8 -7
  53. package/x/signals/index.js.map +1 -1
  54. package/x/signals/lazy/class.d.ts +19 -0
  55. package/x/signals/lazy/class.js +59 -0
  56. package/x/signals/lazy/class.js.map +1 -0
  57. package/x/signals/lazy/fn.d.ts +3 -0
  58. package/x/signals/lazy/fn.js +17 -0
  59. package/x/signals/lazy/fn.js.map +1 -0
  60. package/x/signals/{tests/lazy.test.d.ts → lazy/test.d.ts} +2 -3
  61. package/x/signals/{tests/lazy.test.js → lazy/test.js} +23 -13
  62. package/x/signals/lazy/test.js.map +1 -0
  63. package/x/signals/signal/class.d.ts +20 -0
  64. package/x/signals/signal/class.js +57 -0
  65. package/x/signals/signal/class.js.map +1 -0
  66. package/x/signals/signal/fn.d.ts +7 -0
  67. package/x/signals/signal/fn.js +23 -0
  68. package/x/signals/signal/fn.js.map +1 -0
  69. package/x/signals/{tests/signal.test.d.ts → signal/test.d.ts} +8 -7
  70. package/x/signals/{tests/signal.test.js → signal/test.js} +50 -46
  71. package/x/signals/signal/test.js.map +1 -0
  72. package/x/signals/signals.test.d.ts +25 -20
  73. package/x/signals/signals.test.js +8 -8
  74. package/x/signals/signals.test.js.map +1 -1
  75. package/x/signals/types.d.ts +4 -19
  76. package/x/signals/utils/default-compare.js +1 -1
  77. package/x/signals/utils/default-compare.js.map +1 -1
  78. package/x/signals/utils/symbols.d.ts +7 -0
  79. package/x/signals/utils/symbols.js +8 -0
  80. package/x/signals/utils/symbols.js.map +1 -0
  81. package/x/tests.test.js +1 -45
  82. package/x/tests.test.js.map +1 -1
  83. package/x/tracker/bindings/react.d.ts +10 -0
  84. package/x/tracker/bindings/react.js +29 -0
  85. package/x/tracker/bindings/react.js.map +1 -0
  86. package/x/tracker/index.d.ts +1 -0
  87. package/x/tracker/index.js +1 -0
  88. package/x/tracker/index.js.map +1 -1
  89. package/s/signals/core/derived.ts +0 -65
  90. package/s/signals/core/effect.ts +0 -31
  91. package/s/signals/core/lazy.ts +0 -78
  92. package/s/signals/core/parts/reactive.ts +0 -12
  93. package/s/signals/core/parts/readable.ts +0 -16
  94. package/s/signals/core/signal.ts +0 -101
  95. package/s/signals/porcelain.ts +0 -30
  96. package/s/signals/tests/effect.test.ts +0 -89
  97. package/s/tree/index.ts +0 -7
  98. package/s/tree/parts/branch.ts +0 -55
  99. package/s/tree/parts/chronobranch.ts +0 -86
  100. package/s/tree/parts/persistence.ts +0 -31
  101. package/s/tree/parts/trunk.ts +0 -70
  102. package/s/tree/parts/types.ts +0 -70
  103. package/s/tree/parts/utils/immute.ts +0 -43
  104. package/s/tree/parts/utils/process-options.ts +0 -7
  105. package/s/tree/parts/utils/setup.ts +0 -40
  106. package/s/tree/tree.test.ts +0 -366
  107. package/x/signals/core/derived.d.ts +0 -10
  108. package/x/signals/core/derived.js +0 -52
  109. package/x/signals/core/derived.js.map +0 -1
  110. package/x/signals/core/effect.d.ts +0 -5
  111. package/x/signals/core/effect.js +0 -17
  112. package/x/signals/core/effect.js.map +0 -1
  113. package/x/signals/core/lazy.d.ts +0 -11
  114. package/x/signals/core/lazy.js +0 -60
  115. package/x/signals/core/lazy.js.map +0 -1
  116. package/x/signals/core/parts/reactive.d.ts +0 -5
  117. package/x/signals/core/parts/reactive.js +0 -9
  118. package/x/signals/core/parts/reactive.js.map +0 -1
  119. package/x/signals/core/parts/readable.d.ts +0 -6
  120. package/x/signals/core/parts/readable.js +0 -15
  121. package/x/signals/core/parts/readable.js.map +0 -1
  122. package/x/signals/core/signal.d.ts +0 -13
  123. package/x/signals/core/signal.js +0 -77
  124. package/x/signals/core/signal.js.map +0 -1
  125. package/x/signals/porcelain.d.ts +0 -8
  126. package/x/signals/porcelain.js +0 -15
  127. package/x/signals/porcelain.js.map +0 -1
  128. package/x/signals/tests/derived.test.js.map +0 -1
  129. package/x/signals/tests/effect.test.js +0 -72
  130. package/x/signals/tests/effect.test.js.map +0 -1
  131. package/x/signals/tests/lazy.test.js.map +0 -1
  132. package/x/signals/tests/signal.test.js.map +0 -1
  133. package/x/tree/index.d.ts +0 -5
  134. package/x/tree/index.js +0 -6
  135. package/x/tree/index.js.map +0 -1
  136. package/x/tree/parts/branch.d.ts +0 -14
  137. package/x/tree/parts/branch.js +0 -42
  138. package/x/tree/parts/branch.js.map +0 -1
  139. package/x/tree/parts/chronobranch.d.ts +0 -25
  140. package/x/tree/parts/chronobranch.js +0 -75
  141. package/x/tree/parts/chronobranch.js.map +0 -1
  142. package/x/tree/parts/persistence.d.ts +0 -2
  143. package/x/tree/parts/persistence.js +0 -23
  144. package/x/tree/parts/persistence.js.map +0 -1
  145. package/x/tree/parts/trunk.d.ts +0 -19
  146. package/x/tree/parts/trunk.js +0 -57
  147. package/x/tree/parts/trunk.js.map +0 -1
  148. package/x/tree/parts/types.d.ts +0 -28
  149. package/x/tree/parts/types.js +0 -2
  150. package/x/tree/parts/types.js.map +0 -1
  151. package/x/tree/parts/utils/immute.d.ts +0 -11
  152. package/x/tree/parts/utils/immute.js +0 -33
  153. package/x/tree/parts/utils/immute.js.map +0 -1
  154. package/x/tree/parts/utils/process-options.d.ts +0 -2
  155. package/x/tree/parts/utils/process-options.js +0 -4
  156. package/x/tree/parts/utils/process-options.js.map +0 -1
  157. package/x/tree/parts/utils/setup.d.ts +0 -8
  158. package/x/tree/parts/utils/setup.js +0 -24
  159. package/x/tree/parts/utils/setup.js.map +0 -1
  160. package/x/tree/tree.test.d.ts +0 -40
  161. package/x/tree/tree.test.js +0 -325
  162. package/x/tree/tree.test.js.map +0 -1
package/LICENSE CHANGED
@@ -1,7 +1,7 @@
1
1
 
2
2
  MIT License
3
3
 
4
- Copyright (c) 2025 Chase Moskal
4
+ Copyright (c) 2026 Chase Moskal
5
5
 
6
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
7
7
  of this software and associated documentation files (the "Software"), to deal
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
- - **write signal *(and await all downstream effects)***
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
- ### 🚦 `signal.derived` and `signal.lazy` are computed signals
89
- - **signal.derived**
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 = signal.derived(() => $a() * $b())
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
- - **signal.lazy**
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
- ### 🚦 core primitive classes
111
- - **the hipster-fn syntax has a slight performance cost**
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
- - **`SignalyFn<V>`** can be `SignalFn<V>` or `DerivedFn<V>` or `LazyFn<V>`
149
- - 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.
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.2.7",
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
+