├── .editorconfig
├── .github
└── workflows
│ └── coverage.yml
├── .gitignore
├── .npmrc
├── .prettierrc.json
├── LICENSE
├── README.md
├── babel.config.cjs
├── jest.config.json
├── lerna.json
├── og.png
├── package.json
├── src
├── index.d.ts
└── index.js
├── tests
├── batch.test.js
├── box.test.js
├── expr.test.js
├── flow.test.js
├── graph.test.js
├── lib.js
├── perf
│ └── tree.js
├── sel.test.js
└── untrack.test.js
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | end_of_line = lf
7 | indent_style = space
8 | indent_size = 2
9 | quote_type = double
10 | insert_final_newline = true
11 | trim_trailing_whitespace = true
12 |
13 | [*.md]
14 | indent_style = space
15 | indent_size = 2
16 | max_line_length = off
17 | trim_trailing_whitespace = false
18 |
--------------------------------------------------------------------------------
/.github/workflows/coverage.yml:
--------------------------------------------------------------------------------
1 | name: coverage
2 | on: [push]
3 | jobs:
4 | run:
5 | name: node ${{ matrix.node }} on ${{ matrix.os }}
6 | runs-on: ${{ matrix.os }}
7 |
8 | strategy:
9 | fail-fast: false
10 | matrix:
11 | node: [18]
12 | os: [ubuntu-latest]
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 | - uses: actions/setup-node@v1
17 | with:
18 | node-version: ${{ matrix.node }}
19 | - run: node --version
20 | - run: yarn --version
21 | - run: yarn bootstrap
22 | - run: yarn test --coverage
23 | - uses: coverallsapp/github-action@master
24 | with:
25 | github-token: ${{ secrets.GITHUB_TOKEN }}
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # IDEs and editors
2 | .idea
3 | .project
4 | .classpath
5 | .c9/
6 | *.launch
7 | .settings/
8 | *.sublime-workspace
9 |
10 | # IDE - VSCode
11 | .vscode/*
12 | !.vscode/settings.json
13 | !.vscode/tasks.json
14 | !.vscode/launch.json
15 | !.vscode/extensions.json
16 |
17 | # OS files
18 | .DS_Store
19 | Thumbs.db
20 |
21 | # dependencies
22 | node_modules
23 |
24 | # logs
25 | npm-debug.log*
26 | yarn-debug.log*
27 | yarn-error.log*
28 | lerna-debug.log*
29 |
30 | # build
31 | build
32 | dist
33 |
34 | # jest
35 | /coverage
36 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": false,
3 | "tabWidth": 2,
4 | "useTabs": false,
5 | "semi": true
6 | }
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Slava Birch
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://www.npmjs.com/package/reactive-box) [](https://bundlephobia.com/result?p=reactive-box) [](https://coveralls.io/github/betula/reactive-box) [](./src/main.d.ts)
2 |
3 | Minimalistic, [fast](https://github.com/betula/reactive-box-performance), and highly efficient reactivity.
4 |
5 | Hi friends! Today I will tell you how I came to this.
6 |
7 | Redux has so many different functions, Mobx has mutable objects by default, Angular so heavy, Vue so strange, and other them so young :sweat_smile:
8 |
9 | These funny thoughts served as fuel for writing the minimal reaction core. So that everyone can make their own syntax for managing the state of the application in less than 100 lines of code :+1:
10 |
11 | It only three functions:
12 |
13 | + `box` - is the container for an immutable value.
14 | + `sel` - is the cached selector (or computed value) who will mark for recalculating If some of read inside boxes or selectors changed.
15 | + `expr` - is the expression who detects all boxes and selectors read inside and reacted If some of them changed.
16 |
17 | ```javascript
18 | import { box, sel, expr } from "reactive-box";
19 |
20 | const [get, set] = box(0);
21 |
22 | const [next] = sel(() => get() + 1);
23 |
24 | const [run, stop] = expr(
25 | () => `Counter: ${get()} (next value: ${next()})`,
26 | () => console.log(run())
27 | );
28 | console.log(run()); // console: "Counter 0 (next value: 1)"
29 |
30 | set(get() + 1); // console: "Counter 1 (next value: 2)"
31 | ```
32 |
33 | [Try It on RunKit!](https://runkit.com/betula/5fbf60565572d7001a76cd29)
34 |
35 | It is a basis for full feature reactive mathematic!
36 | For example that possible syntax to transcript previous javascript code:
37 |
38 | ```
39 | a` = 0 // create reactive value
40 | next` = a` + 1 // create new reactive value, dependent on the previous one
41 | expr = { "Counter: ${a`} (next value: ${next`})" } // create reactive expression
42 |
43 | // subscribe to expression dependencies were change and run It again
44 | expr: () => console.log(expr())
45 |
46 | // run the expression
47 | console.log(expr()) // message to console "Counter: 0 (next value: 1)"
48 |
49 | a = a` + 1 // here will be fired log to console again with new "Counter: 1 (next value: 2)" message, because a` was changed.
50 | ```
51 |
52 | 1. We create reactive `a`
53 | 2. We create reactive operation `a + 1`
54 | 3. We create reactive expression `"Counter: ${a} (next value: ${next})"`
55 | 4. We subscribe to change of `a` and `next` reactive dependencies
56 | 5. We run reactive expression
57 | 6. We are increasing the value of reactive `a` for demonstration subscriber reaction
58 |
59 | ### Atomic
60 |
61 | These are three basic elements necessary for creating data flow any difficulty.
62 |
63 | The first element is a reactive container for an immutable value. All reactions beginning from container change value reaction.
64 |
65 | The second one is the middle element. It uses all reactive containers as a source of values and returns the result of the expression. It's a transformer set of reactive values to a single one. The selector can be used as a reactive container in other selectors and expressions. It subscribes to change in any of the dependencies. And will recalculate the value if some of the dependency changed, but will propagate changes only if the return value changed.
66 |
67 | And the last one is a reaction subscriber. It provides the possibility to subscribe to change any set of reactive containers. It can be run again after the listener was called.
68 |
69 | ### Deep inside
70 |
71 | - It runs calculations synchronously.
72 |
73 | - [glitch](https://stackoverflow.com/questions/25139257/terminology-what-is-a-glitch-in-functional-reactive-programming-rx) free - your reactions will only be called when there is a consistent state for them to run on.
74 |
75 | - Possibility for modification everywhere: in expressions and selectors!
76 |
77 | ### In the real world
78 |
79 | Below we will talk about more high level abstraction, to the world of React and integration reactive-box into, for best possibilities together!
80 |
81 | Basic usage examples:
82 |
83 | - [Counter with Node.js on RunKit](https://runkit.com/betula/5fbde8473dd2b0001bb8f9be)
84 | - [Counter with React on CodeSandbox](https://codesandbox.io/s/reactive-box-counter-35bp9?hidenavigation=1&module=%2Fsrc%2FApp.tsx)
85 |
86 | It is minimal core for a big family of state managers' syntax. You can use the different syntax of your data flow on one big project, but the single core of your reactions provides the possibility for easy synchronization between them.
87 |
88 | #### Mobx like syntax example ([57 lines of reactive core](https://codesandbox.io/s/reactive-box-mobx-like-counter-nv8rq?hidenavigation=1&module=/src/App.tsx&file=/src/core.ts)):
89 |
90 | ```javascript
91 | import React from "react";
92 | import { computed, immutable, observe, shared } from "./core";
93 |
94 | class Counter {
95 | @immutable value = 0;
96 |
97 | @computed get next() {
98 | return this.value + 1;
99 | }
100 |
101 | increment = () => this.value += 1;
102 | decrement = () => this.value -= 1;
103 | }
104 |
105 | const App = observe(() => {
106 | const { value, next, increment, decrement } = shared(Counter);
107 |
108 | return (
109 |
110 | Counter: {value} (next value: {next})
111 |
112 | Prev
113 | Next
114 |
115 | );
116 | });
117 | ```
118 |
119 | [Try It on CodeSandbox](https://codesandbox.io/s/reactive-box-mobx-like-counter-nv8rq?hidenavigation=1&module=%2Fsrc%2FApp.tsx)
120 |
121 | #### Effector like syntax example ([76 lines of reactive core](https://codesandbox.io/s/reactive-box-store-nku88?hidenavigation=1&module=/src/App.tsx&file=/src/core.ts)):
122 |
123 | ```javascript
124 | import React from "react";
125 | import { action, store, selector, useState } from "./core";
126 |
127 | const increment = action();
128 | const decrement = action();
129 |
130 | const counter = store(0)
131 | .on(increment, (state) => state + 1)
132 | .on(decrement, (state) => state - 1);
133 |
134 | const next = selector(() => counter.get() + 1);
135 |
136 | const App = () => {
137 | const value = useState(counter);
138 | const nextValue = useState(next);
139 |
140 | return (
141 |
142 | Counter: {value} (next value: {nextValue})
143 |
144 | Prev
145 | Next
146 |
147 | );
148 | }
149 | ```
150 |
151 | [Try It on CodeSandbox](https://codesandbox.io/s/reactive-box-store-nku88?hidenavigation=1&module=%2Fsrc%2FApp.tsx)
152 |
153 | ### More examples
154 |
155 | - [Simple model with React on CodeSandbox](https://codesandbox.io/s/reactive-box-model-yopk5?hidenavigation=1&module=%2Fsrc%2FApp.tsx)
156 | - [Mobx like todo-mvc with React on CodeSandbox](https://codesandbox.io/s/reactive-box-todos-u5q3e?hidenavigation=1&module=%2Fsrc%2Fshared%2Ftodos.ts)
157 | - [Realar state manager](https://github.com/betula/realar)
158 |
159 | ### Articles about
160 |
161 | - [664 Bytes reactivity on dev.to](https://dev.to/betula/reactive-box-1hm5)
162 |
163 | You can easily make your own state manager system or another observable and reactive data flow. It's so funny :blush:
164 |
165 | ### How to install
166 |
167 | ```bash
168 | npm i reactive-box
169 | ```
170 |
171 | Thanks for your time!
172 |
--------------------------------------------------------------------------------
/babel.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | ['@babel/preset-env', {targets: {node: 'current'}}],
4 | '@babel/preset-typescript',
5 | ],
6 | };
--------------------------------------------------------------------------------
/jest.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "transform": {
3 | "^.+\\.js$": "babel-jest"
4 | },
5 | "testMatch": [
6 | "/**/*.test.js"
7 | ],
8 | "modulePathIgnorePatterns": [
9 | "/node_modules/"
10 | ],
11 | "coveragePathIgnorePatterns": [
12 | "/tests"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/lerna.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.8.0",
3 | "packages": [
4 | "."
5 | ],
6 | "hoist": false,
7 | "npmClient": "npm",
8 | "npmClientArgs": [
9 | "--no-package-lock"
10 | ],
11 | "loglevel": "verbose"
12 | }
13 |
--------------------------------------------------------------------------------
/og.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/re-js/reactive-box/9e5123afea18d606169f0ee8032759072f91e748/og.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reactive-box",
3 | "version": "0.9.0",
4 | "description": "Minimalistic, fast, and highly efficient reactivity",
5 | "types": "src/index.d.ts",
6 | "main": "dist/reactive-box.js",
7 | "module": "dist/reactive-box.module.js",
8 | "umd:main": "dist/reactive-box.umd.js",
9 | "source": "src/index.js",
10 | "files": [
11 | "src",
12 | "dist"
13 | ],
14 | "scripts": {
15 | "bootstrap": "yarn && yarn build",
16 | "publish": "lerna publish",
17 | "test": "jest",
18 | "format": "prettier -w src/** tests/**",
19 | "test:perf:tree": "node ./tests/perf/tree.js",
20 | "clean": "rm -rf dist",
21 | "build": "yarn clean && microbundle build --raw --target web --generateTypes false",
22 | "dev": "microbundle watch --raw --format cjs"
23 | },
24 | "devDependencies": {
25 | "@babel/preset-env": "7.23.5",
26 | "@babel/preset-typescript": "7.23.3",
27 | "jest": "29.7.0",
28 | "lerna": "3.22.1",
29 | "microbundle": "0.15.1",
30 | "prettier": "2.2.0",
31 | "simple-statistics": "7.3.2"
32 | },
33 | "sideEffects": false,
34 | "publishConfig": {
35 | "access": "public"
36 | },
37 | "repository": {
38 | "type": "git",
39 | "url": "git+ssh://git@github.com/re-js/reactive-box.git"
40 | },
41 | "keywords": [
42 | "reactivity",
43 | "reactive",
44 | "box",
45 | "minimal",
46 | "minimalistic",
47 | "lightweight",
48 | "observer",
49 | "observable",
50 | "reaction",
51 | "performance"
52 | ],
53 | "author": "Slava Birch ",
54 | "license": "MIT",
55 | "bugs": {
56 | "url": "https://github.com/re-js/reactive-box/issues"
57 | },
58 | "homepage": "https://github.com/re-js/reactive-box#readme",
59 | "exports": {
60 | ".": {
61 | "types": "./src/index.d.ts",
62 | "browser": "./dist/reactive-box.module.js",
63 | "umd": "./dist/reactive-box.umd.js",
64 | "import": "./dist/reactive-box.mjs",
65 | "require": "./dist/reactive-box.js"
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/index.d.ts:
--------------------------------------------------------------------------------
1 | declare function box(
2 | initialValue?: T,
3 | onChange?: (currentValue: T, prevValue: T) => void,
4 | comparer?: (value: T, nextValue: T) => boolean
5 | ): [() => T, (nextValue: T) => void];
6 |
7 | declare function sel any>(
8 | body: T,
9 | comparer?: (value: T, nextValue: T) => boolean
10 | ): [T, () => void];
11 |
12 | declare function expr void>(body: T): [T, () => void];
13 | declare function expr any>(
14 | body: T,
15 | updater: () => void
16 | ): [T, () => void];
17 |
18 | declare function batch(): () => void;
19 | declare function untrack(): () => void;
20 |
21 | export { box, sel, expr, batch, untrack };
22 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 0: rels or (sync for expr)
3 | * 1: deps
4 | * 2: level
5 | * 3: (valid for sel)
6 | * 4: (recalc for sel)
7 | */
8 | let context_node;
9 | let context_untrack;
10 | let level_nodes;
11 | let stack_nodes = new Map();
12 | let level_current;
13 | let batch_nodes;
14 |
15 | const reactions_loop_limit = 1000000;
16 |
17 | // node: sel or expr node
18 | // type: 0 - rels, 1 - deps
19 | const free = (node, type) => {
20 | node[type].forEach((target) => target[1 - type].delete(node));
21 | node[type].clear();
22 | };
23 |
24 | // node: box or sel node
25 | const read = (node) => {
26 | if (context_node && !context_untrack) {
27 | context_node[1].add(node);
28 | node[0].add(context_node);
29 | }
30 | };
31 |
32 | const calculate_level = (node) => {
33 | if (context_node) {
34 | if (context_node[2] < node[2] + 1) {
35 | context_node[2] = node[2] + 1;
36 | }
37 | }
38 | };
39 |
40 | const node_expand = (node) =>
41 | node[0].forEach((rel) => {
42 | const stack_node_h = stack_nodes.get(rel);
43 | if (stack_node_h) {
44 | if (stack_node_h[0]) stack_node_h[1] = 1;
45 | return;
46 | }
47 | // [, , ]
48 | stack_nodes.set(rel, [0, 0, 0]);
49 |
50 | let level = rel[2];
51 | let list = level_nodes.get(level);
52 | !list && level_nodes.set(level, (list = new Set()));
53 |
54 | if (!level_current || level_current > level) level_current = level;
55 |
56 | list.add(rel);
57 | });
58 |
59 | const throw_infinity_reactions = () => {
60 | throw new Error("Infinity reactions loop");
61 | };
62 |
63 | const write = (box_node, is_array) => {
64 | if (batch_nodes)
65 | return is_array
66 | ? box_node.forEach(batch_nodes.add.bind(batch_nodes))
67 | : batch_nodes.add(box_node);
68 |
69 | const stack_level_current = level_current;
70 | const stack_level_nodes = level_nodes;
71 |
72 | level_current = 0;
73 | level_nodes = new Map();
74 |
75 | is_array ? box_node.forEach(node_expand) : node_expand(box_node);
76 |
77 | try {
78 | let limit = reactions_loop_limit;
79 |
80 | while (level_current) {
81 | let nodes = level_nodes.get(level_current);
82 |
83 | if (!nodes.size) {
84 | level_current = 0;
85 | level_nodes.forEach(
86 | (list, level) =>
87 | list.size &&
88 | (!level_current || level_current > level) &&
89 | (level_current = level)
90 | );
91 | }
92 | if (!level_current) break;
93 | nodes = level_nodes.get(level_current);
94 |
95 | const iter = nodes.values();
96 | const node = iter.next().value;
97 |
98 | const stack_node_h = stack_nodes.get(node);
99 | stack_node_h[0] = 1;
100 |
101 | if (stack_node_h[2]) nodes.delete(node);
102 | else
103 | do {
104 | stack_node_h[1] = 0;
105 | let expr, sel;
106 |
107 | if (node.length === 3) expr = 1;
108 | else {
109 | if (node[0].size) sel = 1;
110 | else node[3] = 0;
111 | }
112 |
113 | free(node, 1);
114 | nodes.delete(node);
115 |
116 | if (expr) node[0]();
117 | if (sel) {
118 | if (node[4]()) {
119 | node_expand(node);
120 | free(node, 0);
121 | }
122 | }
123 | if (stack_node_h[2]) {
124 | free(node, 1);
125 | sel && free(node, 0);
126 | break;
127 | }
128 | if (!--limit) throw_infinity_reactions();
129 | } while (stack_node_h[1]);
130 |
131 | stack_nodes.delete(node);
132 | }
133 | } finally {
134 | level_current = stack_level_current;
135 | level_nodes = stack_level_nodes;
136 | }
137 | };
138 |
139 | const batch = () => {
140 | const stack = batch_nodes;
141 | batch_nodes = new Set();
142 |
143 | return () => {
144 | const nodes = batch_nodes;
145 | batch_nodes = stack;
146 | nodes.size && write(nodes, 1);
147 | };
148 | };
149 |
150 | const untrack = () => {
151 | const stack = context_untrack;
152 | context_untrack = 1;
153 | return () => (context_untrack = stack);
154 | };
155 |
156 | const box = (value, is_equals = Object.is) => {
157 | // rels, _, level
158 | const box_node = [new Set(), 0, 0];
159 | return [
160 | () => (read(box_node), calculate_level(box_node), value),
161 | (next_value) => {
162 | if (!is_equals(value, next_value)) {
163 | value = next_value;
164 | write(box_node);
165 | }
166 | },
167 | ];
168 | };
169 |
170 | const sel = (body, is_equals = Object.is) => {
171 | let cache;
172 | let last_context;
173 | const run = () => {
174 | const stack_context_node = context_node;
175 | const stack_untrack = context_untrack;
176 | context_untrack = 0;
177 |
178 | const h = stack_nodes.get(sel_node);
179 | if (h && h[2]) h[2] = 0;
180 |
181 | context_node = sel_node;
182 | context_node[2] = 0; // clear level
183 |
184 | try {
185 | return body.call(last_context, cache);
186 | } finally {
187 | context_node = stack_context_node;
188 | context_untrack = stack_untrack;
189 | }
190 | };
191 | // rels, deps, level, is_cached, checker
192 | const sel_node = [
193 | new Set(),
194 | new Set(),
195 | 0,
196 | 0,
197 | () => {
198 | let next = run();
199 | return !sel_node[3] || is_equals(cache, next)
200 | ? false
201 | : ((cache = next), true);
202 | },
203 | ];
204 |
205 | return [
206 | function () {
207 | last_context = this;
208 | read(sel_node);
209 | if (!sel_node[3]) {
210 | cache = run();
211 | sel_node[3] = 1;
212 | }
213 | calculate_level(sel_node);
214 | return cache;
215 | },
216 | () => {
217 | free(sel_node, 1);
218 | free(sel_node, 0);
219 | sel_node[3] = cache = 0;
220 | last_context = null;
221 | stack_nodes.has(sel_node) && (stack_nodes.get(sel_node)[2] = 1);
222 | },
223 | ];
224 | };
225 |
226 | const expr = (body, sync) => {
227 | let last_context;
228 | if (!sync) sync = () => run.call(last_context);
229 |
230 | // sync, deps, level
231 | const expr_node = [sync, new Set(), 0];
232 |
233 | function run() {
234 | const body_run = () => body.apply((last_context = this), arguments);
235 |
236 | const stack_context_node = context_node;
237 | const stack_untrack = context_untrack;
238 | context_untrack = 0;
239 |
240 | context_node = expr_node;
241 | context_node[2] = 0; // clear level
242 |
243 | let result;
244 | let h = stack_nodes.get(expr_node);
245 | let is_entry;
246 | // [, , ]
247 | if (!h) {
248 | stack_nodes.set(expr_node, (is_entry = h = [1, 0, 0]));
249 | } else if (h[2]) h[2] = 0;
250 |
251 | try {
252 | if (is_entry) {
253 | let limit = reactions_loop_limit;
254 | do {
255 | expr_node[1].size && free(expr_node, 1);
256 | h[1] = 0;
257 | result = body_run();
258 | if (!--limit) throw_infinity_reactions();
259 | } while (h[1] && !h[2]);
260 |
261 | stack_nodes.delete(expr_node);
262 | h[2] && free(expr_node, 1);
263 | } else {
264 | result = body_run();
265 | }
266 | } finally {
267 | context_node = stack_context_node;
268 | context_untrack = stack_untrack;
269 | }
270 | return result;
271 | }
272 |
273 | return [
274 | run,
275 | () => {
276 | free(expr_node, 1);
277 | last_context = null;
278 | stack_nodes.has(expr_node) && (stack_nodes.get(expr_node)[2] = 1);
279 | },
280 | ];
281 | };
282 |
283 | export { box, sel, expr, batch, untrack };
284 |
--------------------------------------------------------------------------------
/tests/batch.test.js:
--------------------------------------------------------------------------------
1 | import { mut, run, comp, batch } from "./lib";
2 |
3 | describe("Batch", () => {
4 | test("should work batch", () => {
5 | const spy = jest.fn();
6 | const a = mut(0);
7 | const b = mut(0);
8 | const c = comp(() => a.val * 10 + b.val);
9 | run(() => spy(c.val));
10 |
11 | expect(spy).toHaveBeenNthCalledWith(1, 0);
12 | expect(spy).toHaveBeenCalledTimes(1);
13 |
14 | a.val = 1;
15 | expect(spy).toHaveBeenNthCalledWith(2, 10);
16 | expect(spy).toHaveBeenCalledTimes(2);
17 |
18 | b.val = 1;
19 | expect(spy).toHaveBeenNthCalledWith(3, 11);
20 | expect(spy).toHaveBeenCalledTimes(3);
21 |
22 | const commit = batch();
23 | a.val = 2;
24 | b.val = 2;
25 | expect(spy).toHaveBeenCalledTimes(3);
26 | commit();
27 | expect(spy).toHaveBeenCalledTimes(4);
28 | expect(spy).toHaveBeenNthCalledWith(4, 22);
29 | });
30 |
31 | test("should work nested batch", () => {
32 | const spy = jest.fn();
33 | const a = mut(0);
34 | const b = mut(0);
35 | const c = comp(() => a.val * 10 + b.val);
36 | run(() => spy(c.val));
37 |
38 | expect(spy).toHaveBeenNthCalledWith(1, 0);
39 | expect(spy).toHaveBeenCalledTimes(1);
40 |
41 | const commit = batch();
42 |
43 | a.val = 2;
44 | const nested = batch();
45 | b.val = 2;
46 | a.val = 3;
47 | nested();
48 |
49 | expect(spy).toHaveBeenCalledTimes(1);
50 | commit();
51 | expect(spy).toHaveBeenCalledTimes(2);
52 | expect(spy).toHaveBeenNthCalledWith(2, 32);
53 | });
54 | });
55 |
--------------------------------------------------------------------------------
/tests/box.test.js:
--------------------------------------------------------------------------------
1 | import { mut, run } from "./lib";
2 |
3 | describe("Box", () => {
4 | test("should work box", () => {
5 | const spy = jest.fn();
6 | const a = mut(0);
7 | run(() => spy(a.val));
8 |
9 | expect(spy).toHaveBeenCalledTimes(1);
10 | expect(spy).toHaveBeenLastCalledWith(0);
11 | a.val = 1;
12 | expect(spy).toHaveBeenCalledTimes(2);
13 | expect(spy).toHaveBeenLastCalledWith(1);
14 | a.val = 1;
15 | expect(spy).toHaveBeenCalledTimes(2);
16 | });
17 |
18 | test("should work custom comparer", () => {
19 | const spy = jest.fn();
20 | const a = mut(NaN, (val, next) => val === next);
21 | run(() => spy(a.val));
22 |
23 | expect(spy).toBeCalledTimes(1);
24 | a.val = NaN;
25 | expect(spy).toBeCalledTimes(2);
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/tests/expr.test.js:
--------------------------------------------------------------------------------
1 | import { mut, runer, expr, run } from "./lib";
2 |
3 | describe("Expr", () => {
4 | test("(sync on each box change with one box) and (expr return value)", () => {
5 | const spy = jest.fn();
6 | const a = mut(1);
7 | const e = runer(() => a.val, spy);
8 |
9 | expect(e()).toBe(1);
10 | expect(spy).toBeCalledTimes(0);
11 |
12 | a.val = 2;
13 | expect(spy).toBeCalledTimes(1);
14 | expect(e()).toBe(2);
15 | });
16 |
17 | test("(rerun on each box change with two boxes) and (non reaction before first run)", () => {
18 | const spy = jest.fn();
19 | const a = mut(1);
20 | const b = mut(2);
21 | const e = runer(() => {
22 | spy(a.val, b.val);
23 | });
24 | a.val = 10;
25 | b.val = 20;
26 | expect(spy).toBeCalledTimes(0);
27 | e();
28 | expect(spy).toHaveBeenCalledTimes(1);
29 | expect(spy).toHaveBeenNthCalledWith(1, 10, 20);
30 | a.val = 11;
31 | expect(spy).toHaveBeenCalledTimes(2);
32 | expect(spy).toHaveBeenNthCalledWith(2, 11, 20);
33 | b.val = 21;
34 | expect(spy).toHaveBeenCalledTimes(3);
35 | expect(spy).toHaveBeenNthCalledWith(3, 11, 21);
36 | });
37 |
38 | test("exclude from graph before run if it necessary", () => {
39 | const spy = jest.fn();
40 | let f = 1;
41 | const a = mut(1);
42 | const b = mut(1);
43 | const r = runer(() => (f ? a.val : b.val), spy);
44 | r();
45 | f = 0;
46 | r();
47 | a.val = 2;
48 | expect(spy).toBeCalledTimes(0);
49 | });
50 |
51 | test("exclude from graph on free call", () => {
52 | const spy = jest.fn();
53 | const a = mut(1);
54 | const e = expr(() => a.val, spy);
55 | e[0]();
56 | e[1]();
57 | a.val = 2;
58 | expect(spy).toBeCalledTimes(0);
59 | e[0]();
60 | a.val = 1;
61 | expect(spy).toBeCalledTimes(1);
62 | });
63 |
64 | test("get run context from prev run call", () => {
65 | const spy = jest.fn();
66 | const a = mut(0);
67 | const e = runer(function () {
68 | spy(this, a.val);
69 | });
70 | e.call(["a"]);
71 | expect(spy).toHaveBeenLastCalledWith(["a"], 0);
72 | a.val = 1;
73 | expect(spy).toHaveBeenLastCalledWith(["a"], 1);
74 | e.call(["b"]);
75 | expect(spy).toHaveBeenLastCalledWith(["b"], 1);
76 | a.val = 0;
77 | expect(spy).toHaveBeenLastCalledWith(["b"], 0);
78 | });
79 |
80 | test("stop should work correctly in another expr", () => {
81 | const spy = jest.fn();
82 | const a = mut(0);
83 |
84 | run(
85 | () => a.val,
86 | () => s2()
87 | );
88 | const [r2, s2] = expr(() => a.val, spy);
89 | r2();
90 | run(
91 | () => a.val,
92 | () => s2()
93 | );
94 |
95 | a.val = 1;
96 | expect(spy).toBeCalledTimes(0);
97 | });
98 |
99 | test("stop should work correctly in self", () => {
100 | const spy = jest.fn();
101 | const a = mut(0);
102 |
103 | const [r1, s1] = expr(() => {
104 | if (a.val) {
105 | s1();
106 | }
107 | spy(a.val);
108 | });
109 | r1();
110 | expect(spy).toBeCalledTimes(1);
111 | a.val = 1;
112 | expect(spy).toBeCalledTimes(2);
113 |
114 | a.val = 0;
115 | expect(spy).toBeCalledTimes(2);
116 | });
117 |
118 | test("stop and run again should work correctly in self", () => {
119 | const spy = jest.fn();
120 | const a = mut(0);
121 |
122 | const [r1, s1] = expr(() => {
123 | spy(a.val);
124 | if (a.val === 1) {
125 | s1();
126 | a.val = 0;
127 | r1();
128 | }
129 | });
130 | r1();
131 | expect(spy).toBeCalledTimes(1);
132 | a.val = 1;
133 | expect(spy).toHaveBeenNthCalledWith(2, 1);
134 | expect(spy).toHaveBeenNthCalledWith(3, 0);
135 | expect(spy).toBeCalledTimes(3);
136 | expect(a.val).toBe(0);
137 | a.val = 2;
138 | expect(spy).toBeCalledTimes(4);
139 | expect(spy).toHaveBeenNthCalledWith(4, 2);
140 | });
141 |
142 | test("stop in first iteration", () => {
143 | const spy = jest.fn();
144 | const spy_2 = jest.fn();
145 | const b = mut(1);
146 |
147 | const body = () => {
148 | b.val += b.val;
149 | b.val += b.val;
150 | b.val += b.val;
151 | s();
152 | spy_2();
153 | };
154 | run(() => spy(b.val));
155 |
156 | const [r, s] = expr(body);
157 | r();
158 |
159 | expect(spy_2).toHaveBeenCalledTimes(1);
160 | expect(spy).toHaveBeenNthCalledWith(1, 1);
161 | expect(spy).toHaveBeenNthCalledWith(2, 2);
162 | expect(spy).toHaveBeenNthCalledWith(3, 4);
163 | expect(spy).toHaveBeenNthCalledWith(4, 8);
164 | expect(spy).toHaveBeenCalledTimes(4);
165 | });
166 | });
167 |
--------------------------------------------------------------------------------
/tests/flow.test.js:
--------------------------------------------------------------------------------
1 | import { mut, run } from "./lib";
2 |
3 | describe("Flow", () => {
4 | test("should work flow", () => {
5 | const spy = jest.fn();
6 |
7 | const a = mut(0);
8 | const c = mut(1);
9 |
10 | // flow section
11 | const b = mut(0);
12 | run(() => {
13 | b.val = (c.val > 1) ? a.val : 0;
14 | })
15 | // end flow section
16 |
17 | // TODO: Whats happends with order of execution if will change depth but not a value?
18 |
19 | run(() => {
20 | spy(b.val, a.val);
21 | });
22 |
23 | expect(spy).toHaveBeenCalledTimes(1);
24 | expect(spy).toHaveBeenLastCalledWith(0, 0);
25 | c.val = 2;
26 |
27 | a.val = 1;
28 | expect(spy).toHaveBeenNthCalledWith(2, 0, 1); // TODO: broken execution order
29 | expect(spy).toHaveBeenNthCalledWith(3, 1, 1);
30 |
31 | // expect(spy).toHaveBeenCalledTimes(2);
32 | });
33 | });
34 |
--------------------------------------------------------------------------------
/tests/graph.test.js:
--------------------------------------------------------------------------------
1 | import { mut, run, comp, selec, runer, expr } from "./lib";
2 |
3 | describe("Graph", () => {
4 | test("expr run only once for three deep sel", () => {
5 | const spy = jest.fn();
6 | const m = mut(1);
7 | const a = comp(() => m.val);
8 | const b = comp(() => a.val);
9 | const c = comp(() => b.val);
10 | run(() => c.val, spy);
11 |
12 | expect(spy).toBeCalledTimes(0);
13 | m.val = 2;
14 | expect(spy).toBeCalledTimes(1);
15 | });
16 |
17 | test("infinity loop error for expr cycle", () => {
18 | const m = mut(0);
19 | const r = runer(() => {
20 | m.val += 1;
21 | });
22 | expect(r).toThrow("Infinity reactions loop");
23 | });
24 |
25 | test("infinity loop error for write updates", () => {
26 | const m = mut(0);
27 | const b = mut(0);
28 | run(() => {
29 | if (b.val) m.val += 1;
30 | });
31 | expect(() => ++b.val).toThrow("Infinity reactions loop");
32 | });
33 |
34 | test("two expr with two sels and one shared and second box change in first expr", () => {
35 | const spy = jest.fn();
36 | const m1 = mut(1);
37 | const m2 = mut(5);
38 | const s1 = selec(() => m1.val);
39 | const s2 = selec(() => m2.val);
40 | const r1 = runer(() => {
41 | m2.val = s1() + 1;
42 | });
43 | const r2 = runer(() => {
44 | s1();
45 | spy(s2());
46 | });
47 |
48 | r2();
49 | r1();
50 | expect(spy).toHaveBeenCalledTimes(2);
51 | expect(spy).toHaveBeenNthCalledWith(2, 2);
52 | m1.val = 2;
53 | expect(spy).toHaveBeenCalledTimes(3);
54 | expect(spy).toHaveBeenNthCalledWith(3, 3);
55 | });
56 |
57 | test("write and read selector in write phase", () => {
58 | const spy = jest.fn();
59 | const a = mut(0);
60 | const b = mut(1);
61 | const s_1 = comp(() => b.val + 1);
62 | const s_2 = comp(() => s_1.val + 1);
63 | const s_3 = comp(() => s_2.val + 1);
64 |
65 | const e = expr(() => {
66 | if (a.val > 0) {
67 | b.val = a.val + 1;
68 | spy(s_3.val);
69 | }
70 | });
71 | e[0]();
72 |
73 | expect(s_3.val).toBe(4);
74 |
75 | a.val = 1;
76 |
77 | expect(spy).toHaveBeenNthCalledWith(1, 5);
78 | expect(spy).toBeCalledTimes(1);
79 | });
80 |
81 | test("deep struct with modify", () => {
82 | const spy1 = jest.fn();
83 | const spy2 = jest.fn();
84 | const a = mut(0);
85 | const b = mut(0);
86 |
87 | const n1 = comp(() => a.val + 1);
88 | const n2 = comp(() => n1.val + 1);
89 | const r1 = comp(() => a.val + "-" + n2.val);
90 |
91 | run(() => {
92 | spy1(r1.val);
93 | if (a.val === 1) {
94 | a.val = 2;
95 | b.val = 1;
96 | }
97 | });
98 | const r2 = comp(() => {
99 | return r1.val + "-" + b.val;
100 | });
101 | run(() => {
102 | spy2(r2.val);
103 | });
104 |
105 | expect(spy1).toHaveBeenNthCalledWith(1, "0-2");
106 | expect(spy1).toBeCalledTimes(1);
107 | expect(spy2).toHaveBeenNthCalledWith(1, "0-2-0");
108 | expect(spy2).toBeCalledTimes(1);
109 |
110 | a.val = 1;
111 |
112 | expect(spy1).toHaveBeenNthCalledWith(2, "1-3");
113 | expect(spy1).toHaveBeenNthCalledWith(3, "2-4");
114 | expect(spy1).toBeCalledTimes(3);
115 | expect(spy2).toHaveBeenNthCalledWith(2, "2-4-1");
116 | expect(spy2).toBeCalledTimes(2);
117 | });
118 |
119 | test("binary tree", () => {
120 | const spy = jest.fn();
121 | const a1 = mut(0);
122 | const a2 = mut(0);
123 | const a3 = mut(0);
124 | const a4 = mut(0);
125 | const c1 = comp(() => a1.val + a2.val);
126 | const c2 = comp(() => a3.val + a4.val);
127 | const c3 = comp(() => c1.val + c2.val);
128 |
129 | run(() => spy(c3.val));
130 | expect(spy).toHaveBeenNthCalledWith(1, 0);
131 | a1.val = 1;
132 | expect(spy).toHaveBeenNthCalledWith(2, 1);
133 | a2.val = 1;
134 | expect(spy).toHaveBeenNthCalledWith(3, 2);
135 | a3.val = 1;
136 | expect(spy).toHaveBeenNthCalledWith(4, 3);
137 | a4.val = 1;
138 | expect(spy).toHaveBeenNthCalledWith(5, 4);
139 | });
140 | });
141 |
--------------------------------------------------------------------------------
/tests/lib.js:
--------------------------------------------------------------------------------
1 | import { box, expr, sel, batch, untrack } from "..";
2 |
3 | module.exports.batch = batch;
4 | module.exports.untrack = untrack;
5 |
6 | module.exports.box = box;
7 | module.exports.mut = (value, comparer) => {
8 | const b = box(value, comparer);
9 | const obj = {};
10 | Object.defineProperty(obj, "val", {
11 | get: b[0],
12 | set: b[1],
13 | });
14 | return obj;
15 | };
16 |
17 | module.exports.expr = expr;
18 | module.exports.runer = (body, sync) => expr(body, sync)[0];
19 | module.exports.run = (body, sync) => expr(body, sync)[0]();
20 |
21 | module.exports.on = (body, fn) => {
22 | const e = expr(body, () => fn(e[0]()));
23 | e[0]();
24 | }
25 | module.exports.sync = (body, fn) => {
26 | const e = expr(body, () => fn(e[0]()));
27 | fn(e[0]());
28 | }
29 |
30 | module.exports.sel = sel;
31 | module.exports.selec = (body, comparer) => sel(body, comparer)[0];
32 | module.exports.comp = (body, comparer) => {
33 | const s = sel(body, comparer);
34 | const obj = {};
35 | Object.defineProperty(obj, "val", {
36 | get: s[0],
37 | });
38 | return obj;
39 | };
40 |
41 | module.exports.delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
42 |
--------------------------------------------------------------------------------
/tests/perf/tree.js:
--------------------------------------------------------------------------------
1 | const { performance } = require("perf_hooks");
2 | const stat = require("simple-statistics");
3 | const { box, sel, expr } = require("../..");
4 |
5 | const deep = 8;
6 | let init_times = [];
7 | let times = [];
8 |
9 | let has_last_answer = 0;
10 | let last_anwser = 0;
11 |
12 | for (let i = 0; i < 10; i++) {
13 | const [iter_init_time, iter_times, iter_anwser] = tree(deep, 10);
14 | init_times.push(iter_init_time);
15 | times.push(...iter_times);
16 |
17 | if (!has_last_answer) {
18 | last_anwser = iter_anwser;
19 | has_last_answer = 1;
20 | } else {
21 | if (iter_anwser !== last_anwser) throw "Results not eqaul";
22 | }
23 | }
24 |
25 | const memory_used = process.memoryUsage();
26 |
27 | console.log("Boxes:", 2 << deep);
28 | console.log("Selectors:", (2 << deep) - 2);
29 |
30 | console.log("Inits time (ms):");
31 | console.log("Mean:", stat.mean(init_times));
32 | console.log("Median:", stat.median(init_times));
33 | console.log("Harmonic mean:", stat.harmonicMean(init_times));
34 | console.log("Geometric mean:", stat.geometricMean(init_times));
35 |
36 | console.log("Ops time (ms):");
37 | console.log("Mean:", stat.mean(times));
38 | console.log("Median:", stat.median(times));
39 | console.log("Harmonic mean:", stat.harmonicMean(times));
40 | console.log("Geometric mean:", stat.geometricMean(times));
41 |
42 | console.log("Memory used (MB):");
43 | for (let key in memory_used) {
44 | console.log(
45 | `${key}: ${Math.round((memory_used[key] / 1024 / 1024) * 100) / 100}`
46 | );
47 | }
48 |
49 | function tree(deep, iters) {
50 | const boxes = [];
51 | let answer;
52 | let time, init_time;
53 | const times = [];
54 |
55 | function level(deep) {
56 | if (deep === 0) {
57 | const x = box(0);
58 | boxes.push(x);
59 | return x;
60 | }
61 |
62 | const a = level(deep - 1);
63 | const b = level(deep - 1);
64 | return sel(() => a[0]() + b[0]());
65 | }
66 |
67 | function make(deep) {
68 | const a = level(deep);
69 | const b = level(deep);
70 | expr(() => (answer = a[0]() + b[0]()))[0]();
71 | }
72 |
73 | function init(deep) {
74 | init_time = performance.now();
75 | make(deep);
76 | init_time = performance.now() - init_time;
77 | }
78 |
79 | function op(ind) {
80 | let total = 0,
81 | i = 0,
82 | len = boxes.length;
83 |
84 | time = performance.now();
85 | for (; i < len; i++) {
86 | boxes[i][1](ind + 1);
87 | total += answer;
88 | }
89 | time = performance.now() - time;
90 | times.push(time);
91 |
92 | // console.log(`Op${ind + 1}:`, total, '\ttime:', time);
93 | }
94 |
95 | init(deep);
96 | for (let i = 0; i < iters; i++) op(i);
97 |
98 | return [init_time, times, answer];
99 | }
100 |
--------------------------------------------------------------------------------
/tests/sel.test.js:
--------------------------------------------------------------------------------
1 | import { mut, selec, sel, comp, run, runer, sync, on } from "./lib";
2 |
3 | describe("Sel", () => {
4 | test("sel run only once on each box change with one box", () => {
5 | const spy = jest.fn();
6 | const a = mut(1);
7 | const s = selec(() => (spy(), a.val));
8 |
9 | expect(spy).toBeCalledTimes(0);
10 |
11 | expect(s()).toBe(1);
12 | expect(spy).toBeCalledTimes(1);
13 | expect(s()).toBe(1);
14 | expect(spy).toBeCalledTimes(1);
15 |
16 | a.val = 2;
17 | expect(s()).toBe(2);
18 | expect(spy).toBeCalledTimes(2);
19 | expect(s()).toBe(2);
20 | expect(spy).toBeCalledTimes(2);
21 | });
22 |
23 | test("sel run only once on leave and comback to graph", () => {
24 | const spy = jest.fn();
25 | const a = mut(1);
26 | const s = selec(() => (spy(a.val), a.val));
27 | const b = mut(0);
28 | run(() => {
29 | if (b.val === 0) s();
30 | });
31 |
32 | expect(spy).toBeCalledTimes(1);
33 | expect(spy).toHaveBeenLastCalledWith(1);
34 | expect(s()).toBe(1);
35 | b.val = 1;
36 | b.val = 0;
37 | expect(s()).toBe(1);
38 | expect(spy).toBeCalledTimes(1);
39 | });
40 |
41 | test("should work custom comparer", () => {
42 | const spy = jest.fn();
43 | const a = mut(0);
44 | const s = selec(
45 | () => (a.val, NaN),
46 | (val, next) => val === next
47 | );
48 | run(() => spy(s()));
49 |
50 | expect(spy).toBeCalledTimes(1);
51 | a.val = 1;
52 | expect(spy).toBeCalledTimes(2);
53 | });
54 |
55 | test("should update cache only if comparer return false", () => {
56 | const d1 = { a: 0 };
57 | const d2 = { a: 0 };
58 | const d3 = { a: 1 };
59 | const spy = jest.fn();
60 | const a = mut(d1);
61 | const s = selec(
62 | () => a.val,
63 | (val, next) => val.a === next.a
64 | );
65 | run(() => spy(s()));
66 |
67 | expect(spy).toBeCalledTimes(1);
68 | a.val = d2;
69 | expect(s()).not.toBe(d2);
70 | expect(spy).toBeCalledTimes(1);
71 | a.val = d3;
72 | expect(spy).toBeCalledTimes(2);
73 | expect(s()).toBe(d3);
74 | });
75 |
76 | test("sel should exclude from graph and invalidate after free", () => {
77 | const spy = jest.fn();
78 | const spy1 = jest.fn();
79 | const a = mut(1);
80 | const s = sel(() => (spy(), a.val));
81 |
82 | run(() => spy1(s[0]()));
83 | expect(spy).toBeCalledTimes(1);
84 | expect(spy1).toBeCalledTimes(1);
85 | s[1]();
86 | s[0]();
87 | expect(spy).toBeCalledTimes(2);
88 |
89 | a.val = 2;
90 | expect(spy1).toBeCalledTimes(1);
91 | });
92 |
93 | test("sel should pass this context", () => {
94 | const spy = jest.fn();
95 | const s = selec(function () {
96 | spy(this);
97 | });
98 | s.call(["a"]);
99 | expect(spy).toBeCalledWith(["a"]);
100 | });
101 |
102 | test("sel should propogate change only if return value changed", () => {
103 | const spy = jest.fn();
104 | const a = mut("a");
105 | const s = selec(() => a.val[0]);
106 | run(() => spy(s()));
107 |
108 | expect(spy).toBeCalledTimes(1);
109 | expect(spy).toBeCalledWith("a");
110 | a.val += "b";
111 | expect(spy).toBeCalledTimes(1);
112 | a.val = "ba";
113 | expect(spy).toBeCalledTimes(2);
114 | expect(spy).toHaveBeenLastCalledWith("b");
115 | });
116 |
117 | test("should save context for sel recalc from prev call", () => {
118 | const spy = jest.fn();
119 | const a = mut(0);
120 | const s = selec(function () {
121 | spy(this, a.val);
122 | });
123 | const e = runer(function () {
124 | s.call(this);
125 | });
126 | e.call(["a"]);
127 | expect(spy).toHaveBeenLastCalledWith(["a"], 0);
128 | a.val = 1;
129 | expect(spy).toHaveBeenLastCalledWith(["a"], 1);
130 | e.call(["b"]);
131 | a.val = 2;
132 | expect(spy).toHaveBeenLastCalledWith(["b"], 2);
133 | });
134 |
135 | test("should save consistent data", () => {
136 | const spy = jest.fn();
137 | const a = mut(0);
138 | const n1 = comp(() => a.val + 1);
139 | const n1_1 = comp(() => n1.val + 1);
140 | const n1_1_1 = comp(() => n1_1.val + 1);
141 | const n2 = comp(() => spy(a.val + "-" + n1_1_1.val));
142 |
143 | run(() => n2.val);
144 |
145 | expect(spy).toBeCalledTimes(1);
146 | expect(spy).toHaveBeenLastCalledWith("0-3");
147 | a.val = 1;
148 | expect(spy).toHaveBeenNthCalledWith(2, "1-4");
149 | expect(spy).toBeCalledTimes(2);
150 | });
151 |
152 | test("should allow modification in selector", () => {
153 | const a = mut(0);
154 | const c = comp(() => {
155 | return (a.val = a.val || 10);
156 | });
157 |
158 | expect(c.val).toBe(10);
159 | });
160 |
161 | test("should safe consistent for init modifiable selector", () => {
162 | const spy = jest.fn();
163 | const a = mut(0);
164 | const c = comp(() => {
165 | if (a.val < 10) {
166 | a.val += 1;
167 | }
168 | return a.val;
169 | });
170 | run(() => {
171 | const m = c.val;
172 | spy(m);
173 | });
174 |
175 | expect(spy).toHaveBeenNthCalledWith(1, 10);
176 | expect(spy).toBeCalledTimes(1);
177 | });
178 |
179 | test("should safe double consistent for modifiable selector and expr", () => {
180 | const spy = jest.fn();
181 | const a = mut(0);
182 | const b = mut(0);
183 | const c = comp(() => {
184 | if (a.val < 10) a.val += 1;
185 |
186 | if (b.val === 1) {
187 | if (a.val < 20) a.val += 1;
188 | else b.val = 2;
189 | }
190 | return a.val;
191 | });
192 | run(() => {
193 | const m = c.val;
194 | const v = !b.val ? ((b.val = 1), b.val) : b.val;
195 | spy(m, v);
196 | });
197 |
198 | expect(spy).toHaveBeenNthCalledWith(1, 10, 1);
199 | expect(spy).toHaveBeenNthCalledWith(2, 20, 2);
200 | expect(spy).toBeCalledTimes(2);
201 | });
202 |
203 | test("should safe correct reactions order for changing depth without modification", () => {
204 | const spy = jest.fn();
205 | const a = mut(0);
206 | const b = mut(0);
207 |
208 | const m0 = comp(() => {
209 | return !b.val ? a.val : k0.val;
210 | });
211 | const k0 = comp(() => {
212 | return !b.val ? m0.val : a.val;
213 | });
214 |
215 | const m = comp(() => m0.val);
216 | const k = comp(() => k0.val);
217 |
218 | let i = 0;
219 | run(() => (k.val, spy("k", i++)));
220 | run(() => (m.val, spy("m", i++)));
221 |
222 | expect(spy).toHaveBeenNthCalledWith(1, "k", 0);
223 | expect(spy).toHaveBeenNthCalledWith(2, "m", 1);
224 | expect(spy).toBeCalledTimes(2);
225 | spy.mockReset();
226 |
227 | a.val = 1;
228 | expect(spy).toHaveBeenNthCalledWith(1, "m", 2);
229 | expect(spy).toHaveBeenNthCalledWith(2, "k", 3);
230 | expect(spy).toBeCalledTimes(2);
231 | spy.mockReset();
232 |
233 | // switch
234 | b.val = 1;
235 | expect(spy).toBeCalledTimes(0);
236 |
237 | // check
238 | a.val = 2;
239 | // TODO: Whats happends with order of execution if will change depth but not a value?
240 | // TODO: check failed (m:4, k:5)
241 | // expect(spy).toHaveBeenNthCalledWith(1, 'k', 4);
242 | // expect(spy).toHaveBeenNthCalledWith(2, 'm', 5);
243 | expect(spy).toBeCalledTimes(2);
244 | spy.mockReset();
245 | });
246 |
247 | test("stop should work correctly in self", () => {
248 | const spy = jest.fn();
249 | const spy_2 = jest.fn();
250 | const a = mut(0);
251 |
252 | const [r1, s1] = sel(() => {
253 | if (a.val) {
254 | s1();
255 | }
256 | spy(a.val);
257 | return a.val;
258 | });
259 |
260 | run(() => spy_2(r1()));
261 |
262 | expect(spy).toBeCalledTimes(1);
263 | a.val = 1;
264 | expect(spy).toBeCalledTimes(2);
265 | expect(spy_2).toHaveBeenNthCalledWith(1, 0);
266 |
267 | a.val = 0;
268 | expect(spy).toBeCalledTimes(2);
269 | expect(spy_2).toBeCalledTimes(1);
270 | });
271 |
272 | test("stop and run again should work correctly in self", () => {
273 | const spy = jest.fn();
274 | const spy_2 = jest.fn();
275 | const a = mut(0);
276 |
277 | const [r1, s1] = sel(() => {
278 | spy(a.val);
279 | if (a.val === 1) {
280 | s1();
281 | a.val = 0;
282 | r1();
283 | }
284 | return a.val;
285 | });
286 | run(() => spy_2(r1()));
287 |
288 | expect(spy).toBeCalledTimes(1);
289 | a.val = 1;
290 | expect(spy).toHaveBeenNthCalledWith(2, 1);
291 | expect(spy).toHaveBeenNthCalledWith(3, 0);
292 | expect(spy).toBeCalledTimes(3);
293 | expect(a.val).toBe(0);
294 | expect(spy_2).toHaveBeenNthCalledWith(1, 0);
295 |
296 | a.val = 2;
297 | expect(spy).toBeCalledTimes(4);
298 | expect(spy).toHaveBeenNthCalledWith(4, 2);
299 | expect(spy_2).toBeCalledTimes(1);
300 | });
301 |
302 | test("cached value as first argument of body function", () => {
303 | const spy = jest.fn();
304 | const spy_on = jest.fn();
305 |
306 | const a = mut(1);
307 | const stop = mut(0);
308 | const s = selec((cache) => (spy(cache), a.val, stop.val ? cache : a.val));
309 |
310 | sync(s, spy_on);
311 | expect(spy).toBeCalledWith(undefined); spy.mockReset();
312 | expect(spy_on).toBeCalledWith(1); spy_on.mockReset();
313 |
314 | a.val = 2;
315 | expect(spy).toBeCalledWith(1); spy.mockReset();
316 | expect(spy_on).toBeCalledWith(2); spy_on.mockReset();
317 |
318 | stop.val = 1;
319 | expect(spy).toBeCalledWith(2); spy.mockReset();
320 | expect(spy_on).toBeCalledTimes(0);
321 |
322 | a.val = 3;
323 | expect(spy).toBeCalledWith(2); spy.mockReset();
324 | expect(spy_on).toBeCalledTimes(0);
325 |
326 | stop.val = 0;
327 | expect(spy).toBeCalledWith(2); spy.mockReset();
328 | expect(spy_on).toBeCalledWith(3); spy_on.mockReset();
329 | });
330 |
331 | test("two nested selectors just reading in reaction order", () => {
332 | const spy = jest.fn();
333 |
334 | const a = mut([]);
335 | const b = comp(() => a.val[0]);
336 | const c = comp(() => b.val);
337 |
338 | on(() => a.val, () => spy(c.val));
339 |
340 | a.val = [2];
341 | expect(spy).toBeCalledWith(2); spy.mockReset();
342 | a.val = [2];
343 | expect(spy).toBeCalledWith(2); spy.mockReset();
344 | a.val = [4];
345 | // expect(spy).toBeCalledWith(4); spy.mockReset(); // TODO: broken reset of selector cache
346 | });
347 | });
348 |
--------------------------------------------------------------------------------
/tests/untrack.test.js:
--------------------------------------------------------------------------------
1 | import { box, run, runer, untrack } from "./lib";
2 |
3 | describe("Untrack", () => {
4 | test("should work untrack", () => {
5 | const spy = jest.fn();
6 | const a = box(0);
7 | run(() => {
8 | a[0]();
9 | spy();
10 | });
11 |
12 | a[1](1);
13 | expect(spy).toHaveBeenCalledTimes(2);
14 |
15 | spy.mockClear();
16 |
17 | run(() => {
18 | const finish = untrack();
19 | untrack()();
20 | const nested_finish = untrack();
21 | a[0]();
22 | nested_finish();
23 | finish();
24 | spy();
25 | });
26 |
27 | a[1](1);
28 | expect(spy).toHaveBeenCalledTimes(1);
29 | });
30 |
31 | test("should work nested autotrack in untrack", () => {
32 | const spy = jest.fn();
33 | const spy_r = jest.fn();
34 | const a = box(0);
35 |
36 | const r = runer(() => {
37 | spy_r(a[0]());
38 | });
39 |
40 | run(() => {
41 | const track = untrack();
42 | r();
43 | a[0]();
44 | track();
45 | spy();
46 | });
47 |
48 | expect(spy_r).toHaveBeenCalledTimes(1);
49 | expect(spy).toHaveBeenCalledTimes(1);
50 |
51 | a[1](1);
52 | expect(spy_r).toHaveBeenCalledTimes(2);
53 | expect(spy).toHaveBeenCalledTimes(1);
54 | });
55 | });
56 |
--------------------------------------------------------------------------------