Skip to content

Commit 168c177

Browse files
authored
feat(agera): introduce new reactivity system (#49)
1 parent 97a0735 commit 168c177

38 files changed

+6886
-4409
lines changed

package.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,15 @@
4242
"del-cli": "^6.0.0",
4343
"eslint": "^8.28.0",
4444
"kida": "workspace:^",
45-
"nanoviews": "workspace:^",
4645
"nano-staged": "^0.8.0",
46+
"nanoviews": "workspace:^",
4747
"simple-git-hooks": "^2.7.0",
48-
"typescript": "^5.0.0"
48+
"typescript": "5.3.3",
49+
"vite-plugin-filter-replace": "^0.1.14"
50+
},
51+
"pnpm": {
52+
"patchedDependencies": {
53+
"vite": "patches/vite.patch"
54+
}
4955
}
5056
}

packages/agera/.eslintrc.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"extends": [
3+
"@trigen/eslint-config/typescript",
4+
"@trigen/eslint-config/typescript-requiring-type-checking",
5+
"@trigen/eslint-config/jest"
6+
],
7+
"env": {
8+
"browser": true,
9+
"node": true
10+
},
11+
"parserOptions": {
12+
"tsconfigRootDir": "./packages/agera",
13+
"project": ["./tsconfig.json"]
14+
},
15+
"rules": {
16+
"@typescript-eslint/no-invalid-void-type": "off",
17+
"consistent-return": "off",
18+
"prefer-destructuring": "off"
19+
}
20+
}

packages/agera/.size-limit.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[
2+
{
3+
"name": "All publics",
4+
"path": "dist/index.js",
5+
"import": "*",
6+
"limit": "1.77 kB"
7+
},
8+
{
9+
"name": "Signal",
10+
"path": "dist/index.js",
11+
"import": "{ signal }",
12+
"limit": "1.21 kB"
13+
},
14+
{
15+
"name": "Popular set",
16+
"path": "dist/index.js",
17+
"import": "{ signal, computed, effect }",
18+
"limit": "1.37 kB"
19+
}
20+
]

packages/agera/LICENSE

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
MIT License
2+
3+
alien-signals
4+
Copyright (c) 2024-present Johnson Chu
5+
6+
agera
7+
Copyright (c) 2025-present Daniil Onoshko
8+
9+
Permission is hereby granted, free of charge, to any person obtaining a copy
10+
of this software and associated documentation files (the "Software"), to deal
11+
in the Software without restriction, including without limitation the rights
12+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13+
copies of the Software, and to permit persons to whom the Software is
14+
furnished to do so, subject to the following conditions:
15+
16+
The above copyright notice and this permission notice shall be included in all
17+
copies or substantial portions of the Software.
18+
19+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25+
SOFTWARE.

