diff --git a/benchmark/benchmarks/react/1000fields/index.ts b/benchmark/benchmarks/react/1000fields/index.ts index 9c1385be..3bb8218a 100644 --- a/benchmark/benchmarks/react/1000fields/index.ts +++ b/benchmark/benchmarks/react/1000fields/index.ts @@ -81,8 +81,8 @@ suite .add('Agile State', configTest(agileState)) .add('Agile nested State', configTest(agileNestedState)) .add('Pulse Collection', configTest(pulseCollection)) - // .add('Pulse State', configTest(pulseState)) - // .add('Pulse nested State', configTest(pulseNestedState)) + .add('Pulse State', configTest(pulseState)) + .add('Pulse nested State', configTest(pulseNestedState)) .add('Hookstate', configTest(hookstate)) .add('Jotai', configTest(jotai)) .add('Mobx', configTest(mobx)) diff --git a/benchmark/benchmarks/typescript/cloneDeep/bench/lodash.ts b/benchmark/benchmarks/typescript/cloneDeep/bench/lodash.ts new file mode 100644 index 00000000..16e2fa76 --- /dev/null +++ b/benchmark/benchmarks/typescript/cloneDeep/bench/lodash.ts @@ -0,0 +1,6 @@ +// @ts-ignore +import _ from 'lodash'; + +export function cloneDeep(value: T): T { + return _.cloneDeep(value); +} diff --git a/benchmark/benchmarks/typescript/cloneDeep/bench/looper.ts b/benchmark/benchmarks/typescript/cloneDeep/bench/looper.ts new file mode 100644 index 00000000..146c5c7e --- /dev/null +++ b/benchmark/benchmarks/typescript/cloneDeep/bench/looper.ts @@ -0,0 +1,19 @@ +export function cloneDeep(value: T): T { + // Extra checking 'value == null' because 'typeof null === object' + if (value == null || typeof value !== 'object') return value; + + // Ignore everything that is no object or array but has the type of an object (e.g. classes) + const valConstructorName = Object.getPrototypeOf( + value + ).constructor.name.toLowerCase(); + if (valConstructorName !== 'object' && valConstructorName !== 'array') + return value; + + let temp; + const newObject: any = Array.isArray(value) ? [] : {}; + for (const property in value) { + temp = value[property]; + newObject[property] = cloneDeep(temp); + } + return newObject as T; +} diff --git a/benchmark/benchmarks/typescript/cloneDeep/bench/stringify.ts b/benchmark/benchmarks/typescript/cloneDeep/bench/stringify.ts new file mode 100644 index 00000000..87cee1d2 --- /dev/null +++ b/benchmark/benchmarks/typescript/cloneDeep/bench/stringify.ts @@ -0,0 +1,3 @@ +export function cloneDeep(value: T): T { + return JSON.parse(JSON.stringify(value)); +} diff --git a/benchmark/benchmarks/typescript/cloneDeep/index.ts b/benchmark/benchmarks/typescript/cloneDeep/index.ts new file mode 100644 index 00000000..1ecda695 --- /dev/null +++ b/benchmark/benchmarks/typescript/cloneDeep/index.ts @@ -0,0 +1,56 @@ +import Benchmark, { Suite } from 'benchmark'; +import { + cycleLog, + CycleResultInterface, + endBenchmarkLog, + getCycleResult, + startBenchmarkLog, +} from '../../benchmarkManager'; + +// Files to run the Benchmark on +import * as lodash from './bench/lodash'; +import * as looper from './bench/looper'; +import * as stringify from './bench/stringify'; + +const toClone = { x1: true, x2: undefined }; + +// @ts-ignore +// Benchmark.js requires an instance of itself globally +window.Benchmark = Benchmark; + +// Create new Benchmark Test Suite +const suite = new Suite('clone deep'); + +const results: CycleResultInterface[] = []; + +// Add Tests to the Benchmark Test Suite +suite + .add('Lodash', function () { + lodash.cloneDeep(toClone); + }) + .add('Looper', function () { + looper.cloneDeep(toClone); + }) + .add('Stringify', function () { + stringify.cloneDeep(toClone); + }) + + // Add Listener + .on('start', function (this: any) { + startBenchmarkLog(this.name); + }) + .on('cycle', (event: any) => { + const cycleResult = getCycleResult(event); + cycleLog(cycleResult); + results.push(cycleResult); + }) + .on('complete', function (this: any) { + endBenchmarkLog(this.name, results, this.filter('fastest').map('name')); + + // @ts-ignore + // Notify server that the Benchmark Test Suite has ended + window.TEST.ended = true; + }) + + // Run Benchmark Test Suite + .run({ async: true }); diff --git a/benchmark/package.json b/benchmark/package.json index 52dcd5bf..006b4707 100644 --- a/benchmark/package.json +++ b/benchmark/package.json @@ -12,6 +12,7 @@ "test:1000fields": "yarn test ./benchmarks/react/1000fields", "test:computed": "yarn test ./benchmarks/react/computed", "test:defineConfig": "yarn test ./benchmarks/typescript/defineConfig", + "test:cloneDeep": "yarn test ./benchmarks/typescript/cloneDeep", "install:dev:agile": "yalc add @agile-ts/core @agile-ts/react & yarn install", "install:prod:agile": "yarn add @agile-ts/core @agile-ts/react & yarn install" }, @@ -21,8 +22,9 @@ }, "dependencies": { "@agile-ts/core": "file:.yalc/@agile-ts/core", + "@agile-ts/logger": "file:.yalc/@agile-ts/logger", "@agile-ts/react": "file:.yalc/@agile-ts/react", - "@hookstate/core": "^3.0.8", + "@hookstate/core": "^3.0.11", "@pulsejs/core": "^4.0.0-beta.3", "@pulsejs/react": "^4.0.0-beta.3", "@reduxjs/toolkit": "^1.6.0", diff --git a/benchmark/yarn.lock b/benchmark/yarn.lock index 5e123e81..fdae0a71 100644 --- a/benchmark/yarn.lock +++ b/benchmark/yarn.lock @@ -3,7 +3,12 @@ "@agile-ts/core@file:.yalc/@agile-ts/core": - version "0.2.0-alpha.3" + version "0.2.0-alpha.5" + dependencies: + "@agile-ts/utils" "^0.0.7" + +"@agile-ts/logger@file:.yalc/@agile-ts/logger": + version "0.0.7" dependencies: "@agile-ts/utils" "^0.0.7" @@ -22,10 +27,10 @@ dependencies: regenerator-runtime "^0.13.4" -"@hookstate/core@^3.0.8": - version "3.0.8" - resolved "https://registry.yarnpkg.com/@hookstate/core/-/core-3.0.8.tgz#d6838153d6d43c2f35cfca475c31248192564e62" - integrity sha512-blQagGIVIbNoUiNCRrvaXqFmUe7WGMY35ok/LENfl2pcIsLBjkreYIZiaSFi83tkycwq7ZOmcQz/R1nvLKhH8w== +"@hookstate/core@^3.0.11": + version "3.0.11" + resolved "https://registry.yarnpkg.com/@hookstate/core/-/core-3.0.11.tgz#515f2748b3741f3d7e90cbc1acff8d6ac84e5494" + integrity sha512-cbI711aFWX4d8+xkLxikmLnR+f55ePHrBMWFA4gTLEoCa1+Cg0pfNw7h7zSodYMeYt8Y5A5TVSh7a5p8lBC89A== "@pulsejs/core@^4.0.0-beta.3": version "4.0.0-beta.3" diff --git a/examples/react/develop/fields/.env b/examples/react/develop/fields/.env new file mode 100644 index 00000000..6f809cc2 --- /dev/null +++ b/examples/react/develop/fields/.env @@ -0,0 +1 @@ +SKIP_PREFLIGHT_CHECK=true diff --git a/examples/react/develop/fields/.gitignore b/examples/react/develop/fields/.gitignore new file mode 100644 index 00000000..4d29575d --- /dev/null +++ b/examples/react/develop/fields/.gitignore @@ -0,0 +1,23 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/examples/react/develop/fields/README.md b/examples/react/develop/fields/README.md new file mode 100644 index 00000000..02aac3f6 --- /dev/null +++ b/examples/react/develop/fields/README.md @@ -0,0 +1,70 @@ +# Getting Started with Create React App + +This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). + +## Available Scripts + +In the project directory, you can run: + +### `yarn start` + +Runs the app in the development mode.\ +Open [http://localhost:3000](http://localhost:3000) to view it in the browser. + +The page will reload if you make edits.\ +You will also see any lint errors in the console. + +### `yarn test` + +Launches the test runner in the interactive watch mode.\ +See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. + +### `yarn build` + +Builds the app for production to the `build` folder.\ +It correctly bundles React in production mode and optimizes the build for the best performance. + +The build is minified and the filenames include the hashes.\ +Your app is ready to be deployed! + +See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. + +### `yarn eject` + +**Note: this is a one-way operation. Once you `eject`, you can’t go back!** + +If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. + +Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. + +You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. + +## Learn More + +You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). + +To learn React, check out the [React documentation](https://reactjs.org/). + +### Code Splitting + +This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) + +### Analyzing the Bundle Size + +This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) + +### Making a Progressive Web App + +This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) + +### Advanced Configuration + +This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) + +### Deployment + +This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) + +### `yarn build` fails to minify + +This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) diff --git a/examples/react/develop/fields/package.json b/examples/react/develop/fields/package.json new file mode 100644 index 00000000..ad32f1c1 --- /dev/null +++ b/examples/react/develop/fields/package.json @@ -0,0 +1,38 @@ +{ + "name": "fields", + "version": "0.1.0", + "private": true, + "dependencies": { + "@agile-ts/core": "file:.yalc/@agile-ts/core", + "@agile-ts/react": "file:.yalc/@agile-ts/react", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-scripts": "4.0.3" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject", + "install:dev:agile": "yalc add @agile-ts/core @agile-ts/react & yarn install", + "install:prod:agile": "yarn add @agile-ts/core @agile-ts/react & yarn install" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/examples/react/develop/fields/public/favicon.ico b/examples/react/develop/fields/public/favicon.ico new file mode 100644 index 00000000..a11777cc Binary files /dev/null and b/examples/react/develop/fields/public/favicon.ico differ diff --git a/examples/react/develop/fields/public/index.html b/examples/react/develop/fields/public/index.html new file mode 100644 index 00000000..aa069f27 --- /dev/null +++ b/examples/react/develop/fields/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
+ + + diff --git a/examples/react/develop/fields/public/logo192.png b/examples/react/develop/fields/public/logo192.png new file mode 100644 index 00000000..fc44b0a3 Binary files /dev/null and b/examples/react/develop/fields/public/logo192.png differ diff --git a/examples/react/develop/fields/public/logo512.png b/examples/react/develop/fields/public/logo512.png new file mode 100644 index 00000000..a4e47a65 Binary files /dev/null and b/examples/react/develop/fields/public/logo512.png differ diff --git a/examples/react/develop/fields/public/manifest.json b/examples/react/develop/fields/public/manifest.json new file mode 100644 index 00000000..080d6c77 --- /dev/null +++ b/examples/react/develop/fields/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/examples/react/develop/fields/public/robots.txt b/examples/react/develop/fields/public/robots.txt new file mode 100644 index 00000000..e9e57dc4 --- /dev/null +++ b/examples/react/develop/fields/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/examples/react/develop/fields/src/App.js b/examples/react/develop/fields/src/App.js new file mode 100644 index 00000000..5796839d --- /dev/null +++ b/examples/react/develop/fields/src/App.js @@ -0,0 +1,60 @@ +import React from 'react'; +import { createCollection, LogCodeManager, shared } from '@agile-ts/core'; +import reactIntegration, { useAgile, useValue } from '@agile-ts/react'; + +LogCodeManager.setAllowLogging(false); +shared.integrate(reactIntegration); + +const FIELDS = createCollection({ + initialData: Array.from(Array(5000).keys()).map((i) => ({ + id: i, + name: `Field #${i + 1}`, + })), +}); + +let renderFieldsCount = 0; + +function Field({ index }) { + const ITEM = FIELDS.getItem(index); + const item = useAgile(ITEM); + + console.log(`Rerender Fields at '${index}':`, ++renderFieldsCount); + + return ( +
+ Last {``} render at: {new Date().toISOString()} +   + { + ITEM?.patch({ name: e.target.value }); + }} + /> +
+ ); +} + +export default function App() { + const fieldKeys = useValue(FIELDS); + + return ( +
+
+ Last {``} render at: {new Date().toISOString()} +
+
+ {fieldKeys.map((key, i) => ( + + ))} + +
+ ); +} diff --git a/examples/react/develop/fields/src/index.js b/examples/react/develop/fields/src/index.js new file mode 100644 index 00000000..c1f31c5f --- /dev/null +++ b/examples/react/develop/fields/src/index.js @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import App from './App'; + +ReactDOM.render( + + + , + document.getElementById('root') +); diff --git a/examples/react/develop/functional-component-ts/src/App.tsx b/examples/react/develop/functional-component-ts/src/App.tsx index 7c0849b3..2321584a 100644 --- a/examples/react/develop/functional-component-ts/src/App.tsx +++ b/examples/react/develop/functional-component-ts/src/App.tsx @@ -1,7 +1,6 @@ import React, { useEffect } from 'react'; import './App.css'; import { useAgile, useWatcher, useProxy, useSelector } from '@agile-ts/react'; -import { useEvent } from '@agile-ts/event/dist/react'; import { COUNTUP, externalCreatedItem, @@ -15,6 +14,7 @@ import { } from './core'; import { generateId } from '@agile-ts/utils'; import { globalBind } from '@agile-ts/core'; +import { useEvent } from '@agile-ts/event'; let rerenderCount = 0; let rerenderCountInCountupView = 0; diff --git a/examples/react/develop/simple-todo-list/.env b/examples/react/develop/simple-todo-list/.env new file mode 100644 index 00000000..6f809cc2 --- /dev/null +++ b/examples/react/develop/simple-todo-list/.env @@ -0,0 +1 @@ +SKIP_PREFLIGHT_CHECK=true diff --git a/examples/react/develop/simple-todo-list/.gitignore b/examples/react/develop/simple-todo-list/.gitignore new file mode 100644 index 00000000..4d29575d --- /dev/null +++ b/examples/react/develop/simple-todo-list/.gitignore @@ -0,0 +1,23 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/examples/react/develop/simple-todo-list/README.md b/examples/react/develop/simple-todo-list/README.md new file mode 100644 index 00000000..02aac3f6 --- /dev/null +++ b/examples/react/develop/simple-todo-list/README.md @@ -0,0 +1,70 @@ +# Getting Started with Create React App + +This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). + +## Available Scripts + +In the project directory, you can run: + +### `yarn start` + +Runs the app in the development mode.\ +Open [http://localhost:3000](http://localhost:3000) to view it in the browser. + +The page will reload if you make edits.\ +You will also see any lint errors in the console. + +### `yarn test` + +Launches the test runner in the interactive watch mode.\ +See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. + +### `yarn build` + +Builds the app for production to the `build` folder.\ +It correctly bundles React in production mode and optimizes the build for the best performance. + +The build is minified and the filenames include the hashes.\ +Your app is ready to be deployed! + +See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. + +### `yarn eject` + +**Note: this is a one-way operation. Once you `eject`, you can’t go back!** + +If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. + +Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. + +You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. + +## Learn More + +You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). + +To learn React, check out the [React documentation](https://reactjs.org/). + +### Code Splitting + +This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) + +### Analyzing the Bundle Size + +This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) + +### Making a Progressive Web App + +This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) + +### Advanced Configuration + +This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) + +### Deployment + +This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) + +### `yarn build` fails to minify + +This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) diff --git a/examples/react/develop/simple-todo-list/package.json b/examples/react/develop/simple-todo-list/package.json new file mode 100644 index 00000000..c034b229 --- /dev/null +++ b/examples/react/develop/simple-todo-list/package.json @@ -0,0 +1,39 @@ +{ + "name": "simple-todo-list", + "version": "0.1.0", + "private": true, + "dependencies": { + "@agile-ts/core": "file:.yalc/@agile-ts/core", + "@agile-ts/logger": "file:.yalc/@agile-ts/logger", + "@agile-ts/react": "file:.yalc/@agile-ts/react", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-scripts": "4.0.3" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject", + "install:dev:agile": "yalc add @agile-ts/core @agile-ts/react @agile-ts/logger & yarn install", + "install:prod:agile": "yarn add @agile-ts/core @agile-ts/react @agile-ts/logger & yarn install" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/examples/react/develop/simple-todo-list/public/favicon.ico b/examples/react/develop/simple-todo-list/public/favicon.ico new file mode 100644 index 00000000..a11777cc Binary files /dev/null and b/examples/react/develop/simple-todo-list/public/favicon.ico differ diff --git a/examples/react/develop/simple-todo-list/public/index.html b/examples/react/develop/simple-todo-list/public/index.html new file mode 100644 index 00000000..aa069f27 --- /dev/null +++ b/examples/react/develop/simple-todo-list/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
+ + + diff --git a/examples/react/develop/simple-todo-list/public/logo192.png b/examples/react/develop/simple-todo-list/public/logo192.png new file mode 100644 index 00000000..fc44b0a3 Binary files /dev/null and b/examples/react/develop/simple-todo-list/public/logo192.png differ diff --git a/examples/react/develop/simple-todo-list/public/logo512.png b/examples/react/develop/simple-todo-list/public/logo512.png new file mode 100644 index 00000000..a4e47a65 Binary files /dev/null and b/examples/react/develop/simple-todo-list/public/logo512.png differ diff --git a/examples/react/develop/simple-todo-list/public/manifest.json b/examples/react/develop/simple-todo-list/public/manifest.json new file mode 100644 index 00000000..080d6c77 --- /dev/null +++ b/examples/react/develop/simple-todo-list/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/examples/react/develop/simple-todo-list/public/robots.txt b/examples/react/develop/simple-todo-list/public/robots.txt new file mode 100644 index 00000000..e9e57dc4 --- /dev/null +++ b/examples/react/develop/simple-todo-list/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/examples/react/develop/simple-todo-list/src/App.js b/examples/react/develop/simple-todo-list/src/App.js new file mode 100644 index 00000000..7844b8c2 --- /dev/null +++ b/examples/react/develop/simple-todo-list/src/App.js @@ -0,0 +1,53 @@ +import React from 'react'; +import { generateId } from '@agile-ts/core'; +import { useAgile } from '@agile-ts/react'; +import { TODOS } from './core'; + +const App = () => { + // With the 'useAgile' Hook we bind our first Collection to the 'RandomComponent' for reactivity + const todos = useAgile(TODOS); + + // Current Input of Name Form + const [currentInput, setCurrentInput] = React.useState(''); + + return ( +
+

Simple TODOS

+ { + setCurrentInput(event.target.value); + }} + /> + + {todos.map((value) => ( +
+
{value.name}
+ +
+ ))} +
+ ); +}; + +export default App; diff --git a/examples/react/develop/simple-todo-list/src/core.js b/examples/react/develop/simple-todo-list/src/core.js new file mode 100644 index 00000000..881290a6 --- /dev/null +++ b/examples/react/develop/simple-todo-list/src/core.js @@ -0,0 +1,11 @@ +import { createCollection, globalBind } from '@agile-ts/core'; +import { assignSharedAgileLoggerConfig, Logger } from '@agile-ts/logger'; + +assignSharedAgileLoggerConfig({ level: Logger.level.DEBUG }); + +// Create Collection +export const TODOS = createCollection({ + initialData: [{ id: 1, name: 'Clean Bathroom' }], +}).persist('todos'); // persist does store the Collection in the Local Storage + +globalBind('__core__', { TODOS }); diff --git a/examples/react/develop/simple-todo-list/src/index.js b/examples/react/develop/simple-todo-list/src/index.js new file mode 100644 index 00000000..c1f31c5f --- /dev/null +++ b/examples/react/develop/simple-todo-list/src/index.js @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import App from './App'; + +ReactDOM.render( + + + , + document.getElementById('root') +); diff --git a/packages/core/src/collection/collection.persistent.ts b/packages/core/src/collection/collection.persistent.ts index 2b2232f1..0cbf3987 100644 --- a/packages/core/src/collection/collection.persistent.ts +++ b/packages/core/src/collection/collection.persistent.ts @@ -36,10 +36,10 @@ export class CollectionPersistent< config: CreatePersistentConfigInterface = {} ) { super(collection.agileInstance(), { - instantiate: false, + loadValue: false, }); config = defineConfig(config, { - instantiate: true, + loadValue: true, storageKeys: [], defaultStorageKey: null as any, }); @@ -51,7 +51,7 @@ export class CollectionPersistent< }); // Load/Store persisted value/s for the first time - if (this.ready && config.instantiate) this.initialLoading(); + if (this.ready && config.loadValue) this.initialLoading(); } /** @@ -104,6 +104,7 @@ export class CollectionPersistent< // Persist default Group and load its value manually to be 100% sure // that it was loaded completely + defaultGroup.loadedInitialValue = false; defaultGroup.persist(defaultGroupStorageKey, { loadValue: false, defaultStorageKey: this.config.defaultStorageKey || undefined, @@ -113,14 +114,6 @@ export class CollectionPersistent< if (defaultGroup.persistent?.ready) await defaultGroup.persistent.initialLoading(); - // TODO rebuild the default Group once at the end when all Items were loaded into the Collection - // because otherwise it rebuilds the Group for each loaded Item - // (-> warnings are printed for all not yet loaded Items when rebuilding the Group) - // or rethink the whole Group rebuild process by adding a 'addItem()', 'removeItem()' and 'updateItem()' function - // so that there is no need for rebuilding the whole Group when for example only Item B changed or Item C was added - // - // See Issue by starting the vue develop example app and adding some todos to the _todo_ list - // Persist Items found in the default Group's value for (const itemKey of defaultGroup._value) { const item = this.collection().getItem(itemKey); @@ -156,13 +149,14 @@ export class CollectionPersistent< followCollectionPersistKeyPattern: false, // Because of the dynamic 'storageItemKey', the key is already formatted above }); if (placeholderItem?.persistent?.ready) { - const loadedPersistedValueIntoItem = await placeholderItem.persistent.loadPersistedValue(); // TODO FIRST GROUP REBUILD (by assigning loaded value to Item) + const loadedPersistedValueIntoItem = await placeholderItem.persistent.loadPersistedValue(); // If successfully loaded Item value, assign Item to Collection if (loadedPersistedValueIntoItem) { this.collection().assignItem(placeholderItem, { overwrite: true, // Overwrite to overwrite the existing placeholder Item, if one exists - }); // TODO SECOND GROUP REBUILD (by calling rebuildGroupThatInclude() in the assignItem() method) + rebuildGroups: false, // Not necessary since the Groups that include the to assign Item were already rebuild while assigning the loaded value to the Item via 'loadPersistedValue()' + }); placeholderItem.isPersisted = true; @@ -176,6 +170,7 @@ export class CollectionPersistent< } } + defaultGroup.loadedInitialValue = true; return true; }; const success = await loadValuesIntoCollection(); diff --git a/packages/core/src/collection/collection.ts b/packages/core/src/collection/collection.ts index 31b8a107..60535136 100644 --- a/packages/core/src/collection/collection.ts +++ b/packages/core/src/collection/collection.ts @@ -1,26 +1,27 @@ import { Agile, - Item, - Group, - GroupKey, - Selector, - SelectorKey, - StorageKey, - GroupConfigInterface, - isValidObject, - normalizeArray, - copy, CollectionPersistent, - GroupAddConfigInterface, ComputedTracker, + copy, + defineConfig, generateId, - SideEffectConfigInterface, - SelectorConfigInterface, - removeProperties, + Group, + GroupAddConfigInterface, + GroupConfigInterface, + GroupIngestConfigInterface, + GroupKey, isFunction, + isValidObject, + Item, LogCodeManager, + normalizeArray, PatchOptionConfigInterface, - defineConfig, + removeProperties, + Selector, + SelectorConfigInterface, + SelectorKey, + StorageKey, + TrackedChangeMethod, } from '../internal'; export class Collection< @@ -106,7 +107,7 @@ export class Collection< // Rebuild of Groups // Not necessary because if Items are added to the Collection, // (after 'isInstantiated = true') - // the Groups which contain these added Items are rebuilt. + // the Groups which contain these added Items get rebuilt. // for (const key in this.groups) this.groups[key].rebuild(); } @@ -978,7 +979,7 @@ export class Collection< // Create Persistent (-> persist value) this.persistent = new CollectionPersistent(this, { - instantiate: _config.loadValue, + loadValue: _config.loadValue, storageKeys: _config.storageKeys, key: key, defaultStorageKey: _config.defaultStorageKey, @@ -1405,6 +1406,7 @@ export class Collection< config = defineConfig(config, { overwrite: false, background: false, + rebuildGroups: true, }); const primaryKey = this.config.primaryKey; let itemKey = item._value[primaryKey]; @@ -1442,9 +1444,10 @@ export class Collection< // Rebuild Groups that include itemKey // after adding Item with itemKey to the Collection // (because otherwise it can't find the Item as it isn't added yet) - this.rebuildGroupsThatIncludeItemKey(itemKey, { - background: config.background, - }); + if (config.rebuildGroups) + this.rebuildGroupsThatIncludeItemKey(itemKey, { + background: config.background, + }); if (increaseCollectionSize) this.size++; @@ -1460,29 +1463,49 @@ export class Collection< */ public rebuildGroupsThatIncludeItemKey( itemKey: ItemKey, - config: RebuildGroupsThatIncludeItemKeyConfigInterface = {} + config: GroupIngestConfigInterface = {} ): void { - config = defineConfig(config, { - background: false, - sideEffects: { - enabled: true, - exclude: [], - }, - }); - // Rebuild Groups that include itemKey - for (const groupKey in this.groups) { + for (const groupKey of Object.keys(this.groups)) { const group = this.getGroup(groupKey); - if (group?.has(itemKey)) { - // Not necessary because a sideEffect of ingesting the Group - // into the runtime is to rebuilt itself - // group.rebuild(); - - group?.rebuild({ - background: config?.background, - sideEffects: config?.sideEffects, - storage: false, - }); + if (group != null && group.has(itemKey)) { + const index = group._preciseItemKeys.findIndex((ik) => itemKey === ik); + + // Update Group output at index + if (index !== -1) { + group.rebuild( + [ + { + key: itemKey, + index: index, + method: TrackedChangeMethod.UPDATE, + }, + ], + config + ); + } + // Add Item to the Group output if it isn't yet represented there to be updated + else { + const indexOfBeforeItemKey = + group.nextStateValue.findIndex((ik) => itemKey === ik) - 1; + + group.rebuild( + [ + { + key: itemKey, + index: + indexOfBeforeItemKey >= 0 + ? group._preciseItemKeys.findIndex( + (ik) => + group.nextStateValue[indexOfBeforeItemKey] === ik + ) + 1 + : 0, + method: TrackedChangeMethod.ADD, + }, + ], + config + ); + } } } } @@ -1601,20 +1624,6 @@ export interface UpdateItemKeyConfigInterface { background?: boolean; } -export interface RebuildGroupsThatIncludeItemKeyConfigInterface { - /** - * Whether to rebuilt the Group in background. - * So that the UI isn't notified of these changes and thus doesn't rerender. - * @default false - */ - background?: boolean; - /** - * Whether to execute the defined side effects. - * @default true - */ - sideEffects?: SideEffectConfigInterface; -} - export interface HasConfigInterface { /** * Whether Items that do not officially exist, @@ -1694,4 +1703,9 @@ export interface AssignItemConfigInterface { * @default false */ background?: boolean; + /** + * Whether to rebuild all Groups that include the itemKey of the to assign Item. + * @default true + */ + rebuildGroups?: boolean; } diff --git a/packages/core/src/collection/group/group.observer.ts b/packages/core/src/collection/group/group.observer.ts index 4c0e860c..3e5d490a 100644 --- a/packages/core/src/collection/group/group.observer.ts +++ b/packages/core/src/collection/group/group.observer.ts @@ -6,10 +6,10 @@ import { equal, generateId, RuntimeJob, - Item, IngestConfigInterface, CreateRuntimeJobConfigInterface, defineConfig, + removeProperties, } from '../../internal'; export class GroupObserver extends Observer { @@ -45,14 +45,14 @@ export class GroupObserver extends Observer { * into the runtime wrapped into a Runtime-Job * where it is executed accordingly. * - * During the execution the runtime applies the rebuilt `nextGroupOutput` to the Group, + * During the execution the runtime applies the `nextGroupOutput` to the Group, * updates its dependents and re-renders the UI-Components it is subscribed to. * * @internal * @param config - Configuration object */ public ingest(config: GroupIngestConfigInterface = {}): void { - this.group().rebuild(config); + this.ingestOutput(this.group().nextGroupOutput, config); } /** @@ -63,23 +63,17 @@ export class GroupObserver extends Observer { * updates its dependents and re-renders the UI-Components it is subscribed to. * * @internal - * @param newGroupItems - New Group Items to be applied to the Group. + * @param newGroupOutput - New Group Output to be applied to the Group. * @param config - Configuration object. */ - public ingestItems( - newGroupItems: Item[], + public ingestOutput( + newGroupOutput: DataType[], config: GroupIngestConfigInterface = {} ): void { const group = this.group(); config = defineConfig(config, { perform: true, - background: false, - sideEffects: { - enabled: true, - exclude: [], - }, force: false, - maxTriesToUpdate: 3, }); // Force overwriting the Group value if it is a placeholder. @@ -89,24 +83,19 @@ export class GroupObserver extends Observer { } // Assign next Group output to Observer - this.nextGroupOutput = copy( - newGroupItems.map((item) => { - return item._value; - }) - ); + this.nextGroupOutput = copy(newGroupOutput); // Check if current Group output and to assign Group output are equal if (equal(group._output, this.nextGroupOutput) && !config.force) return; // Create Runtime-Job const job = new RuntimeJob(this, { - sideEffects: config.sideEffects, - force: config.force, - background: config.background, - key: - config.key ?? - `${this._key != null ? this._key + '_' : ''}${generateId()}_output`, - maxTriesToUpdate: config.maxTriesToUpdate, + ...removeProperties(config, ['perform']), + ...{ + key: + config.key ?? + `${this._key != null ? this._key + '_' : ''}${generateId()}_output`, + }, }); // Pass created Job into the Runtime @@ -130,6 +119,7 @@ export class GroupObserver extends Observer { // Assign new Group output group._output = copy(observer.nextGroupOutput); + group.nextGroupOutput = copy(observer.nextGroupOutput); // Assign new public output to the Observer (output used by the Integrations) job.observer.previousValue = copy(job.observer.value); diff --git a/packages/core/src/collection/group/index.ts b/packages/core/src/collection/group/index.ts index dc339159..c404b7db 100644 --- a/packages/core/src/collection/group/index.ts +++ b/packages/core/src/collection/group/index.ts @@ -18,6 +18,7 @@ import { GroupObserver, StateObserver, defineConfig, + GroupIngestConfigInterface, } from '../../internal'; export class Group< @@ -30,14 +31,23 @@ export class Group< static rebuildGroupSideEffectKey = 'rebuildGroup'; // Item values represented by the Group - _output: Array = []; + public _output: Array = []; + // Next output of the Group (which can be used for dynamic Group updates) + public nextGroupOutput: Array = []; + // Precise itemKeys of the Group only include itemKeys + // that actually exist in the corresponding Collection + public _preciseItemKeys: Array = []; // Manages dependencies to other States and subscriptions of UI-Components. // It also serves as an interface to the runtime. public observers: GroupObservers = {} as any; // Keeps track of all Item identifiers for Items that couldn't be found in the Collection - notFoundItemKeys: Array = []; + public notFoundItemKeys: Array = []; + + // Whether the initial value was loaded from the corresponding Persistent + // https://github.com/agile-ts/agile/issues/155 + public loadedInitialValue = true; /** * An extension of the State Class that categorizes and preserves the ordering of structured data. @@ -72,7 +82,9 @@ export class Group< // Add side effect to Group // that rebuilds the Group whenever the Group value changes - this.addSideEffect(Group.rebuildGroupSideEffectKey, () => this.rebuild()); + this.addSideEffect(Group.rebuildGroupSideEffectKey, (state, config) => { + this.rebuild(config?.any?.trackedChanges || [], config); + }); // Initial rebuild this.rebuild(); @@ -104,7 +116,7 @@ export class Group< * @param itemKey - Key/Name identifier of the Item. */ public has(itemKey: ItemKey) { - return this.value.findIndex((key) => key === itemKey) !== -1; + return this.value.indexOf(itemKey) !== -1; } /** @@ -130,22 +142,46 @@ export class Group< */ public remove( itemKeys: ItemKey | ItemKey[], - config: StateIngestConfigInterface = {} + config: GroupRemoveConfigInterface = {} ): this { const _itemKeys = normalizeArray(itemKeys); const notExistingItemKeysInCollection: Array = []; const notExistingItemKeys: Array = []; let newGroupValue = copy(this.nextStateValue); + // Need to temporary update the preciseItemKeys + // since in the rebuild one action (trackedChanges) is performed after the other + // which requires a dynamic updated index + const updatedPreciseItemKeys = copy(this._preciseItemKeys); + config = defineConfig(config, { + softRebuild: true, + any: {}, + }); + config.any['trackedChanges'] = []; // TODO might be improved since the 'any' property is very vague // Remove itemKeys from Group _itemKeys.forEach((itemKey) => { + const exists = newGroupValue.includes(itemKey); + // Check if itemKey exists in Group - if (!newGroupValue.includes(itemKey)) { + if (!exists) { notExistingItemKeys.push(itemKey); notExistingItemKeysInCollection.push(itemKey); return; } + // Track changes to soft rebuild the Group when rebuilding the Group in a side effect + if (config.softRebuild) { + const index = updatedPreciseItemKeys.findIndex((ik) => ik === itemKey); + if (index !== -1) { + updatedPreciseItemKeys.splice(index, 1); + config.any['trackedChanges'].push({ + index, + method: TrackedChangeMethod.REMOVE, + key: itemKey, + }); + } + } + // Check if itemKey exists in Collection if (!this.collection().getItem(itemKey)) notExistingItemKeysInCollection.push(itemKey); @@ -162,7 +198,7 @@ export class Group< if (notExistingItemKeysInCollection.length >= _itemKeys.length) config.background = true; - this.set(newGroupValue, config); + this.set(newGroupValue, removeProperties(config, ['softRebuild'])); return this; } @@ -183,27 +219,42 @@ export class Group< const _itemKeys = normalizeArray(itemKeys); const notExistingItemKeysInCollection: Array = []; const existingItemKeys: Array = []; - let newGroupValue = copy(this.nextStateValue); - defineConfig(config, { + const newGroupValue = copy(this.nextStateValue); + // Need to temporary update the preciseItemKeys + // since in the rebuild one action (trackedChanges) is performed after the other + // which requires a dynamic updated index + const updatedPreciseItemKeys = copy(this._preciseItemKeys); + config = defineConfig(config, { method: 'push', - overwrite: false, + softRebuild: true, + any: {}, }); + config.any['trackedChanges'] = []; // TODO might be improved since the 'any' property is very vague // Add itemKeys to Group _itemKeys.forEach((itemKey) => { + const exists = newGroupValue.includes(itemKey); + // Check if itemKey exists in Collection if (!this.collection().getItem(itemKey)) notExistingItemKeysInCollection.push(itemKey); - // Remove itemKey temporary from newGroupValue - // if it should be overwritten and already exists in the newGroupValue - if (newGroupValue.includes(itemKey)) { - if (config.overwrite) { - newGroupValue = newGroupValue.filter((key) => key !== itemKey); - } else { - existingItemKeys.push(itemKey); - return; - } + // Handle existing Item + if (exists) { + existingItemKeys.push(itemKey); + return; + } + + // Track changes to soft rebuild the Group when rebuilding the Group in a side effect + if (config.softRebuild) { + const index = + config.method === 'push' ? updatedPreciseItemKeys.length : 0; + updatedPreciseItemKeys.push(itemKey); + config.any['trackedChanges'].push({ + index, + method: TrackedChangeMethod.ADD, + key: itemKey, + }); } // Add new itemKey to Group @@ -221,7 +272,10 @@ export class Group< ) config.background = true; - this.set(newGroupValue, removeProperties(config, ['method', 'overwrite'])); + this.set( + newGroupValue, + removeProperties(config, ['method', 'softRebuild']) + ); return this; } @@ -339,26 +393,76 @@ export class Group< * [Learn more..](https://agile-ts.org/docs/core/collection/group/methods#rebuild) * * @internal + * @param trackedChanges - Changes that were tracked between two rebuilds. * @param config - Configuration object */ - public rebuild(config: StateIngestConfigInterface = {}): this { - const notFoundItemKeys: Array = []; // Item keys that couldn't be found in the Collection - const groupItems: Array> = []; - + public rebuild( + trackedChanges: TrackedChangeInterface[] = [], + config: GroupIngestConfigInterface = {} + ): this { // Don't rebuild Group if Collection isn't correctly instantiated yet // (because only after a successful instantiation the Collection // contains the Items which are essential for a proper rebuild) if (!this.collection().isInstantiated) return this; - // Fetch Items from Collection - this._value.forEach((itemKey) => { - const item = this.collection().getItem(itemKey); - if (item != null) groupItems.push(item); - else notFoundItemKeys.push(itemKey); - }); + // Item keys that couldn't be found in the Collection + const notFoundItemKeys: Array = []; + + // Soft rebuild the Collection (-> rebuild only parts of the Collection) + if (trackedChanges.length > 0) { + trackedChanges.forEach((change) => { + const item = this.collection().getItem(change.key); + + switch (change.method) { + case TrackedChangeMethod.ADD: + // this._value.splice(change.index, 0, change.key); // Already updated in 'add' method + if (item != null) { + this._preciseItemKeys.splice(change.index, 0, change.key); + this.nextGroupOutput.splice(change.index, 0, copy(item._value)); + } else { + notFoundItemKeys.push(change.key); + } + break; + case TrackedChangeMethod.UPDATE: + if (item != null) { + this.nextGroupOutput[change.index] = copy(item._value); + } else { + notFoundItemKeys.push(change.key); + } + break; + case TrackedChangeMethod.REMOVE: + // this._value.splice(change.index, 1); // Already updated in 'remove' method + this._preciseItemKeys.splice(change.index, 1); + this.nextGroupOutput.splice(change.index, 1); + break; + default: + break; + } + }); + this.observers['output'].ingest(config); + } + // Hard rebuild the whole Collection + else { + const groupItemValues: Array = []; + + // Reset precise itemKeys array to rebuild it from scratch + this._preciseItemKeys = []; + + // Fetch Items from Collection + this._value.forEach((itemKey) => { + const item = this.collection().getItem(itemKey); + if (item != null) { + groupItemValues.push(item._value); + this._preciseItemKeys.push(itemKey); + } else notFoundItemKeys.push(itemKey); + }); + + // Ingest rebuilt Group output into the Runtime + this.observers['output'].ingestOutput(groupItemValues, config); + } // Logging - if (notFoundItemKeys.length > 0) { + if (notFoundItemKeys.length > 0 && this.loadedInitialValue) { LogCodeManager.log( '1C:02:00', [this.collection()._key, this._key], @@ -368,9 +472,6 @@ export class Group< this.notFoundItemKeys = notFoundItemKeys; - // Ingest rebuilt Group output into the Runtime - this.observers['output'].ingestItems(groupItems, config); - return this; } } @@ -395,11 +496,22 @@ export interface GroupAddConfigInterface extends StateIngestConfigInterface { */ method?: 'unshift' | 'push'; /** - * If the to add `itemKey` already exists, - * whether its position should be overwritten with the position of the new `itemKey`. - * @default false + * Whether to soft rebuild the Group. + * -> only rebuild the parts of the Group that have actually changed + * instead of rebuilding the whole Group. + * @default true + */ + softRebuild?: boolean; +} + +export interface GroupRemoveConfigInterface extends StateIngestConfigInterface { + /** + * Whether to soft rebuild the Group. + * -> only rebuild the parts of the Group that have actually changed + * instead of rebuilding the whole Group. + * @default true */ - overwrite?: boolean; + softRebuild?: boolean; } export interface GroupConfigInterface { @@ -425,3 +537,24 @@ export interface GroupPersistConfigInterface */ followCollectionPersistKeyPattern?: boolean; } + +export enum TrackedChangeMethod { + ADD, + REMOVE, + UPDATE, +} + +export interface TrackedChangeInterface { + /** + * TODO + */ + method: TrackedChangeMethod; + /** + * TODO + */ + key: ItemKey; + /** + * TODO + */ + index: number; +} diff --git a/packages/core/src/collection/item.ts b/packages/core/src/collection/item.ts index db98104f..44266aee 100644 --- a/packages/core/src/collection/item.ts +++ b/packages/core/src/collection/item.ts @@ -166,9 +166,6 @@ export class Item extends EnhancedState< this.addSideEffect>( Item.updateGroupSideEffectKey, (instance, config) => { - // TODO optimise this because currently the whole Group rebuilds - // although only one Item value has changed which definitely needs no complete rebuild - // https://github.com/agile-ts/agile/issues/113 instance.collection().rebuildGroupsThatIncludeItemKey(itemKey, config); }, { weight: 100 } diff --git a/packages/core/src/logCodeManager.ts b/packages/core/src/logCodeManager.ts index fb5aaffc..5c1afbcf 100644 --- a/packages/core/src/logCodeManager.ts +++ b/packages/core/src/logCodeManager.ts @@ -305,7 +305,9 @@ if (process.env.NODE_ENV !== 'production') { log, logCodeLogTypes: logCodeTypes, logCodeMessages: logCodeMessages, - getLogger: loggerPackage.getLogger, + getLogger: () => { + return loggerPackage?.getLogger() ?? null; + }, logIfTags, setAllowLogging, }; diff --git a/packages/core/src/runtime/runtime.job.ts b/packages/core/src/runtime/runtime.job.ts index 54748c87..73dbd4b3 100644 --- a/packages/core/src/runtime/runtime.job.ts +++ b/packages/core/src/runtime/runtime.job.ts @@ -40,6 +40,7 @@ export class RuntimeJob { }, force: false, maxTriesToUpdate: 3, + any: {}, }); this.config = { @@ -47,6 +48,7 @@ export class RuntimeJob { force: config.force, sideEffects: config.sideEffects, maxTriesToUpdate: config.maxTriesToUpdate, + any: config.any, }; this.observer = observer; this.rerender = @@ -115,6 +117,10 @@ export interface RuntimeJobConfigInterface { * @default 3 */ maxTriesToUpdate?: number | null; + /** + * Anything unrelated that might be required by a side effect. + */ + any?: any; } export interface SideEffectConfigInterface { diff --git a/packages/core/src/state/state.enhanced.ts b/packages/core/src/state/state.enhanced.ts index fe3a4c67..a922d7cc 100644 --- a/packages/core/src/state/state.enhanced.ts +++ b/packages/core/src/state/state.enhanced.ts @@ -455,7 +455,7 @@ export class EnhancedState extends State { // Create Persistent (-> persist value) this.persistent = new StatePersistent(this, { - instantiate: _config.loadValue, + loadValue: _config.loadValue, storageKeys: _config.storageKeys, key: key, defaultStorageKey: _config.defaultStorageKey, diff --git a/packages/core/src/state/state.observer.ts b/packages/core/src/state/state.observer.ts index f2ef181c..332a9f9e 100644 --- a/packages/core/src/state/state.observer.ts +++ b/packages/core/src/state/state.observer.ts @@ -14,8 +14,8 @@ import { SubscriptionContainer, ObserverKey, defineConfig, + removeProperties, } from '../internal'; -import type { EnhancedState } from '../internal'; export class StateObserver extends Observer { // State the Observer belongs to @@ -88,15 +88,7 @@ export class StateObserver extends Observer { const state = this.state(); config = defineConfig(config, { perform: true, - background: false, - sideEffects: { - enabled: true, - exclude: [], - }, force: false, - storage: true, - overwrite: false, - maxTriesToUpdate: 3, }); // Force overwriting the State value if it is a placeholder. @@ -116,15 +108,12 @@ export class StateObserver extends Observer { // Create Runtime-Job const job = new StateRuntimeJob(this, { - storage: config.storage, - sideEffects: config.sideEffects, - force: config.force, - background: config.background, - overwrite: config.overwrite, - key: - config.key ?? - `${this._key != null ? this._key + '_' : ''}${generateId()}_value`, - maxTriesToUpdate: config.maxTriesToUpdate, + ...removeProperties(config, ['perform']), + ...{ + key: + config.key ?? + `${this._key != null ? this._key + '_' : ''}${generateId()}_value`, + }, }); // Pass created Job into the Runtime diff --git a/packages/core/src/state/state.persistent.ts b/packages/core/src/state/state.persistent.ts index 5a1d7f58..8d1fa0d9 100644 --- a/packages/core/src/state/state.persistent.ts +++ b/packages/core/src/state/state.persistent.ts @@ -25,10 +25,10 @@ export class StatePersistent extends Persistent { config: CreatePersistentConfigInterface = {} ) { super(state.agileInstance(), { - instantiate: false, + loadValue: false, }); config = defineConfig(config, { - instantiate: true, + loadValue: true, storageKeys: [], defaultStorageKey: null as any, }); @@ -40,7 +40,7 @@ export class StatePersistent extends Persistent { }); // Load/Store persisted value/s for the first time - if (this.ready && config.instantiate) this.initialLoading(); + if (this.ready && config.loadValue) this.initialLoading(); } /** diff --git a/packages/core/src/state/state.runtime.job.ts b/packages/core/src/state/state.runtime.job.ts index 16e1628a..2578c7f9 100644 --- a/packages/core/src/state/state.runtime.job.ts +++ b/packages/core/src/state/state.runtime.job.ts @@ -35,6 +35,7 @@ export class StateRuntimeJob extends RuntimeJob { storage: true, overwrite: false, maxTriesToUpdate: 3, + any: {}, }); this.config = { @@ -44,6 +45,7 @@ export class StateRuntimeJob extends RuntimeJob { storage: config.storage, overwrite: config.overwrite, maxTriesToUpdate: config.maxTriesToUpdate, + any: config.any, }; } } diff --git a/packages/core/src/storages/persistent.ts b/packages/core/src/storages/persistent.ts index 11deec56..f83aeae9 100644 --- a/packages/core/src/storages/persistent.ts +++ b/packages/core/src/storages/persistent.ts @@ -46,14 +46,14 @@ export class Persistent { this.agileInstance = () => agileInstance; this._key = Persistent.placeHolderKey; config = defineConfig(config, { - instantiate: true, + loadValue: true, storageKeys: [], defaultStorageKey: null as any, }); this.config = { defaultStorageKey: config.defaultStorageKey as any }; // Instantiate Persistent - if (config.instantiate) { + if (config.loadValue) { this.instantiatePersistent({ storageKeys: config.storageKeys, key: config.key, @@ -318,11 +318,11 @@ export interface CreatePersistentConfigInterface { */ defaultStorageKey?: StorageKey; /** - * Whether the Persistent should be instantiated immediately + * Whether the Persistent should load/persist the value immediately * or whether this should be done manually. * @default true */ - instantiate?: boolean; + loadValue?: boolean; } export interface PersistentConfigInterface { diff --git a/packages/core/tests/unit/collection/collection.persistent.test.ts b/packages/core/tests/unit/collection/collection.persistent.test.ts index b4f64ef1..4c44d3f8 100644 --- a/packages/core/tests/unit/collection/collection.persistent.test.ts +++ b/packages/core/tests/unit/collection/collection.persistent.test.ts @@ -61,6 +61,7 @@ describe('CollectionPersistent Tests', () => { }); expect(collectionPersistent.initialLoading).toHaveBeenCalled(); + // Check if Persistent was called with correct parameters expect(collectionPersistent._key).toBe(CollectionPersistent.placeHolderKey); expect(collectionPersistent.ready).toBeTruthy(); expect(collectionPersistent.isPersisted).toBeFalsy(); @@ -94,6 +95,7 @@ describe('CollectionPersistent Tests', () => { }); expect(collectionPersistent.initialLoading).toHaveBeenCalled(); + // Check if Persistent was called with correct parameters expect(collectionPersistent._key).toBe(CollectionPersistent.placeHolderKey); expect(collectionPersistent.ready).toBeTruthy(); expect(collectionPersistent.isPersisted).toBeFalsy(); @@ -124,6 +126,7 @@ describe('CollectionPersistent Tests', () => { }); expect(collectionPersistent.initialLoading).not.toHaveBeenCalled(); + // Check if Persistent was called with correct parameters expect(collectionPersistent._key).toBe(CollectionPersistent.placeHolderKey); expect(collectionPersistent.ready).toBeFalsy(); expect(collectionPersistent.isPersisted).toBeFalsy(); @@ -134,7 +137,7 @@ describe('CollectionPersistent Tests', () => { }); }); - it("should create CollectionPersistent and shouldn't call initialLoading if Persistent is ready (config.instantiate = false)", () => { + it("should create CollectionPersistent and shouldn't call initialLoading if Persistent is ready (config.loadValue = false)", () => { // Overwrite instantiatePersistent once to not call it and set ready property jest .spyOn(CollectionPersistent.prototype, 'instantiatePersistent') @@ -144,7 +147,7 @@ describe('CollectionPersistent Tests', () => { }); const collectionPersistent = new CollectionPersistent(dummyCollection, { - instantiate: false, + loadValue: false, }); expect(collectionPersistent).toBeInstanceOf(CollectionPersistent); @@ -156,6 +159,7 @@ describe('CollectionPersistent Tests', () => { }); expect(collectionPersistent.initialLoading).not.toHaveBeenCalled(); + // Check if Persistent was called with correct parameters expect(collectionPersistent._key).toBe(CollectionPersistent.placeHolderKey); expect(collectionPersistent.ready).toBeTruthy(); expect(collectionPersistent.isPersisted).toBeFalsy(); @@ -430,7 +434,10 @@ describe('CollectionPersistent Tests', () => { ).toHaveBeenCalledTimes(1); expect(dummyCollection.assignItem).toHaveBeenCalledWith( placeholderItem1, - { overwrite: true } + { + overwrite: true, + rebuildGroups: false, + } ); expect(placeholderItem1.isPersisted).toBeTruthy(); @@ -454,7 +461,11 @@ describe('CollectionPersistent Tests', () => { placeholderItem2?.persistent?.loadPersistedValue ).not.toHaveBeenCalled(); expect(dummyCollection.assignItem).not.toHaveBeenCalledWith( - placeholderItem2 + placeholderItem2, + { + overwrite: true, + rebuildGroups: false, + } ); // Because Item persistent isn't ready expect(placeholderItem2.isPersisted).toBeFalsy(); @@ -477,9 +488,12 @@ describe('CollectionPersistent Tests', () => { expect( placeholderItem3?.persistent?.loadPersistedValue ).toHaveBeenCalledTimes(1); - expect(dummyCollection.assignItem).not.toHaveBeenCalledWith( - placeholderItem3 - ); // Because Item persistent 'leadPersistedValue()' returned false -> Item properly doesn't exist in Storage + expect( + dummyCollection.assignItem + ).not.toHaveBeenCalledWith(placeholderItem3, { + overwrite: true, + rebuildGroups: false, + }); // Because Item persistent 'leadPersistedValue()' returned false -> Item properly doesn't exist in Storage expect(placeholderItem3.isPersisted).toBeFalsy(); expect(collectionPersistent.setupSideEffects).toHaveBeenCalledWith( @@ -561,7 +575,11 @@ describe('CollectionPersistent Tests', () => { '3' ); // Because Item 3 is already present in the Collection expect(dummyCollection.assignItem).not.toHaveBeenCalledWith( - placeholderItem3 + placeholderItem3, + { + overwrite: true, + rebuildGroups: false, + } ); // Because Item 3 is already present in the Collection // Placeholder Item 1 @@ -582,7 +600,10 @@ describe('CollectionPersistent Tests', () => { ).toHaveBeenCalledTimes(1); expect(dummyCollection.assignItem).toHaveBeenCalledWith( placeholderItem1, - { overwrite: true } + { + overwrite: true, + rebuildGroups: false, + } ); expect(placeholderItem1.isPersisted).toBeTruthy(); @@ -853,7 +874,9 @@ describe('CollectionPersistent Tests', () => { it("shouldn't add rebuild Storage side effect to the default Group", () => { collectionPersistent.setupSideEffects(); - expect(dummyDefaultGroup.addSideEffect).toHaveBeenCalledWith( + expect( + dummyDefaultGroup.addSideEffect + ).toHaveBeenCalledWith( CollectionPersistent.defaultGroupSideEffectKey, expect.any(Function), { weight: 0 } diff --git a/packages/core/tests/unit/collection/collection.test.ts b/packages/core/tests/unit/collection/collection.test.ts index 86a019a1..9dee6d0b 100644 --- a/packages/core/tests/unit/collection/collection.test.ts +++ b/packages/core/tests/unit/collection/collection.test.ts @@ -7,6 +7,7 @@ import { CollectionPersistent, ComputedTracker, StatePersistent, + TrackedChangeMethod, } from '../../../src'; import * as Utils from '@agile-ts/utils'; import { LogMock } from '../../helper/logMock'; @@ -264,10 +265,10 @@ describe('Collection Tests', () => { key: 'group1Key', }); - expect(collection.createGroup).toHaveBeenCalledWith( - 'group1Key', - [1, 2] - ); + expect(collection.createGroup).toHaveBeenCalledWith('group1Key', [ + 1, + 2, + ]); LogMock.hasLoggedCode('1B:02:00'); expect(response).toBeInstanceOf(Group); @@ -1820,19 +1821,19 @@ describe('Collection Tests', () => { }); describe('persist function tests', () => { - it('should create persistent with CollectionKey (default config)', () => { + it('should create Persistent with CollectionKey (default config)', () => { collection.persist(); expect(collection.persistent).toBeInstanceOf(CollectionPersistent); expect(CollectionPersistent).toHaveBeenCalledWith(collection, { - instantiate: true, + loadValue: true, storageKeys: [], key: collection._key, defaultStorageKey: null, }); }); - it('should create persistent with CollectionKey (specific config)', () => { + it('should create Persistent with CollectionKey (specific config)', () => { collection.persist({ storageKeys: ['test1', 'test2'], loadValue: false, @@ -1841,26 +1842,26 @@ describe('Collection Tests', () => { expect(collection.persistent).toBeInstanceOf(CollectionPersistent); expect(CollectionPersistent).toHaveBeenCalledWith(collection, { - instantiate: false, + loadValue: false, storageKeys: ['test1', 'test2'], key: collection._key, defaultStorageKey: 'test1', }); }); - it('should create persistent with passed Key (default config)', () => { + it('should create Persistent with passed Key (default config)', () => { collection.persist('passedKey'); expect(collection.persistent).toBeInstanceOf(CollectionPersistent); expect(CollectionPersistent).toHaveBeenCalledWith(collection, { - instantiate: true, + loadValue: true, storageKeys: [], key: 'passedKey', defaultStorageKey: null, }); }); - it('should create persistent with passed Key (specific config)', () => { + it('should create Persistent with passed Key (specific config)', () => { collection.persist('passedKey', { storageKeys: ['test1', 'test2'], loadValue: false, @@ -1869,14 +1870,14 @@ describe('Collection Tests', () => { expect(collection.persistent).toBeInstanceOf(CollectionPersistent); expect(CollectionPersistent).toHaveBeenCalledWith(collection, { - instantiate: false, + loadValue: false, storageKeys: ['test1', 'test2'], key: 'passedKey', defaultStorageKey: 'test1', }); }); - it("shouldn't overwrite existing persistent", () => { + it("shouldn't overwrite existing Persistent", () => { const dummyPersistent = new CollectionPersistent(collection); collection.persistent = dummyPersistent; collection.isPersisted = true; @@ -2871,7 +2872,7 @@ describe('Collection Tests', () => { collection.assignData = jest.fn(); }); - it('should assign valid Item to Collection (default config)', () => { + it('should assign valid Item to the Collection (default config)', () => { const response = collection.assignItem(toAddDummyItem2); expect(response).toBeTruthy(); @@ -2893,21 +2894,19 @@ describe('Collection Tests', () => { LogMock.hasNotLogged('warn'); }); - it('should assign valid Item to Collection (config.background = true)', () => { + it('should assign valid Item to the Collection (specific config)', () => { const response = collection.assignItem(toAddDummyItem2, { background: true, + rebuildGroups: false, }); expect(response).toBeTruthy(); expect(collection.size).toBe(2); expect(collection.data).toHaveProperty('dummyItem2'); expect(collection.data['dummyItem2']).toBe(toAddDummyItem2); - expect(collection.rebuildGroupsThatIncludeItemKey).toHaveBeenCalledWith( - 'dummyItem2', - { - background: true, - } - ); + expect( + collection.rebuildGroupsThatIncludeItemKey + ).not.toHaveBeenCalled(); expect(collection.assignData).not.toHaveBeenCalled(); expect(toAddDummyItem2.patch).not.toHaveBeenCalled(); @@ -3021,10 +3020,24 @@ describe('Collection Tests', () => { let dummyGroup1: Group; let dummyGroup2: Group; + let dummyItem1: Item; + let dummyItem2: Item; + beforeEach(() => { - dummyGroup1 = new Group(collection, ['dummyItem1', 'dummyItem2'], { - key: 'dummyGroup1', - }); + dummyItem1 = new Item(collection, { id: 'dummyItem1', name: 'Jeff' }); + dummyItem2 = new Item(collection, { id: 'dummyItem2', name: 'Jeff' }); + collection.data = { + dummyItem1: dummyItem1, + dummyItem2: dummyItem2, + }; + + dummyGroup1 = new Group( + collection, + ['dummyItem1', 'missingInCollectionItemKey', 'dummyItem2'], + { + key: 'dummyGroup1', + } + ); dummyGroup2 = new Group(collection, ['dummyItem2'], { key: 'dummyGroup2', }); @@ -3037,43 +3050,89 @@ describe('Collection Tests', () => { dummyGroup2.rebuild = jest.fn(); }); - it('should call ingest on each Group that includes the passed ItemKey (default config)', () => { + it('should update the Item in each Group (output) that includes the specified itemKey (default config)', () => { collection.rebuildGroupsThatIncludeItemKey('dummyItem1'); - expect(dummyGroup1.rebuild).toHaveBeenCalledWith({ - background: false, - sideEffects: { - enabled: true, - exclude: [], - }, - storage: false, - }); + // Group 1 + expect(dummyGroup1.rebuild).toHaveBeenCalledWith( + [ + { + key: 'dummyItem1', + index: 0, + method: TrackedChangeMethod.UPDATE, + }, + ], + {} + ); + + // Group 2 expect(dummyGroup2.rebuild).not.toHaveBeenCalled(); }); - it('should call ingest on each Group that includes the passed ItemKey (specific config)', () => { + it('should update the Item in each Group (output) that includes the specified itemKey (specific config)', () => { collection.rebuildGroupsThatIncludeItemKey('dummyItem2', { + key: 'frank', background: true, - sideEffects: { - enabled: false, - }, + force: true, }); - expect(dummyGroup1.rebuild).toHaveBeenCalledWith({ - background: true, - sideEffects: { - enabled: false, - }, - storage: false, - }); - expect(dummyGroup2.rebuild).toHaveBeenCalledWith({ - background: true, - sideEffects: { - enabled: false, - }, - storage: false, - }); + // Group 1 + expect(dummyGroup1.rebuild).toHaveBeenCalledWith( + [ + { + key: 'dummyItem2', + index: 1, + method: TrackedChangeMethod.UPDATE, + }, + ], + { + key: 'frank', + background: true, + force: true, + } + ); + + // Group 2 + expect(dummyGroup2.rebuild).toHaveBeenCalledWith( + [ + { + key: 'dummyItem2', + index: 0, + method: TrackedChangeMethod.UPDATE, + }, + ], + { + key: 'frank', + background: true, + force: true, + } + ); }); + + it( + 'should update the Item in each Group (output) that includes the specified itemKey ' + + "although the Item doesn't exist in the Group output yet", + () => { + collection.rebuildGroupsThatIncludeItemKey( + 'missingInCollectionItemKey' + ); + + // Group 1 + expect(dummyGroup1.rebuild).toHaveBeenCalledWith( + [ + { + key: 'missingInCollectionItemKey', + index: 1, + method: TrackedChangeMethod.ADD, + }, + ], + {} + ); + + // Group 2 + expect(dummyGroup2.rebuild).not.toHaveBeenCalled(); + } + ); }); }); }); diff --git a/packages/core/tests/unit/collection/group/group.observer.test.ts b/packages/core/tests/unit/collection/group/group.observer.test.ts index 488147b1..24b30205 100644 --- a/packages/core/tests/unit/collection/group/group.observer.test.ts +++ b/packages/core/tests/unit/collection/group/group.observer.test.ts @@ -110,31 +110,41 @@ describe('GroupObserver Tests', () => { describe('ingest function tests', () => { beforeEach(() => { - dummyGroup.rebuild = jest.fn(); + groupObserver.ingestOutput = jest.fn(); }); - it('should rebuild the Group and ingests it into the runtime (default config)', () => { + it('should call ingestOutput with nextGroupOutput (default config)', () => { + groupObserver.group().nextGroupOutput = 'jeff' as any; + groupObserver.ingest(); - expect(dummyGroup.rebuild).toHaveBeenCalledWith({}); + expect(groupObserver.ingestOutput).toHaveBeenCalledWith( + groupObserver.group().nextGroupOutput, + {} + ); }); - it('should rebuild the Group and ingests it into the runtime (specific config)', () => { + it('should call ingestOutput with nextGroupOutput (specific config)', () => { + groupObserver.group().nextGroupOutput = 'jeff' as any; + groupObserver.ingest({ background: true, force: true, maxTriesToUpdate: 5, }); - expect(dummyGroup.rebuild).toHaveBeenCalledWith({ - background: true, - force: true, - maxTriesToUpdate: 5, - }); + expect(groupObserver.ingestOutput).toHaveBeenCalledWith( + groupObserver.group().nextGroupOutput, + { + background: true, + force: true, + maxTriesToUpdate: 5, + } + ); }); }); - describe('ingestItems function tests', () => { + describe('ingestOutput function tests', () => { beforeEach(() => { dummyAgile.runtime.ingest = jest.fn(); }); @@ -155,10 +165,11 @@ describe('GroupObserver Tests', () => { }, force: false, maxTriesToUpdate: 3, + any: {}, }); }); - groupObserver.ingestItems([dummyItem1, dummyItem2]); + groupObserver.ingestOutput([dummyItem1._value, dummyItem2._value]); expect(groupObserver.nextGroupOutput).toStrictEqual([ dummyItem1._value, @@ -187,10 +198,11 @@ describe('GroupObserver Tests', () => { }, force: true, maxTriesToUpdate: 5, + any: {}, }); }); - groupObserver.ingestItems([dummyItem1, dummyItem2], { + groupObserver.ingestOutput([dummyItem1._value, dummyItem2._value], { perform: false, force: true, sideEffects: { @@ -219,7 +231,7 @@ describe('GroupObserver Tests', () => { () => { dummyGroup._output = [dummyItem1._value, dummyItem2._value]; - groupObserver.ingestItems([dummyItem1, dummyItem2]); + groupObserver.ingestOutput([dummyItem1._value, dummyItem2._value]); expect(groupObserver.nextGroupOutput).toStrictEqual([ dummyItem1._value, @@ -246,10 +258,13 @@ describe('GroupObserver Tests', () => { }, force: true, maxTriesToUpdate: 3, + any: {}, }); }); - groupObserver.ingestItems([dummyItem1, dummyItem2], { force: true }); + groupObserver.ingestOutput([dummyItem1._value, dummyItem2._value], { + force: true, + }); expect(groupObserver.nextGroupOutput).toStrictEqual([ dummyItem1._value, @@ -277,11 +292,12 @@ describe('GroupObserver Tests', () => { }, force: true, maxTriesToUpdate: 3, + any: {}, }); }); dummyGroup.isPlaceholder = true; - groupObserver.ingestItems([dummyItem1, dummyItem2]); + groupObserver.ingestOutput([dummyItem1._value, dummyItem2._value]); expect(groupObserver.nextGroupOutput).toStrictEqual([ dummyItem1._value, @@ -312,6 +328,7 @@ describe('GroupObserver Tests', () => { ]; dummyJob.observer.value = [dummyItem1._value]; dummyGroup._output = [dummyItem1._value]; + dummyGroup.nextGroupOutput = [dummyItem1._value]; groupObserver.perform(dummyJob); @@ -319,6 +336,10 @@ describe('GroupObserver Tests', () => { dummyItem1._value, dummyItem2._value, ]); + expect(dummyGroup.nextGroupOutput).toStrictEqual([ + dummyItem1._value, + dummyItem2._value, + ]); expect(groupObserver.value).toStrictEqual([ dummyItem1._value, diff --git a/packages/core/tests/unit/collection/group/group.test.ts b/packages/core/tests/unit/collection/group/group.test.ts index 17574a02..b544c54c 100644 --- a/packages/core/tests/unit/collection/group/group.test.ts +++ b/packages/core/tests/unit/collection/group/group.test.ts @@ -1,13 +1,14 @@ import { - Group, Agile, Collection, - StateObserver, - ComputedTracker, - Item, CollectionPersistent, - GroupObserver, + ComputedTracker, EnhancedState, + Group, + GroupObserver, + Item, + StateObserver, + TrackedChangeMethod, } from '../../../../src'; import { LogMock } from '../../../helper/logMock'; @@ -47,7 +48,9 @@ describe('Group Tests', () => { expect(group.collection()).toBe(dummyCollection); expect(group._output).toStrictEqual([]); + expect(group.nextGroupOutput).toStrictEqual([]); expect(group.notFoundItemKeys).toStrictEqual([]); + expect(group.loadedInitialValue).toBeTruthy(); // Check if State was called with correct parameters expect(group._key).toBeUndefined(); @@ -86,7 +89,9 @@ describe('Group Tests', () => { expect(group.collection()).toBe(dummyCollection); expect(group._output).toStrictEqual([]); + expect(group.nextGroupOutput).toStrictEqual([]); expect(group.notFoundItemKeys).toStrictEqual([]); + expect(group.loadedInitialValue).toBeTruthy(); // Check if State was called with correct parameters expect(group._key).toBe('dummyKey'); @@ -122,7 +127,9 @@ describe('Group Tests', () => { expect(group.collection()).toBe(dummyCollection); expect(group._output).toStrictEqual([]); + expect(group.nextGroupOutput).toStrictEqual([]); expect(group.notFoundItemKeys).toStrictEqual([]); + expect(group.loadedInitialValue).toBeTruthy(); // Check if State was called with correct parameters expect(group._key).toBeUndefined(); @@ -153,14 +160,12 @@ describe('Group Tests', () => { group = new Group(dummyCollection, [], { key: 'groupKey', }); - dummyCollection.collect({ id: 'dummyItem1Key', name: 'coolName' }); - dummyCollection.collect({ id: 'dummyItem2Key', name: 'coolName' }); + dummyCollection.collect({ id: 'dummyItem1Key', name: 'jeff' }); + dummyCollection.collect({ id: 'dummyItem2Key', name: 'frank' }); + dummyCollection.collect({ id: 'dummyItem3Key', name: 'hans' }); dummyItem1 = dummyCollection.getItem('dummyItem1Key') as any; dummyItem2 = dummyCollection.getItem('dummyItem2Key') as any; - dummyItem3 = new Item(dummyCollection, { - id: 'dummyItem3Key', - name: 'coolName', - }); + dummyItem3 = dummyCollection.getItem('dummyItem3Key') as any; }); describe('output get function tests', () => { @@ -224,122 +229,191 @@ describe('Group Tests', () => { describe('remove function tests', () => { beforeEach(() => { - group.nextStateValue = [ + group._value = [ 'dummyItem1Key', 'dummyItem2Key', - 'dummyItem3Key', + 'missingInCollectionItemKey', ]; + group.nextStateValue = group._value; + group._preciseItemKeys = ['dummyItem1Key', 'dummyItem2Key']; + group.set = jest.fn(); }); - it('should remove Item from Group not in background (default config)', () => { + it('should remove Item from Group (default config)', () => { group.remove('dummyItem1Key'); expect(group.set).toHaveBeenCalledWith( - ['dummyItem2Key', 'dummyItem3Key'], - {} + ['dummyItem2Key', 'missingInCollectionItemKey'], + { + any: { + trackedChanges: [ + { + index: 0, + method: TrackedChangeMethod.REMOVE, + key: 'dummyItem1Key', + }, + ], + }, + } ); }); - it('should remove Item from Group in background (config.background = true)', () => { - group.remove('dummyItem1Key', { background: true }); + it('should remove Item from Group (specific config)', () => { + group.remove('dummyItem1Key', { + background: true, + force: true, + storage: false, + softRebuild: false, + }); expect(group.set).toHaveBeenCalledWith( - ['dummyItem2Key', 'dummyItem3Key'], - { background: true } + ['dummyItem2Key', 'missingInCollectionItemKey'], + { + background: true, + force: true, + storage: false, + any: { trackedChanges: [] }, + } ); }); - it("shouldn't remove not existing Item from Group (default config)", () => { + it("shouldn't remove not existing Item from Group", () => { group.remove('notExistingKey'); expect(group.set).not.toHaveBeenCalled(); }); - it("should remove Item from Group that doesn't exist in Collection in background (default config)", () => { - group.remove('dummyItem3Key'); - - expect(group.set).toHaveBeenCalledWith( - ['dummyItem1Key', 'dummyItem2Key'], - { background: true } - ); - }); - - it('should remove Items from Group not in background (default config)', () => { - group.remove(['dummyItem1Key', 'notExistingItemKey', 'dummyItem3Key']); + it('should remove Items from Group', () => { + group.remove([ + 'dummyItem1Key', + 'notExistingItemKey', + 'missingInCollectionItemKey', + 'dummyItem2Key', + ]); - expect(group.set).toHaveBeenCalledWith(['dummyItem2Key'], {}); + expect(group.set).toHaveBeenCalledWith([], { + any: { + trackedChanges: [ + { + index: 0, + method: TrackedChangeMethod.REMOVE, + key: 'dummyItem1Key', + }, + { + index: 0, + method: TrackedChangeMethod.REMOVE, + key: 'dummyItem2Key', + }, + ], + }, + }); }); - it("should remove Items from Group in background if passing not existing Item and Item that doesn't exist in Collection (default config)", () => { - group.remove(['notExistingItemKey', 'dummyItem3Key']); + it("should remove Item/s from Group that doesn't exist in the Collection in background", () => { + group.remove('missingInCollectionItemKey'); expect(group.set).toHaveBeenCalledWith( ['dummyItem1Key', 'dummyItem2Key'], - { background: true } + { + background: true, + any: { trackedChanges: [] }, + } ); }); + + it( + 'should remove Items from Group in background ' + + 'if passing not existing Items to remove ' + + "and Items that doesn't exist in the Collection", + () => { + group.remove(['notExistingItemKey', 'missingInCollectionItemKey']); + + expect(group.set).toHaveBeenCalledWith( + ['dummyItem1Key', 'dummyItem2Key'], + { + background: true, + any: { trackedChanges: [] }, + } + ); + } + ); }); describe('add function tests', () => { beforeEach(() => { - group.nextStateValue = ['placeholder', 'dummyItem1Key', 'placeholder']; + group._value = ['placeholder', 'dummyItem1Key', 'placeholder']; + group.nextStateValue = group._value; + group._preciseItemKeys = ['dummyItem1Key']; + group.set = jest.fn(); }); - it('should add Item to Group at the end not in background (default config)', () => { + it('should add Item at the end of the Group (default config)', () => { group.add('dummyItem2Key'); expect(group.set).toHaveBeenCalledWith( ['placeholder', 'dummyItem1Key', 'placeholder', 'dummyItem2Key'], - {} - ); - }); - - it("should add Item to Group at the beginning not in background (config.method = 'unshift')", () => { - group.add('dummyItem2Key', { method: 'unshift' }); - - expect(group.set).toHaveBeenCalledWith( - ['dummyItem2Key', 'placeholder', 'dummyItem1Key', 'placeholder'], - {} + { + any: { + trackedChanges: [ + { + index: 1, + method: TrackedChangeMethod.ADD, + key: 'dummyItem2Key', + }, + ], + }, + } ); }); - it('should add Item to Group at the end in background (config.background = true)', () => { - group.add('dummyItem2Key', { background: true }); + it('should add Item at the end of the Group (specific config)', () => { + group.add('dummyItem2Key', { + background: true, + force: true, + storage: false, + softRebuild: false, + }); expect(group.set).toHaveBeenCalledWith( ['placeholder', 'dummyItem1Key', 'placeholder', 'dummyItem2Key'], - { background: true } + { + background: true, + force: true, + storage: false, + any: { trackedChanges: [] }, + } ); }); - it("should add Item to Group at the end that doesn't exist in Collection in background (default config)", () => { - group.add('dummyItem3Key'); + it("should add Item at the beginning of the Group (config.method = 'unshift')", () => { + group.add('dummyItem2Key', { method: 'unshift' }); expect(group.set).toHaveBeenCalledWith( - ['placeholder', 'dummyItem1Key', 'placeholder', 'dummyItem3Key'], - { background: true } + ['dummyItem2Key', 'placeholder', 'dummyItem1Key', 'placeholder'], + { + any: { + trackedChanges: [ + { + index: 0, + method: TrackedChangeMethod.ADD, + key: 'dummyItem2Key', + }, + ], + }, + } ); }); - it("shouldn't add existing Item to Group again (default config)", () => { + it("shouldn't add already existing Item to the Group (default config)", () => { group.add('dummyItem1Key'); expect(group.set).not.toHaveBeenCalled(); }); - it('should remove existingItem and add it again at the end to the Group not in background (config.overwrite = true)', () => { - group.add('dummyItem1Key', { overwrite: true }); - - expect(group.set).toHaveBeenCalledWith( - ['placeholder', 'placeholder', 'dummyItem1Key'], - {} - ); - }); - - it('should add Items to Group at the end not in background (default config)', () => { - group.add(['dummyItem1Key', 'dummyItem2Key', 'dummyItem3Key']); + it('should add Items at the end of the Group', () => { + group.add(['dummyItem1Key', 'dummyItem2Key', 'notExistingItemKey']); expect(group.set).toHaveBeenCalledWith( [ @@ -347,20 +421,76 @@ describe('Group Tests', () => { 'dummyItem1Key', 'placeholder', 'dummyItem2Key', - 'dummyItem3Key', + 'notExistingItemKey', ], - {} + { + any: { + trackedChanges: [ + { + index: 1, + method: TrackedChangeMethod.ADD, + key: 'dummyItem2Key', + }, + { + index: 2, + method: TrackedChangeMethod.ADD, + key: 'notExistingItemKey', + }, + ], + }, + } ); }); - it('should add Items toGroup at the end in background if passing existing Item and in Collection not existing Item (default config)', () => { - group.add(['dummyItem1Key', 'dummyItem3Key']); + it("should add Item that doesn't exist in Collection at the end of the Group in background", () => { + group.add('notExistingItemKey'); expect(group.set).toHaveBeenCalledWith( - ['placeholder', 'dummyItem1Key', 'placeholder', 'dummyItem3Key'], - { background: true } + ['placeholder', 'dummyItem1Key', 'placeholder', 'notExistingItemKey'], + { + background: true, + any: { + trackedChanges: [ + { + index: 1, + method: TrackedChangeMethod.ADD, + key: 'notExistingItemKey', + }, + ], + }, + } ); }); + + it( + 'should add Items at the end of the Group in background ' + + 'if passing already added Items ' + + "and Items that doesn't exist in the Collection", + () => { + group.add(['dummyItem1Key', 'notExistingItemKey']); + + expect(group.set).toHaveBeenCalledWith( + [ + 'placeholder', + 'dummyItem1Key', + 'placeholder', + 'notExistingItemKey', + ], + { + background: true, + any: { + trackedChanges: [ + { + index: 1, + method: TrackedChangeMethod.ADD, + key: 'notExistingItemKey', + }, + ], + }, + } + ); + } + ); }); describe('replace function tests', () => { @@ -395,7 +525,7 @@ describe('Group Tests', () => { describe('getItems function tests', () => { beforeEach(() => { - group._value = ['dummyItem1Key', 'dummyItem3Key', 'dummyItem2Key']; + group._value = ['dummyItem1Key', 'notExistingItemKey', 'dummyItem2Key']; }); it('should return all existing Items of the Group', () => { @@ -511,54 +641,210 @@ describe('Group Tests', () => { describe('rebuild function tests', () => { beforeEach(() => { - group._value = ['dummyItem1Key', 'dummyItem3Key', 'dummyItem2Key']; - group.observers['output'].ingestItems = jest.fn(); - }); - - it('should ingest the built Group output and set notFoundItemKeys to the not found Item Keys (default config)', () => { - group.rebuild(); - - expect(group.notFoundItemKeys).toStrictEqual(['dummyItem3Key']); - expect(group._output).toStrictEqual([]); // because of mocking 'ingestValue' - expect(group.observers['output'].ingestItems).toHaveBeenCalledWith( - [dummyItem1, dummyItem2], - {} - ); - - LogMock.hasLoggedCode( - '1C:02:00', - [dummyCollection._key, group._key], - ['dummyItem3Key'] - ); - }); - - it('should ingest the built Group output and set notFoundItemKeys to the not found Item Keys (specific config)', () => { - group.rebuild({ storage: true, overwrite: true, background: false }); - - expect(group.notFoundItemKeys).toStrictEqual(['dummyItem3Key']); - expect(group._output).toStrictEqual([]); // because of mocking 'ingestValue' - expect(group.observers['output'].ingestItems).toHaveBeenCalledWith( - [dummyItem1, dummyItem2], - { storage: true, overwrite: true, background: false } - ); + group._value = [ + 'dummyItem1Key', + 'missingInCollectionItemKey', + 'dummyItem2Key', + 'dummyItem3Key', + ]; + group.observers['output'].ingestOutput = jest.fn(); + group.observers['output'].ingest = jest.fn(); + }); + + it( + 'should hard rebuild the Group if no trackedChanges were specified ' + + 'and set notExistingItemKeys to the not found Item Keys (default config)', + () => { + group.rebuild(); + + expect(group.notFoundItemKeys).toStrictEqual([ + 'missingInCollectionItemKey', + ]); + expect(group.observers['output'].ingestOutput).toHaveBeenCalledWith( + [dummyItem1._value, dummyItem2._value, dummyItem3._value], + {} + ); + expect(group.observers['output'].ingest).not.toHaveBeenCalled(); + + LogMock.hasLoggedCode( + '1C:02:00', + [dummyCollection._key, group._key], + ['missingInCollectionItemKey'] + ); + } + ); + + it( + 'should hard rebuild the Group if no trackedChanges were specified ' + + 'and set notExistingItemKeys to the not found Item Keys (specific config)', + () => { + group.rebuild([], { background: true, force: false, key: 'frank' }); + + expect(group.notFoundItemKeys).toStrictEqual([ + 'missingInCollectionItemKey', + ]); + expect(group.observers['output'].ingestOutput).toHaveBeenCalledWith( + [dummyItem1._value, dummyItem2._value, dummyItem3._value], + { background: true, force: false, key: 'frank' } + ); + + LogMock.hasLoggedCode( + '1C:02:00', + [dummyCollection._key, group._key], + ['missingInCollectionItemKey'] + ); + } + ); + + it( + 'should soft rebuild the Group if trackedChanges were specified ' + + 'and set notExistingItemKeys to the not found itemKeys (ADD)', + () => { + group.nextGroupOutput = [{ id: 'dummyItem1Key', name: 'jeff' }]; + group._preciseItemKeys = ['dummyItem1Key']; + + group.rebuild( + [ + { + index: 1, + method: TrackedChangeMethod.ADD, + key: 'dummyItem3Key', + }, + { + index: 2, + method: TrackedChangeMethod.ADD, + key: 'missingInCollectionItemKey', + }, + ], + { key: 'test', background: true } + ); + + expect(group.notFoundItemKeys).toStrictEqual([ + 'missingInCollectionItemKey', + ]); + expect(group.observers['output'].ingestOutput).not.toHaveBeenCalled(); + expect(group.observers['output'].ingest).toHaveBeenCalledWith({ + key: 'test', + background: true, + }); + expect(group.nextGroupOutput).toStrictEqual([ + { id: 'dummyItem1Key', name: 'jeff' }, + { id: 'dummyItem3Key', name: 'hans' }, + ]); + expect(group._preciseItemKeys).toStrictEqual([ + 'dummyItem1Key', + 'dummyItem3Key', + ]); + + LogMock.hasLoggedCode( + '1C:02:00', + [dummyCollection._key, group._key], + ['missingInCollectionItemKey'] + ); + } + ); + + it('should soft rebuild the Group if trackedChanges were specified (REMOVE)', () => { + group.nextGroupOutput = [ + { id: 'dummyItem1Key', name: 'jeff' }, + { id: 'dummyItem2Key', name: 'frank' }, + { id: 'dummyItem3Key', name: 'hans' }, + ]; + group._preciseItemKeys = [ + 'dummyItem1Key', + 'dummyItem2Key', + 'dummyItem3Key', + ]; - LogMock.hasLoggedCode( - '1C:02:00', - [dummyCollection._key, group._key], - ['dummyItem3Key'] + group.rebuild( + [ + { + index: 1, + method: TrackedChangeMethod.REMOVE, + key: 'dummyItem2Key', + }, + ], + { key: 'test', background: true } ); - }); - - it("shouldn't intest the build Group output if the Collection was not properly instantiated", () => { - dummyCollection.isInstantiated = false; - - group.rebuild(); expect(group.notFoundItemKeys).toStrictEqual([]); - expect(group._output).toStrictEqual([]); - expect(group.observers['output'].ingestItems).not.toHaveBeenCalled(); + expect(group.observers['output'].ingestOutput).not.toHaveBeenCalled(); + expect(group.observers['output'].ingest).toHaveBeenCalledWith({ + key: 'test', + background: true, + }); + expect(group.nextGroupOutput).toStrictEqual([ + { id: 'dummyItem1Key', name: 'jeff' }, + { id: 'dummyItem3Key', name: 'hans' }, + ]); + expect(group._preciseItemKeys).toStrictEqual([ + 'dummyItem1Key', + 'dummyItem3Key', + ]); + LogMock.hasNotLogged('warn'); }); + + it( + 'should soft rebuild the Group if trackedChanges were specified ' + + 'and set notExistingItemKeys to the not found itemKeys (UPDATE)', + () => { + dummyItem1._value = { id: 'dummyItem1Key', name: 'frank' }; + group.nextGroupOutput = [{ id: 'dummyItem1Key', name: 'jeff' }]; + group._preciseItemKeys = ['dummyItem1Key']; + + group.rebuild( + [ + { + index: 0, + method: TrackedChangeMethod.UPDATE, + key: 'dummyItem1Key', + }, + { + index: 1, + method: TrackedChangeMethod.UPDATE, + key: 'missingInCollectionItemKey', + }, + ], + { key: 'test', background: true } + ); + + expect(group.notFoundItemKeys).toStrictEqual([ + 'missingInCollectionItemKey', + ]); + expect(group.observers['output'].ingestOutput).not.toHaveBeenCalled(); + expect(group.observers['output'].ingest).toHaveBeenCalledWith({ + key: 'test', + background: true, + }); + expect(group.nextGroupOutput).toStrictEqual([ + { id: 'dummyItem1Key', name: 'frank' }, + ]); + expect(group._preciseItemKeys).toStrictEqual(['dummyItem1Key']); + + LogMock.hasLoggedCode( + '1C:02:00', + [dummyCollection._key, group._key], + ['missingInCollectionItemKey'] + ); + } + ); + + it( + "shouldn't ingest the build Group output " + + 'if the Collection was not properly instantiated', + () => { + dummyCollection.isInstantiated = false; + + group.rebuild(); + + expect(group.notFoundItemKeys).toStrictEqual([]); + expect(group.observers['output'].ingestOutput).not.toHaveBeenCalled(); + expect(group.observers['output'].ingest).not.toHaveBeenCalled(); + + LogMock.hasNotLogged('warn'); + } + ); }); }); }); diff --git a/packages/core/tests/unit/runtime/observer.test.ts b/packages/core/tests/unit/runtime/observer.test.ts index 5810c138..5acbcf64 100644 --- a/packages/core/tests/unit/runtime/observer.test.ts +++ b/packages/core/tests/unit/runtime/observer.test.ts @@ -103,6 +103,7 @@ describe('Observer Tests', () => { }, force: false, maxTriesToUpdate: 3, + any: {}, }); }); @@ -128,6 +129,7 @@ describe('Observer Tests', () => { }, force: true, maxTriesToUpdate: 3, + any: {}, }); }); diff --git a/packages/core/tests/unit/runtime/runtime.job.test.ts b/packages/core/tests/unit/runtime/runtime.job.test.ts index 9d0845f2..a93c132c 100644 --- a/packages/core/tests/unit/runtime/runtime.job.test.ts +++ b/packages/core/tests/unit/runtime/runtime.job.test.ts @@ -36,6 +36,7 @@ describe('RuntimeJob Tests', () => { }, force: false, maxTriesToUpdate: 3, + any: {}, }); expect(job.rerender).toBeTruthy(); expect(job.performed).toBeFalsy(); @@ -60,6 +61,7 @@ describe('RuntimeJob Tests', () => { }, force: true, maxTriesToUpdate: 10, + any: { jeff: 'frank' }, }); expect(job._key).toBe('dummyJob'); @@ -72,6 +74,7 @@ describe('RuntimeJob Tests', () => { }, force: true, maxTriesToUpdate: 10, + any: { jeff: 'frank' }, }); expect(job.rerender).toBeTruthy(); expect(job.performed).toBeFalsy(); @@ -97,6 +100,7 @@ describe('RuntimeJob Tests', () => { }, force: false, maxTriesToUpdate: 3, + any: {}, }); expect(job.rerender).toBeFalsy(); expect(job.performed).toBeFalsy(); @@ -124,6 +128,7 @@ describe('RuntimeJob Tests', () => { }, force: false, maxTriesToUpdate: 3, + any: {}, }); expect(job.rerender).toBeFalsy(); expect(job.performed).toBeFalsy(); diff --git a/packages/core/tests/unit/state/state.enhanced.test.ts b/packages/core/tests/unit/state/state.enhanced.test.ts index 7c80127c..97afe465 100644 --- a/packages/core/tests/unit/state/state.enhanced.test.ts +++ b/packages/core/tests/unit/state/state.enhanced.test.ts @@ -415,19 +415,19 @@ describe('Enhanced State Tests', () => { }); describe('persist function tests', () => { - it('should create persistent with StateKey (default config)', () => { + it('should create Persistent with StateKey (default config)', () => { numberState.persist(); expect(numberState.persistent).toBeInstanceOf(StatePersistent); expect(StatePersistent).toHaveBeenCalledWith(numberState, { - instantiate: true, + loadValue: true, storageKeys: [], key: numberState._key, defaultStorageKey: null, }); }); - it('should create persistent with StateKey (specific config)', () => { + it('should create Persistent with StateKey (specific config)', () => { numberState.persist({ storageKeys: ['test1', 'test2'], loadValue: false, @@ -436,26 +436,26 @@ describe('Enhanced State Tests', () => { expect(numberState.persistent).toBeInstanceOf(StatePersistent); expect(StatePersistent).toHaveBeenCalledWith(numberState, { - instantiate: false, + loadValue: false, storageKeys: ['test1', 'test2'], key: numberState._key, defaultStorageKey: 'test1', }); }); - it('should create persistent with passed Key (default config)', () => { + it('should create Persistent with passed Key (default config)', () => { numberState.persist('passedKey'); expect(numberState.persistent).toBeInstanceOf(StatePersistent); expect(StatePersistent).toHaveBeenCalledWith(numberState, { - instantiate: true, + loadValue: true, storageKeys: [], key: 'passedKey', defaultStorageKey: null, }); }); - it('should create persistent with passed Key (specific config)', () => { + it('should create Persistent with passed Key (specific config)', () => { numberState.persist('passedKey', { storageKeys: ['test1', 'test2'], loadValue: false, @@ -464,7 +464,7 @@ describe('Enhanced State Tests', () => { expect(numberState.persistent).toBeInstanceOf(StatePersistent); expect(StatePersistent).toHaveBeenCalledWith(numberState, { - instantiate: false, + loadValue: false, storageKeys: ['test1', 'test2'], key: 'passedKey', defaultStorageKey: 'test1', diff --git a/packages/core/tests/unit/state/state.observer.test.ts b/packages/core/tests/unit/state/state.observer.test.ts index e6e64367..2c348943 100644 --- a/packages/core/tests/unit/state/state.observer.test.ts +++ b/packages/core/tests/unit/state/state.observer.test.ts @@ -183,6 +183,7 @@ describe('StateObserver Tests', () => { storage: true, overwrite: false, maxTriesToUpdate: 3, + any: {}, }); }); @@ -214,6 +215,7 @@ describe('StateObserver Tests', () => { storage: true, overwrite: true, maxTriesToUpdate: 5, + any: {}, }); }); @@ -270,6 +272,7 @@ describe('StateObserver Tests', () => { storage: true, overwrite: false, maxTriesToUpdate: 3, + any: {}, }); }); @@ -300,6 +303,7 @@ describe('StateObserver Tests', () => { storage: true, overwrite: true, maxTriesToUpdate: 3, + any: {}, }); }); dummyState.isPlaceholder = true; diff --git a/packages/core/tests/unit/state/state.persistent.test.ts b/packages/core/tests/unit/state/state.persistent.test.ts index 1fa45381..95591c38 100644 --- a/packages/core/tests/unit/state/state.persistent.test.ts +++ b/packages/core/tests/unit/state/state.persistent.test.ts @@ -32,41 +32,44 @@ describe('StatePersistent Tests', () => { jest.clearAllMocks(); }); - it("should create StatePersistent and shouldn't call initialLoading if Persistent isn't ready (default config)", () => { - // Overwrite instantiatePersistent once to not call it and set ready property + it('should create StatePersistent and should call initialLoading if Persistent is ready (default config)', () => { + // Overwrite instantiatePersistent once to not call it jest .spyOn(StatePersistent.prototype, 'instantiatePersistent') .mockImplementationOnce(function () { // @ts-ignore - this.ready = false; + this.ready = true; }); const statePersistent = new StatePersistent(dummyState); expect(statePersistent).toBeInstanceOf(StatePersistent); - expect(statePersistent.state()).toBe(dummyState); + expect(statePersistent.instantiatePersistent).toHaveBeenCalledWith({ key: undefined, storageKeys: [], defaultStorageKey: null, }); - expect(statePersistent.initialLoading).not.toHaveBeenCalled(); + expect(statePersistent.initialLoading).toHaveBeenCalled(); + // Check if Persistent was called with correct parameters expect(statePersistent._key).toBe(StatePersistent.placeHolderKey); - expect(statePersistent.ready).toBeFalsy(); + expect(statePersistent.ready).toBeTruthy(); expect(statePersistent.isPersisted).toBeFalsy(); expect(statePersistent.onLoad).toBeUndefined(); expect(statePersistent.storageKeys).toStrictEqual([]); - expect(statePersistent.config).toStrictEqual({ defaultStorageKey: null }); + expect(statePersistent.config).toStrictEqual({ + defaultStorageKey: null, // gets set in 'instantiatePersistent' which is mocked + }); }); - it("should create StatePersistent and shouldn't call initialLoading if Persistent isn't ready (specific config)", () => { + it('should create StatePersistent and should call initialLoading if Persistent is ready (specific config)', () => { // Overwrite instantiatePersistent once to not call it and set ready property jest .spyOn(StatePersistent.prototype, 'instantiatePersistent') .mockImplementationOnce(function () { // @ts-ignore - this.ready = false; + this.ready = true; }); const statePersistent = new StatePersistent(dummyState, { @@ -81,10 +84,11 @@ describe('StatePersistent Tests', () => { storageKeys: ['test1', 'test2'], defaultStorageKey: 'test2', }); - expect(statePersistent.initialLoading).not.toHaveBeenCalled(); + expect(statePersistent.initialLoading).toHaveBeenCalled(); + // Check if Persistent was called with correct parameters expect(statePersistent._key).toBe(StatePersistent.placeHolderKey); - expect(statePersistent.ready).toBeFalsy(); + expect(statePersistent.ready).toBeTruthy(); expect(statePersistent.isPersisted).toBeFalsy(); expect(statePersistent.onLoad).toBeUndefined(); expect(statePersistent.storageKeys).toStrictEqual([]); @@ -93,21 +97,36 @@ describe('StatePersistent Tests', () => { }); }); - it('should create StatePersistent and should call initialLoading if Persistent is ready (default config)', () => { - // Overwrite instantiatePersistent once to not call it + it("should create StatePersistent and shouldn't call initialLoading if Persistent isn't ready", () => { + // Overwrite instantiatePersistent once to not call it and set ready property jest .spyOn(StatePersistent.prototype, 'instantiatePersistent') .mockImplementationOnce(function () { // @ts-ignore - this.ready = true; + this.ready = false; }); const statePersistent = new StatePersistent(dummyState); - expect(statePersistent.initialLoading).toHaveBeenCalled(); + expect(statePersistent).toBeInstanceOf(StatePersistent); + expect(statePersistent.state()).toBe(dummyState); + expect(statePersistent.instantiatePersistent).toHaveBeenCalledWith({ + key: undefined, + storageKeys: [], + defaultStorageKey: null, + }); + expect(statePersistent.initialLoading).not.toHaveBeenCalled(); + + // Check if Persistent was called with correct parameters + expect(statePersistent._key).toBe(StatePersistent.placeHolderKey); + expect(statePersistent.ready).toBeFalsy(); + expect(statePersistent.isPersisted).toBeFalsy(); + expect(statePersistent.onLoad).toBeUndefined(); + expect(statePersistent.storageKeys).toStrictEqual([]); + expect(statePersistent.config).toStrictEqual({ defaultStorageKey: null }); }); - it("should create StatePersistent and shouldn't call initialLoading if Persistent is ready (config.instantiate = false)", () => { + it("should create StatePersistent and shouldn't call initialLoading if Persistent is ready (config.loadValue = false)", () => { // Overwrite instantiatePersistent once to not call it and set ready property jest .spyOn(StatePersistent.prototype, 'instantiatePersistent') @@ -117,10 +136,25 @@ describe('StatePersistent Tests', () => { }); const statePersistent = new StatePersistent(dummyState, { - instantiate: false, + loadValue: false, }); + expect(statePersistent).toBeInstanceOf(StatePersistent); + expect(statePersistent.state()).toBe(dummyState); + expect(statePersistent.instantiatePersistent).toHaveBeenCalledWith({ + key: undefined, + storageKeys: [], + defaultStorageKey: null, + }); expect(statePersistent.initialLoading).not.toHaveBeenCalled(); + + // Check if Persistent was called with correct parameters + expect(statePersistent._key).toBe(StatePersistent.placeHolderKey); + expect(statePersistent.ready).toBeTruthy(); + expect(statePersistent.isPersisted).toBeFalsy(); + expect(statePersistent.onLoad).toBeUndefined(); + expect(statePersistent.storageKeys).toStrictEqual([]); + expect(statePersistent.config).toStrictEqual({ defaultStorageKey: null }); }); describe('StatePersistent Function Tests', () => { @@ -315,7 +349,9 @@ describe('StatePersistent Tests', () => { () => { statePersistent.setupSideEffects(); - expect(dummyState.addSideEffect).toHaveBeenCalledWith( + expect( + dummyState.addSideEffect + ).toHaveBeenCalledWith( StatePersistent.storeValueSideEffectKey, expect.any(Function), { weight: 0 } diff --git a/packages/core/tests/unit/state/state.runtime.job.test.ts b/packages/core/tests/unit/state/state.runtime.job.test.ts index fc2d2461..63bb50a8 100644 --- a/packages/core/tests/unit/state/state.runtime.job.test.ts +++ b/packages/core/tests/unit/state/state.runtime.job.test.ts @@ -48,6 +48,7 @@ describe('RuntimeJob Tests', () => { storage: true, overwrite: false, maxTriesToUpdate: 3, + any: {}, }); expect(job.rerender).toBeTruthy(); expect(job.performed).toBeFalsy(); @@ -70,6 +71,7 @@ describe('RuntimeJob Tests', () => { }, force: true, maxTriesToUpdate: 5, + any: { jeff: 'frank' }, }); expect(job._key).toBe('dummyJob'); @@ -83,6 +85,7 @@ describe('RuntimeJob Tests', () => { storage: true, overwrite: false, maxTriesToUpdate: 5, + any: { jeff: 'frank' }, }); expect(job.rerender).toBeTruthy(); expect(job.performed).toBeFalsy(); @@ -110,6 +113,7 @@ describe('RuntimeJob Tests', () => { storage: true, overwrite: false, maxTriesToUpdate: 3, + any: {}, }); expect(job.rerender).toBeFalsy(); expect(job.performed).toBeFalsy(); @@ -139,6 +143,7 @@ describe('RuntimeJob Tests', () => { storage: true, overwrite: false, maxTriesToUpdate: 3, + any: {}, }); expect(job.rerender).toBeFalsy(); expect(job.performed).toBeFalsy(); diff --git a/packages/core/tests/unit/storages/persistent.test.ts b/packages/core/tests/unit/storages/persistent.test.ts index 44d09106..5c404777 100644 --- a/packages/core/tests/unit/storages/persistent.test.ts +++ b/packages/core/tests/unit/storages/persistent.test.ts @@ -83,7 +83,7 @@ describe('Persistent Tests', () => { .spyOn(Persistent.prototype, 'instantiatePersistent') .mockReturnValueOnce(undefined); - const persistent = new Persistent(dummyAgile, { instantiate: false }); + const persistent = new Persistent(dummyAgile, { loadValue: false }); expect(persistent).toBeInstanceOf(Persistent); expect(persistent.instantiatePersistent).not.toHaveBeenCalled();