packages/agera/README.md

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
# Agera
2+
3+
[![ESM-only package][package]][package-url]
4+
[![NPM version][npm]][npm-url]
5+
[![Dependencies status][deps]][deps-url]
6+
[![Install size][size]][size-url]
7+
[![Build status][build]][build-url]
8+
[![Coverage status][coverage]][coverage-url]
9+
10+
[package]: https://img.shields.io/badge/package-ESM--only-ffe536.svg
11+
[package-url]: https://nodejs.org/api/esm.html
12+
13+
[npm]: https://img.shields.io/npm/v/agera.svg
14+
[npm-url]: https://npmjs.com/package/agera
15+
16+
[deps]: https://img.shields.io/librariesio/release/npm/agera
17+
[deps-url]: https://libraries.io/npm/agera/tree
18+
19+
[size]: https://deno.bundlejs.com/badge?q=agera
20+
[size-url]: https://bundlejs.com/?q=agera
21+
22+
[build]: https://img.shields.io/github/actions/workflow/status/TrigenSoftware/nanoviews/tests.yml?branch=main
23+
[build-url]: https://github.com/TrigenSoftware/nanoviews/actions
24+
25+
[coverage]: https://img.shields.io/codecov/c/github/TrigenSoftware/nanoviews.svg
26+
[coverage-url]: https://app.codecov.io/gh/TrigenSoftware/nanoviews
27+
28+
A small push-pull based signal library based on [alien-signals](https://github.com/stackblitz/alien-signals) algorithm.
29+
30+
Was created as reactivity system for [nanoviews](https://github.com/TrigenSoftware/nanoviews/tree/main/packages/nanoviews) and [Kida](https://github.com/TrigenSoftware/nanoviews/tree/main/packages/kida).
31+
32+
- **Small**. Around 1.37 kB for basic methods (minified and brotlied). Zero dependencies.
33+
- **Super fast** as [alien-signals](https://github.com/stackblitz/alien-signals).
34+
- Designed for best **Tree-Shaking**: only the code you use is included in your bundle.
35+
- **TypeScript**-first.
36+
37+
```ts
38+
import { signal, computed, effect } from 'agera'
39+
40+
const $count = signal(1)
41+
const $doubleCount = computed(() => count() * 2)
42+
43+
effect(() => {
44+
console.log(`Count is: ${$count()}`);
45+
}) // Console: Count is: 1
46+
47+
console.log($doubleCount()) // 2
48+
49+
$count(2) // Console: Count is: 2
50+
51+
console.log($doubleCount()) // 4
52+
```
53+
54+
<hr />
55+
<a href="#install">Install</a>
56+
<span>&nbsp;&nbsp;&nbsp;&nbsp;</span>
57+
<a href="#basics">API</a>
58+
<span>&nbsp;&nbsp;&nbsp;&nbsp;</span>
59+
<a href="#differences-from-alien-signals">Differences from alien-signals</a>
60+
<br />
61+
<hr />
62+
63+
## Install
64+
65+
```bash
66+
pnpm add -D agera
67+
# or
68+
npm i -D agera
69+
# or
70+
yarn add -D agera
71+
```
72+
73+
## API
74+
75+
### Signal
76+
77+
Signal is a basic store type. It stores a single value.
78+
79+
```ts
80+
import { signal, update } from 'agera'
81+
82+
const $count = signal(0)
83+
84+
$count($count() + 1)
85+
// or
86+
update($count, count => count + 1)
87+
```
88+
89+
To watch signal changes, use the `effect` function. Effect will be called immediately and every time the signal changes.
90+
91+
```ts
92+
import { signal, effect } from 'agera'
93+
94+
const $count = signal(0)
95+
96+
const stop = effect(() => {
97+
console.log('Count:', $count())
98+
99+
return () => {
100+
// Cleanup function. Will be called before effect update and before effect stop.
101+
}
102+
})
103+
// later you can stop effect
104+
stop()
105+
```
106+
107+
### Computed
108+
109+
Computed is a signal that computes its value based on other signals.
110+
111+
```ts
112+
import { computed } from 'agera'
113+
114+
const $firstName = signal('John')
115+
const $lastName = signal('Doe')
116+
const $fullName = computed(() => `${$firstName()} ${$lastName()}`)
117+
118+
console.log($fullName()) // John Doe
119+
```
120+
121+
### `effectScope`
122+
123+
`effectScope` creates a scope for effects. It allows to stop all effects in the scope at once.
124+
125+
```ts
126+
import { signal, effectScope, effect } from 'agera'
127+
128+
const $a = signal(0)
129+
const $b = signal(0)
130+
const stop = effectScope(() => {
131+
effect(() => {
132+
console.log('A:', $a())
133+
})
134+
135+
effectScope(() => {
136+
effect(() => {
137+
console.log('B:', $b())
138+
})
139+
})
140+
})
141+
142+
stop() // stop all effects
143+
```
144+
145+
Also there is a possibility to create a lazy scope.
146+
147+
```ts
148+
import { signal, effectScope, effect } from 'agera'
149+
150+
const $a = signal(0)
151+
const $b = signal(0)
152+
// All scopes will run immediately, but effects run is delayed
153+
const start = effectScope(() => {
154+
effect(() => {
155+
console.log('A:', $a())
156+
})
157+
158+
effectScope(() => {
159+
effect(() => {
160+
console.log('B:', $b())
161+
})
162+
})
163+
}, true) // marks scope as lazy
164+
// start all effects
165+
const stop = start()
166+
167+
stop() // stop all effects
168+
```
169+
170+
### Lifecycles
171+
172+
One of main feature of Agera is that every signal can be active or inactive. It allows to create lazy signals, which will use resources only if signal is really used in the UI.
173+
174+
- Signal is active when one or more effects is attached to it.
175+
- Signal is incative when signal has no effects.
176+
177+
`onActivate` lifecycle method adds callback for activation and deactivation events.
178+
179+
```ts
180+
import { signal, onActivate, effect } from 'agera'
181+
182+
const $count = signal(0)
183+
184+
onActivate($count, (active) => {
185+
console.log('Signal is', active ? 'active' : 'inactive')
186+
})
187+
188+
// will activate signal
189+
const stop = effect(() => {
190+
console.log('Count:', $count())
191+
})
192+
// will deactivate signal
193+
stop()
194+
```
195+
196+
### Batch updates
197+
198+
To batch updates you should wrap signal updates between `startBatch` and `endBatch`.
199+
200+
```ts
201+
import { signal, startBatch, endBatch, effect } from 'agera'
202+
203+
const $a = signal(0)
204+
const $b = signal(0)
205+
206+
effect(() => {
207+
console.log('Sum:', $a() + $b())
208+
})
209+
210+
// Effects will be called only once
211+
startBatch()
212+
$a(1)
213+
$b(2)
214+
endBatch()
215+
```
216+
217+
### Skip tracking
218+
219+
To skip tracking of signal changes you should wrap signal calls between `pauseTracking` and `resumeTracking`.
220+
221+
```ts
222+
import { signal, pauseTracking, resumeTracking, effect } from 'agera'
223+
224+
const $a = signal(0)
225+
const $b = signal(0)
226+
227+
effect(() => {
228+
const a = $a()
229+
230+
pauseTracking()
231+
232+
const b = $b()
233+
234+
resumeTracking()
235+
236+
console.log('Sum:', a + b)
237+
})
238+
239+
// Will trigger effect run
240+
$a(1)
241+
242+
// Will not trigger effect run
243+
$b(2)
244+
```
245+
246+
### Morph
247+
248+
`morph` methods allows to create signals that can change their getter and setter on the fly.
249+
250+
```ts
251+
import { signal, morph, $$get, $$set, $$source } from 'agera'
252+
253+
const $string = signal('')
254+
// Debounce signal updates
255+
const $debouncedString = morph($string, {
256+
[$$set]: debounce($string, 300)
257+
})
258+
// Lazy initialization
259+
const $lazyString = morph($string, {
260+
[$$get]() {
261+
this[$$set]('Lazy string')
262+
this[$$get] = this[$$source]
263+
return 'Lazy string'
264+
}
265+
})
266+
```
267+
268+
### `isSignal`
269+
270+
`isSignal` method checks if the value is a signal.
271+
272+
```ts
273+
import { isSignal, signal } from 'agera'
274+
275+
isSignal(signal(1)) // true
276+
```
277+
278+
## Differences from alien-signals
279+
280+
Key differences from [alien-signals](https://github.com/stackblitz/alien-signals):
281+
282+
- **Tree-shaking**. Agera is designed to be tree-shakable. Only the code you use is included in your bundle. Alien-signals is not well tree-shakable.
283+
- **Size**. Agera has a little bit smaller size for basic methods than alien-signals.
284+
- **Lifecycles**. Agera has lifecycles for signals. You can listen to signal activation and deactivation events.
285+
- **Modificated `effectScope`**. Agera has a possibility to put effect scope inside another effect scope. Also there is a possibility to create a lazy scope.

0 commit comments

Comments
 (0)