diff --git a/benchmark/benchmarks/react/1000fields/bench/agilets/collection.tsx b/benchmark/benchmarks/react/1000fields/bench/agilets/collection.tsx index 0d9482bb..22155afa 100644 --- a/benchmark/benchmarks/react/1000fields/bench/agilets/collection.tsx +++ b/benchmark/benchmarks/react/1000fields/bench/agilets/collection.tsx @@ -1,10 +1,9 @@ import React from 'react'; import ReactDom from 'react-dom'; -import { createCollection, shared } from '@agile-ts/core'; +import { createCollection, LogCodeManager, shared } from '@agile-ts/core'; import reactIntegration, { useAgile, useValue } from '@agile-ts/react'; -import { assignSharedAgileLoggerConfig } from '@agile-ts/logger'; -assignSharedAgileLoggerConfig({ active: false }); +LogCodeManager.setAllowLogging(false); shared.integrate(reactIntegration); export default function (target: HTMLElement, fieldsCount: number) { diff --git a/benchmark/benchmarks/react/1000fields/bench/agilets/nestedState.tsx b/benchmark/benchmarks/react/1000fields/bench/agilets/nestedState.tsx index 313154df..213f2fe4 100644 --- a/benchmark/benchmarks/react/1000fields/bench/agilets/nestedState.tsx +++ b/benchmark/benchmarks/react/1000fields/bench/agilets/nestedState.tsx @@ -1,10 +1,9 @@ import * as React from 'react'; import * as ReactDom from 'react-dom'; -import { createState, shared, State } from '@agile-ts/core'; +import { createState, LogCodeManager, shared, State } from '@agile-ts/core'; import reactIntegration, { useAgile } from '@agile-ts/react'; -import { assignSharedAgileLoggerConfig } from '@agile-ts/logger'; -assignSharedAgileLoggerConfig({ active: false }); +LogCodeManager.setAllowLogging(false); shared.integrate(reactIntegration); export default function (target: HTMLElement, fieldsCount: number) { diff --git a/benchmark/benchmarks/react/1000fields/bench/agilets/state.tsx b/benchmark/benchmarks/react/1000fields/bench/agilets/state.tsx index 6db059cd..6190049b 100644 --- a/benchmark/benchmarks/react/1000fields/bench/agilets/state.tsx +++ b/benchmark/benchmarks/react/1000fields/bench/agilets/state.tsx @@ -1,10 +1,9 @@ import React from 'react'; import ReactDom from 'react-dom'; -import { createState, shared } from '@agile-ts/core'; +import { createState, LogCodeManager, shared } from '@agile-ts/core'; import reactIntegration, { useAgile } from '@agile-ts/react'; -import { assignSharedAgileLoggerConfig } from '@agile-ts/logger'; -assignSharedAgileLoggerConfig({ active: false }); +LogCodeManager.setAllowLogging(false); shared.integrate(reactIntegration); export default function (target: HTMLElement, fieldsCount: number) { diff --git a/benchmark/benchmarks/react/computed/bench/agilets/autoTracking.tsx b/benchmark/benchmarks/react/computed/bench/agilets/autoTracking.tsx index c87de1e6..d6f93317 100644 --- a/benchmark/benchmarks/react/computed/bench/agilets/autoTracking.tsx +++ b/benchmark/benchmarks/react/computed/bench/agilets/autoTracking.tsx @@ -1,10 +1,14 @@ import React from 'react'; import ReactDom from 'react-dom'; -import { createComputed, createState, shared } from '@agile-ts/core'; +import { + createComputed, + createState, + LogCodeManager, + shared, +} from '@agile-ts/core'; import reactIntegration, { useAgile } from '@agile-ts/react'; -import { assignSharedAgileLoggerConfig } from '@agile-ts/logger'; -assignSharedAgileLoggerConfig({ active: false }); +LogCodeManager.setAllowLogging(false); shared.integrate(reactIntegration); const COUNT = createState(0); diff --git a/benchmark/benchmarks/react/computed/bench/agilets/hardCoded.tsx b/benchmark/benchmarks/react/computed/bench/agilets/hardCoded.tsx index 55e4c693..e206c482 100644 --- a/benchmark/benchmarks/react/computed/bench/agilets/hardCoded.tsx +++ b/benchmark/benchmarks/react/computed/bench/agilets/hardCoded.tsx @@ -1,10 +1,14 @@ import React from 'react'; import ReactDom from 'react-dom'; -import { createComputed, createState, shared } from '@agile-ts/core'; +import { + createComputed, + createState, + LogCodeManager, + shared, +} from '@agile-ts/core'; import reactIntegration, { useAgile } from '@agile-ts/react'; -import { assignSharedAgileLoggerConfig } from '@agile-ts/logger'; -assignSharedAgileLoggerConfig({ active: false }); +LogCodeManager.setAllowLogging(false); shared.integrate(reactIntegration); const COUNT = createState(0); diff --git a/benchmark/benchmarks/react/counter/bench/agilets.tsx b/benchmark/benchmarks/react/counter/bench/agilets.tsx index 1c9de3f7..9445ae0f 100644 --- a/benchmark/benchmarks/react/counter/bench/agilets.tsx +++ b/benchmark/benchmarks/react/counter/bench/agilets.tsx @@ -1,10 +1,9 @@ import React from 'react'; import ReactDom from 'react-dom'; -import { createState, shared } from '@agile-ts/core'; +import { createState, LogCodeManager, shared } from '@agile-ts/core'; import reactIntegration, { useAgile } from '@agile-ts/react'; -import { assignSharedAgileLoggerConfig } from '@agile-ts/logger'; -assignSharedAgileLoggerConfig({ active: false }); +LogCodeManager.setAllowLogging(false); shared.integrate(reactIntegration); const COUNT = createState(0); diff --git a/examples/plainjs/develop/tree-shaking/package.json b/examples/plainjs/develop/tree-shaking/package.json index 53515c71..4af19172 100644 --- a/examples/plainjs/develop/tree-shaking/package.json +++ b/examples/plainjs/develop/tree-shaking/package.json @@ -6,18 +6,16 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "webpack", - "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" + "install:dev:agile": "yalc add @agile-ts/core & yarn install", + "install:prod:agile": "yarn add @agile-ts/core & yarn install" }, "author": "", "license": "ISC", "devDependencies": { "webpack": "^5.47.0", - "webpack-cli": "^4.7.2" + "webpack-cli": "^4.8.0" }, "dependencies": { - "@agile-ts/core": "file:.yalc/@agile-ts/core", - "@agile-ts/react": "file:.yalc/@agile-ts/react", - "react": "^17.0.2" + "@agile-ts/core": "file:.yalc/@agile-ts/core" } } diff --git a/examples/plainjs/develop/tree-shaking/src/index.js b/examples/plainjs/develop/tree-shaking/src/index.js index 9d713ce5..ed9cc3c7 100644 --- a/examples/plainjs/develop/tree-shaking/src/index.js +++ b/examples/plainjs/develop/tree-shaking/src/index.js @@ -1,8 +1,5 @@ -import { createState } from '@agile-ts/core'; -import { useAgile } from '@agile-ts/react'; +import { createLightState } from '@agile-ts/core'; -const MY_STATE = createState('hi'); +const MY_STATE = createLightState('hi'); console.log(MY_STATE.value); - -useAgile(MY_STATE); diff --git a/examples/react-native/develop/AwesomeTSProject/core/index.ts b/examples/react-native/develop/AwesomeTSProject/core/index.ts index 011ceb0e..3d4dc07f 100644 --- a/examples/react-native/develop/AwesomeTSProject/core/index.ts +++ b/examples/react-native/develop/AwesomeTSProject/core/index.ts @@ -1,20 +1,16 @@ -import { Agile } from '@agile-ts/core'; -import { Event } from '@agile-ts/event'; +import { createState, createComputed, createCollection } from '@agile-ts/core'; +import { createEvent, Event } from '@agile-ts/event'; import { Alert } from 'react-native'; -export const App = new Agile({ - logConfig: { active: true }, -}); - -export const MY_STATE = App.createState('MyState', { key: 'my-state' }); //.persist(); -export const MY_STATE_2 = App.createState('MyState2'); //.persist("my-state2"); -export const MY_STATE_3 = App.createState(1); //.persist("my-state2"); +export const MY_STATE = createState('MyState', { key: 'my-state' }); //.persist(); +export const MY_STATE_2 = createState('MyState2'); //.persist("my-state2"); +export const MY_STATE_3 = createState(1); //.persist("my-state2"); MY_STATE.watch('test', (value: any) => { console.log('Watch ' + value); }); -export const MY_COMPUTED = App.createComputed(() => { +export const MY_COMPUTED = createComputed(() => { return 'test' + MY_STATE.value + '_computed_' + MY_STATE_2.value; }); @@ -23,7 +19,7 @@ interface collectionValueInterface { name: string; } -export const MY_COLLECTION = App.createCollection( +export const MY_COLLECTION = createCollection( (collection) => ({ key: 'my-collection', groups: { @@ -43,7 +39,7 @@ MY_COLLECTION.getGroup('myGroup')?.persist({ console.log('Initial: myCollection ', MY_COLLECTION); -export const MY_EVENT = new Event<{ name: string }>(App); +export const MY_EVENT = createEvent<{ name: string }>(); MY_EVENT.on('Test', (payload) => { Alert.alert( diff --git a/examples/react/develop/class-component-ts/src/core/index.ts b/examples/react/develop/class-component-ts/src/core/index.ts index 1e8c9784..81b85b00 100644 --- a/examples/react/develop/class-component-ts/src/core/index.ts +++ b/examples/react/develop/class-component-ts/src/core/index.ts @@ -1,25 +1,25 @@ -import { Agile, clone, Logger } from '@agile-ts/core'; -import { Event } from '@agile-ts/event'; +import { + clone, + createState, + createComputed, + createCollection, +} from '@agile-ts/core'; +import { createEvent, Event } from '@agile-ts/event'; -export const App = new Agile({ - logConfig: { level: Logger.level.DEBUG, timestamp: true }, - waitForMount: false, -}); - -export const MY_STATE = App.createState('MyState'); //.persist(); -export const MY_STATE_2 = App.createState('MyState2', { +export const MY_STATE = createState('MyState'); //.persist(); +export const MY_STATE_2 = createState('MyState2', { key: 'myState2', }).persist(); MY_STATE_2.onLoad(() => { console.log('On Load'); }); -export const MY_STATE_3 = App.createState(1); //.persist("my-state2"); +export const MY_STATE_3 = createState(1); //.persist("my-state2"); MY_STATE.watch('test', (value: any) => { console.log('Watch ' + value); }); -export const MY_COMPUTED = App.createComputed(() => { +export const MY_COMPUTED = createComputed(() => { return 'test' + MY_STATE.value + '_computed_' + MY_STATE_2.value; }, []).setKey('myComputed'); @@ -28,7 +28,7 @@ interface collectionValueInterface { name: string; } -export const MY_COLLECTION = App.createCollection( +export const MY_COLLECTION = createCollection( (collection) => ({ key: 'my-collection', groups: { @@ -48,7 +48,7 @@ MY_COLLECTION.getGroup('myGroup')?.persist({ console.log('Initial: myCollection ', clone(MY_COLLECTION)); -export const MY_EVENT = new Event<{ name: string }>(App, { +export const MY_EVENT = createEvent<{ name: string }>({ delay: 3000, key: 'myEvent', }); diff --git a/examples/react/develop/functional-component-ts/src/App.tsx b/examples/react/develop/functional-component-ts/src/App.tsx index 860f0656..7c0849b3 100644 --- a/examples/react/develop/functional-component-ts/src/App.tsx +++ b/examples/react/develop/functional-component-ts/src/App.tsx @@ -1,7 +1,7 @@ import React, { useEffect } from 'react'; import './App.css'; import { useAgile, useWatcher, useProxy, useSelector } from '@agile-ts/react'; -import { useEvent } from '@agile-ts/event'; +import { useEvent } from '@agile-ts/event/dist/react'; import { COUNTUP, externalCreatedItem, @@ -44,9 +44,12 @@ const App = (props: any) => { ]); const [myGroup] = useAgile([MY_COLLECTION.getGroupWithReference('myGroup')]); - const selectedObjectItem = useSelector(STATE_OBJECT, (value) => { - return value.age; - }); + const stateObjectAge = useSelector( + STATE_OBJECT, + (value) => { + return value.age; + } + ); const [stateObject, item2, collection2] = useProxy( [STATE_OBJECT, MY_COLLECTION.getItem('id2'), MY_COLLECTION], @@ -56,8 +59,6 @@ const App = (props: any) => { console.log('Item1: ', item2?.name); console.log('Collection: ', collection2.slice(0, 2)); - // const myCollection2 = useAgile(MY_COLLECTION); - const mySelector = useAgile(MY_COLLECTION.getSelector('mySelector')); useEvent(MY_EVENT, () => { @@ -126,6 +127,13 @@ const App = (props: any) => { }}> Change shallow name +

Age: {stateObjectAge}

+
diff --git a/examples/react/develop/functional-component-ts/src/core/index.ts b/examples/react/develop/functional-component-ts/src/core/index.ts index f2f4efe8..5d74ad72 100644 --- a/examples/react/develop/functional-component-ts/src/core/index.ts +++ b/examples/react/develop/functional-component-ts/src/core/index.ts @@ -4,22 +4,25 @@ import Agile, { createComputed, createState, createStorage, + createStorageManager, Item, + assignSharedAgileStorageManager, } from '@agile-ts/core'; -import Event from '@agile-ts/event'; +import { createEvent } from '@agile-ts/event'; import { assignSharedAgileLoggerConfig, Logger } from '@agile-ts/logger'; import { clone } from '@agile-ts/utils'; export const myStorage: any = {}; assignSharedAgileLoggerConfig({ level: Logger.level.DEBUG }); -export const App = new Agile({ - localStorage: true, -}); +export const App = new Agile(); assignSharedAgileInstance(App); +export const storageManager = createStorageManager({ localStorage: true }); +assignSharedAgileStorageManager(storageManager); + // Register custom second Storage -App.registerStorage( +storageManager.register( createStorage({ key: 'myStorage', methods: { @@ -111,7 +114,7 @@ export const externalCreatedItem = new Item(MY_COLLECTION, { console.log('Initial: myCollection ', clone(MY_COLLECTION)); -export const MY_EVENT = new Event<{ name: string }>(App, { +export const MY_EVENT = createEvent<{ name: string }>({ delay: 3000, key: 'myEvent', }); diff --git a/examples/react/develop/functional-component-ts/yarn.lock b/examples/react/develop/functional-component-ts/yarn.lock index b44f5283..d760f47c 100644 --- a/examples/react/develop/functional-component-ts/yarn.lock +++ b/examples/react/develop/functional-component-ts/yarn.lock @@ -3,36 +3,36 @@ "@agile-ts/api@file:.yalc/@agile-ts/api": - version "0.0.19" + version "0.0.21" dependencies: - "@agile-ts/utils" "^0.0.5" + "@agile-ts/utils" "^0.0.7" "@agile-ts/core@file:.yalc/@agile-ts/core": - version "0.1.0" + version "0.2.0-alpha.4" dependencies: - "@agile-ts/utils" "^0.0.5" + "@agile-ts/utils" "^0.0.7" "@agile-ts/event@file:.yalc/@agile-ts/event": - version "0.0.8" + version "0.0.10" "@agile-ts/logger@file:.yalc/@agile-ts/logger": - version "0.0.5" + version "0.0.7" dependencies: - "@agile-ts/utils" "^0.0.5" + "@agile-ts/utils" "^0.0.7" "@agile-ts/multieditor@file:.yalc/@agile-ts/multieditor": - version "0.0.18" + version "0.0.20" "@agile-ts/proxytree@file:.yalc/@agile-ts/proxytree": - version "0.0.4" + version "0.0.5" "@agile-ts/react@file:.yalc/@agile-ts/react": - version "0.1.0" + version "0.2.0-alpha.1" -"@agile-ts/utils@^0.0.5": - version "0.0.5" - resolved "https://registry.yarnpkg.com/@agile-ts/utils/-/utils-0.0.5.tgz#23cc83e60eb6b15734247fac1d77f1fd629ffdb6" - integrity sha512-R86X9MjMty14eoQ4djulZSdHf9mIF9dPcj4g+SABqdA6AqbewS0/BQGNGR5p6gXhqc4+mT8rzkutywdPnMUNfA== +"@agile-ts/utils@^0.0.7": + version "0.0.7" + resolved "https://registry.yarnpkg.com/@agile-ts/utils/-/utils-0.0.7.tgz#3dd1add6b9f63d0a5bf35e71f54ac46448ae047f" + integrity sha512-OviTDC+ZbfyiUx8Gy8veS6YymC/tT6UeP23nT8V0EQV4F2MmuWqZ2yiKk+AYxZx8h74Ey8BVEUX6/ntpxhSNPw== "@babel/code-frame@7.8.3": version "7.8.3" diff --git a/examples/react/develop/multieditor-ts/src/core/agile.ts b/examples/react/develop/multieditor-ts/src/core/agile.ts index e212a41d..d0e37e07 100644 --- a/examples/react/develop/multieditor-ts/src/core/agile.ts +++ b/examples/react/develop/multieditor-ts/src/core/agile.ts @@ -1,10 +1,6 @@ import { Agile, globalBind } from '@agile-ts/core'; -const App = new Agile({ - logConfig: { - active: true, - }, -}); +const App = new Agile(); export default App; diff --git a/examples/react/develop/simple-counter/package.json b/examples/react/develop/simple-counter/package.json index 0b329bb3..058eac66 100644 --- a/examples/react/develop/simple-counter/package.json +++ b/examples/react/develop/simple-counter/package.json @@ -4,8 +4,8 @@ "private": true, "dependencies": { "@agile-ts/core": "file:.yalc/@agile-ts/core", - "@agile-ts/react": "file:.yalc/@agile-ts/react", "@agile-ts/logger": "file:.yalc/@agile-ts/logger", + "@agile-ts/react": "file:.yalc/@agile-ts/react", "@reduxjs/toolkit": "^1.6.1", "jotai": "^1.2.2", "nanostores": "^0.4.1", @@ -16,7 +16,8 @@ "recoil": "^0.4.0" }, "devDependencies": { - "source-map-explorer": "^2.5.2" + "source-map-explorer": "^2.5.2", + "webpack-bundle-analyzer": "^4.4.2" }, "scripts": { "start": "react-scripts start", @@ -24,6 +25,7 @@ "test": "react-scripts test", "eject": "react-scripts eject", "analyze": "yarn run build && source-map-explorer 'build/static/js/*.js'", + "analyze:webpack": "node scripts/analyze.js", "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" }, diff --git a/examples/react/develop/simple-counter/scripts/analyze.js b/examples/react/develop/simple-counter/scripts/analyze.js new file mode 100644 index 00000000..ea2c0df2 --- /dev/null +++ b/examples/react/develop/simple-counter/scripts/analyze.js @@ -0,0 +1,38 @@ +// https://medium.com/@hamidihamza/optimize-react-web-apps-with-webpack-bundle-analyzer-6ecb9f162c76 +// Note: Webpack Bundle Analyzer doesn't show accurately which bundles were tree shaken +// (See: https://github.com/webpack-contrib/webpack-bundle-analyzer/issues/161) + +import dotenv from 'dotenv'; + +// Loads environment variables from the '.env' file +dotenv.config(); + +// https://nodejs.org/docs/latest/api/process.html#process_process_argv +const isDev = process.argv.includes('--dev') || process.env.DEV === 'true'; + +console.log( + `Start bundling a '${isDev ? 'development' : 'production'}' build!` +); + +process.env.NODE_ENV = isDev ? 'development' : 'production'; + +const webpack = require('webpack'); +const BundleAnalyzerPlugin = require('webpack-bundle-analyzer') + .BundleAnalyzerPlugin; +const webpackConfigProd = require('react-scripts/config/webpack.config')( + 'production' +); +const webpackConfigDev = require('react-scripts/config/webpack.config')( + 'development' +); + +// Add Bundle Analyzer Plugin to React webpack config +webpackConfigProd.plugins.push(new BundleAnalyzerPlugin()); +webpackConfigDev.plugins.push(new BundleAnalyzerPlugin()); + +// Build project with webpack +webpack(isDev ? webpackConfigDev : webpackConfigProd, (err, stats) => { + if (err || stats.hasErrors()) { + console.error(err); + } +}); diff --git a/examples/react/develop/simple-counter/src/state-manager/Agile.js b/examples/react/develop/simple-counter/src/state-manager/Agile.js index e86b199e..8ec4ad49 100644 --- a/examples/react/develop/simple-counter/src/state-manager/Agile.js +++ b/examples/react/develop/simple-counter/src/state-manager/Agile.js @@ -1,10 +1,12 @@ import React from 'react'; -import { createState } from '@agile-ts/core'; -import { useAgile, useValue } from '@agile-ts/react'; +import { createLightState } from '@agile-ts/core'; +import { useAgile } from '@agile-ts/react'; -const COUNTER_A = createState(1); -const COUNTER_B = createState(2); -const COUNTER_C = createState(3); +// registerStorageManager(createStorageManager({ localStorage: true })); +// const COUNTER_A = createState(1).persist('persistKey'); +const COUNTER_A = createLightState(1); +const COUNTER_B = createLightState(2); +const COUNTER_C = createLightState(3); const CounterA = () => { const count = useAgile(COUNTER_A); @@ -16,7 +18,7 @@ const CounterA = () => { }; const CounterB = () => { - const count = useValue(COUNTER_B); + const count = useAgile(COUNTER_B); return (
B: {count} diff --git a/examples/react/develop/simple-counter/yarn.lock b/examples/react/develop/simple-counter/yarn.lock index ebaf211d..23f11909 100644 --- a/examples/react/develop/simple-counter/yarn.lock +++ b/examples/react/develop/simple-counter/yarn.lock @@ -3,7 +3,7 @@ "@agile-ts/core@file:.yalc/@agile-ts/core": - version "0.2.0-alpha.3" + version "0.2.0-alpha.4" dependencies: "@agile-ts/utils" "^0.0.7" @@ -2011,6 +2011,11 @@ schema-utils "^2.6.5" source-map "^0.7.3" +"@polka/url@^1.0.0-next.17": + version "1.0.0-next.17" + resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.17.tgz#25fdbdfd282c2f86ddf3fcefbd98be99cd2627e2" + integrity sha512-0p1rCgM3LLbAdwBnc7gqgnvjHg9KpbhcSphergHShlkWz8EdPawoMJ3/VbezI0mGC5eKCDzMaPgF9Yca6cKvrg== + "@reduxjs/toolkit@^1.6.1": version "1.6.1" resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.6.1.tgz#7bc83b47352a663bf28db01e79d17ba54b98ade9" @@ -2701,6 +2706,11 @@ acorn-walk@^7.1.1: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== +acorn-walk@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.1.1.tgz#3ddab7f84e4a7e2313f6c414c5b7dac85f4e3ebc" + integrity sha512-FbJdceMlPHEAWJOILDk1fXD8lnTlEIWFkqtfk+MvmL5q/qlHfN7GEHcsFZWt/Tea9jRNPWUZG4G976nqAAmU9w== + acorn@^6.4.1: version "6.4.2" resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" @@ -2711,6 +2721,11 @@ acorn@^7.1.0, acorn@^7.1.1, acorn@^7.4.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== +acorn@^8.0.4: + version "8.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.4.1.tgz#56c36251fc7cabc7096adc18f05afe814321a28c" + integrity sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA== + address@1.1.2, address@^1.0.1: version "1.1.2" resolved "https://registry.yarnpkg.com/address/-/address-1.1.2.tgz#bf1116c9c758c51b7a933d296b72c221ed9428b6" @@ -3956,6 +3971,11 @@ commander@^4.1.1: resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== +commander@^6.2.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" + integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== + common-tags@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" @@ -7788,7 +7808,7 @@ mime@1.6.0: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== -mime@^2.4.4: +mime@^2.3.1, mime@^2.4.4: version "2.5.2" resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe" integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg== @@ -8297,6 +8317,11 @@ open@^7.0.2, open@^7.3.1: is-docker "^2.0.0" is-wsl "^2.1.1" +opener@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" + integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== + opn@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc" @@ -10501,6 +10526,15 @@ simple-swizzle@^0.2.2: dependencies: is-arrayish "^0.3.1" +sirv@^1.0.7: + version "1.0.14" + resolved "https://registry.yarnpkg.com/sirv/-/sirv-1.0.14.tgz#b826343f573e12653c5b3c3080a3a2a6a06595cd" + integrity sha512-czTFDFjK9lXj0u9mJ3OmJoXFztoilYS+NdRPcJoT182w44wSEkHSiO7A2517GLJ8wKM4GjCm2OXE66Dhngbzjg== + dependencies: + "@polka/url" "^1.0.0-next.17" + mime "^2.3.1" + totalist "^1.0.0" + sisteransi@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" @@ -11220,6 +11254,11 @@ toidentifier@1.0.0: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== +totalist@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df" + integrity sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g== + tough-cookie@^2.3.3, tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" @@ -11665,6 +11704,21 @@ webidl-conversions@^6.1.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514" integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== +webpack-bundle-analyzer@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.4.2.tgz#39898cf6200178240910d629705f0f3493f7d666" + integrity sha512-PIagMYhlEzFfhMYOzs5gFT55DkUdkyrJi/SxJp8EF3YMWhS+T9vvs2EoTetpk5qb6VsCq02eXTlRDOydRhDFAQ== + dependencies: + acorn "^8.0.4" + acorn-walk "^8.0.0" + chalk "^4.1.0" + commander "^6.2.0" + gzip-size "^6.0.0" + lodash "^4.17.20" + opener "^1.5.2" + sirv "^1.0.7" + ws "^7.3.1" + webpack-dev-middleware@^3.7.2: version "3.7.3" resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.3.tgz#0639372b143262e2b84ab95d3b91a7597061c2c5" @@ -12062,6 +12116,11 @@ ws@^7.2.3: resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.3.tgz#1f9643de34a543b8edb124bdcbc457ae55a6e5cd" integrity sha512-hr6vCR76GsossIRsr8OLR9acVVm1jyfEWvhbNjtgPOrfvAlKzvyeg/P6r8RuDjRyrcQoPQT7K0DGEPc7Ae6jzA== +ws@^7.3.1: + version "7.5.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.3.tgz#160835b63c7d97bfab418fc1b8a9fced2ac01a74" + integrity sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg== + xml-name-validator@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" diff --git a/examples/react/develop/tree-shaking/package.json b/examples/react/develop/tree-shaking/package.json new file mode 100644 index 00000000..05202eaf --- /dev/null +++ b/examples/react/develop/tree-shaking/package.json @@ -0,0 +1,25 @@ +{ + "name": "tree-shaking", + "version": "1.0.0", + "main": "src/index.js", + "license": "MIT", + "scripts": { + "build": "npx webpack --config webpack.config.js", + "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" + }, + "devDependencies": { + "babel-core": "^6.26.3", + "babel-loader": "^8.2.2", + "babel-preset-env": "^1.7.0", + "babel-preset-react": "^6.24.1", + "webpack": "^5.51.1", + "webpack-cli": "^4.8.0" + }, + "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" + } +} diff --git a/examples/react/develop/tree-shaking/src/App.jsx b/examples/react/develop/tree-shaking/src/App.jsx new file mode 100644 index 00000000..ac5157ce --- /dev/null +++ b/examples/react/develop/tree-shaking/src/App.jsx @@ -0,0 +1,9 @@ +import React from 'react'; + +export const FooComponent = ({ name }) => ( +
Hello from FooComponent, {name ?? 'unknown'}!
+); + +export const BarComponent = ({ name }) => ( +
Hello from BarComponent, {name ?? 'unknown'}!
+); diff --git a/examples/react/develop/tree-shaking/src/core.js b/examples/react/develop/tree-shaking/src/core.js new file mode 100644 index 00000000..6dcfb7c0 --- /dev/null +++ b/examples/react/develop/tree-shaking/src/core.js @@ -0,0 +1,3 @@ +import { createLightState } from '@agile-ts/core'; + +export const MY_STATE = createLightState('jeff'); diff --git a/examples/react/develop/tree-shaking/src/index.js b/examples/react/develop/tree-shaking/src/index.js new file mode 100644 index 00000000..7fb253f5 --- /dev/null +++ b/examples/react/develop/tree-shaking/src/index.js @@ -0,0 +1,9 @@ +import { BarComponent } from './App'; +import { MY_STATE } from './core'; + +MY_STATE.set('jeff'); + +// we could do something with BarComponent here, +// like ReactDOM.render, but let's just dump it to +// console for simplicity +console.log(BarComponent); diff --git a/examples/react/develop/tree-shaking/webpack.config.js b/examples/react/develop/tree-shaking/webpack.config.js new file mode 100644 index 00000000..1b3fb79c --- /dev/null +++ b/examples/react/develop/tree-shaking/webpack.config.js @@ -0,0 +1,31 @@ +const path = require('path'); + +module.exports = { + mode: 'development', + entry: './src/index.js', + output: { + path: path.resolve(__dirname, 'dist'), + filename: 'app.js', + }, + resolve: { extensions: ['.js', '.jsx'] }, + module: { + rules: [ + { + test: /\.jsx?$/, + exclude: /node_modules/, + use: { + loader: 'babel-loader', + options: { + presets: [['@babel/env', { modules: false }], '@babel/react'], + }, + }, + }, + ], + }, + optimization: { + usedExports: true, + innerGraph: true, + sideEffects: true, + }, + devtool: false, +}; diff --git a/examples/react/release/boxes/package.json b/examples/react/release/boxes/package.json index 479c96d1..078a67a8 100644 --- a/examples/react/release/boxes/package.json +++ b/examples/react/release/boxes/package.json @@ -3,10 +3,10 @@ "version": "0.1.0", "private": true, "dependencies": { - "@agile-ts/core": "^0.1.3", - "@agile-ts/logger": "^0.0.7", - "@agile-ts/proxytree": "^0.0.5", - "@agile-ts/react": "^0.1.2", + "@agile-ts/core": "file:.yalc/@agile-ts/core", + "@agile-ts/logger": "file:.yalc/@agile-ts/logger", + "@agile-ts/proxytree": "file:.yalc/@agile-ts/proxytree", + "@agile-ts/react": "file:.yalc/@agile-ts/react", "lodash": "^4.17.21", "react": "^17.0.2", "react-dom": "^17.0.2", diff --git a/examples/react/release/boxes/src/core/entities/ui/ui.actions.ts b/examples/react/release/boxes/src/core/entities/ui/ui.actions.ts index c3d537cd..2fe86360 100644 --- a/examples/react/release/boxes/src/core/entities/ui/ui.actions.ts +++ b/examples/react/release/boxes/src/core/entities/ui/ui.actions.ts @@ -8,7 +8,6 @@ import { SCREEN, } from './ui.controller'; import core from '../../index'; -import { copy } from '@agile-ts/utils'; export const addDefaultElement = (image: boolean = false) => { if (image) addElement(defaultElementStyle, getRandomImage()); diff --git a/examples/react/release/boxes/src/core/entities/ui/ui.controller.ts b/examples/react/release/boxes/src/core/entities/ui/ui.controller.ts index c92dcf93..071de3b3 100644 --- a/examples/react/release/boxes/src/core/entities/ui/ui.controller.ts +++ b/examples/react/release/boxes/src/core/entities/ui/ui.controller.ts @@ -1,22 +1,22 @@ -import { App } from '../../app'; import { CanvasInterface, ElementInterface, ScreenInterface, } from './ui.interfaces'; +import { createCollection, createState } from '@agile-ts/core'; export const defaultElementStyle = { position: { top: 0, left: 0 }, size: { width: 200, height: 200 }, }; -export const CANVAS = App.createState({ +export const CANVAS = createState({ width: 5000, height: 5000, }); -export const SCREEN = App.createState({ width: 0, height: 0 }); +export const SCREEN = createState({ width: 0, height: 0 }); -export const ELEMENTS = App.createCollection(); +export const ELEMENTS = createCollection(); export const SELECTED_ELEMENT = ELEMENTS.createSelector( 'selectedElement', diff --git a/examples/react/release/boxes/yarn.lock b/examples/react/release/boxes/yarn.lock index 18e63613..07d40055 100644 --- a/examples/react/release/boxes/yarn.lock +++ b/examples/react/release/boxes/yarn.lock @@ -2,29 +2,21 @@ # yarn lockfile v1 -"@agile-ts/core@^0.1.2": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@agile-ts/core/-/core-0.1.2.tgz#5a3974ba0c57a51a19bcdf81b2055e091c884f5e" - integrity sha512-9031MGUrPpg/ZL1ErpwUlHX751HKEtOfbc5Ae7W7x/POGH89Gka09hMAhqQlDrKF2+olVs3sf6PAsAHRv6paGw== +"@agile-ts/core@file:.yalc/@agile-ts/core": + version "0.2.0-alpha.4" dependencies: "@agile-ts/utils" "^0.0.7" -"@agile-ts/logger@^0.0.7": +"@agile-ts/logger@file:.yalc/@agile-ts/logger": version "0.0.7" - resolved "https://registry.yarnpkg.com/@agile-ts/logger/-/logger-0.0.7.tgz#9e89e8d80f80a46901a508432696860f88d5e878" - integrity sha512-6N9qyooo/a7ibyl9L7HnBX0LyMlSwaEYgObYs58KzR19JGF00PX/sUFfQAVplXXsMfT/8HvLyI+4TssmyI6DdQ== dependencies: "@agile-ts/utils" "^0.0.7" -"@agile-ts/proxytree@^0.0.5": +"@agile-ts/proxytree@file:.yalc/@agile-ts/proxytree": version "0.0.5" - resolved "https://registry.yarnpkg.com/@agile-ts/proxytree/-/proxytree-0.0.5.tgz#81c40970707271822a176ee59f93b9230df6311d" - integrity sha512-KODknVD30ld9xPCyt0UCf0yGcroy/0CHEncAdmTFwEvDSMipMaqFQRsAYZ0tgB4bMfFzab40aUmYTK8XDkwdHw== -"@agile-ts/react@^0.1.2": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@agile-ts/react/-/react-0.1.2.tgz#d07f6b935d9322cd60d2e9e3871da554b04460af" - integrity sha512-W4u2+X6KCeXPdkjit/NsMJG5nBsa7dNFaEzyfTsp5Cqbs99zLqY6dO8LUIYyhRt/+HBvEW9o64i/6Kqd59WM1Q== +"@agile-ts/react@file:.yalc/@agile-ts/react": + version "0.2.0-alpha.1" "@agile-ts/utils@^0.0.7": version "0.0.7" diff --git a/examples/react/release/stopwatch-query-url/src/core/index.ts b/examples/react/release/stopwatch-query-url/src/core/index.ts index b9e3cf5e..8ec09cb5 100644 --- a/examples/react/release/stopwatch-query-url/src/core/index.ts +++ b/examples/react/release/stopwatch-query-url/src/core/index.ts @@ -1,4 +1,9 @@ -import { createState, globalBind, shared } from '@agile-ts/core'; +import { + createState, + globalBind, + createStorage, + getStorageManager, +} from '@agile-ts/core'; import queryString from 'query-string'; export type StopwatchStateType = @@ -7,7 +12,7 @@ export type StopwatchStateType = | 'initial'; // Stopwatch is reset // Create Query Storage to store the State in the query (url) -const queryUrlStorage = shared.createStorage({ +const queryUrlStorage = createStorage({ key: 'query-url', methods: { set: (key, value) => { @@ -30,7 +35,7 @@ const queryUrlStorage = shared.createStorage({ }); // Register Query Storage to the shared Agile Instance and set it as default -shared.registerStorage(queryUrlStorage, { default: true }); +getStorageManager().register(queryUrlStorage, { default: true }); // State to keep track of the current time of the Stopwatch const TIME = createState( diff --git a/examples/vue/develop/my-project/src/core.js b/examples/vue/develop/my-project/src/core.js index 0512ec9b..8606056e 100644 --- a/examples/vue/develop/my-project/src/core.js +++ b/examples/vue/develop/my-project/src/core.js @@ -1,24 +1,24 @@ -import { Agile, assignSharedAgileInstance, globalBind } from '@agile-ts/core'; +import { + globalBind, + createState, + createComputed, + createCollection, +} from '@agile-ts/core'; import { Logger, assignSharedAgileLoggerConfig } from '@agile-ts/logger'; import '@agile-ts/vue'; assignSharedAgileLoggerConfig({ level: Logger.level.DEBUG }); -// Create Agile Instance -export const App = new Agile({ localStorage: true }); -assignSharedAgileInstance(App); - // console.debug('hi'); // Doesn't work here idk why // Create State -export const MY_STATE = App.createState('World', { +export const MY_STATE = createState('World', { key: 'my-state', -}) - .computeValue((v) => { - return `Hello ${v}`; - }); +}).computeValue((v) => { + return `Hello ${v}`; +}); -export const MY_COMPUTED = App.createComputed( +export const MY_COMPUTED = createComputed( async () => { await new Promise((resolve) => setTimeout(resolve, 3000)); return `${MY_STATE.value} Frank`; @@ -27,7 +27,7 @@ export const MY_COMPUTED = App.createComputed( ); // Create Collection -export const TODOS = App.createCollection({ +export const TODOS = createCollection({ initialData: [{ id: 1, name: 'Clean Bathroom' }], selectors: [1], }).persist('todos'); diff --git a/packages/core/package.json b/packages/core/package.json index a0b5c23e..5e8cb90b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@agile-ts/core", - "version": "0.2.0-alpha.3", + "version": "0.2.0-alpha.4", "author": "BennoDev", "license": "MIT", "homepage": "https://agile-ts.org/", diff --git a/packages/core/src/agile.ts b/packages/core/src/agile.ts index 23a02432..82fe3bab 100644 --- a/packages/core/src/agile.ts +++ b/packages/core/src/agile.ts @@ -1,12 +1,9 @@ import { Runtime, Integration, - Storage, Integrations, SubController, globalBind, - Storages, - RegisterConfigInterface, LogCodeManager, IntegrationsConfigInterface, defineConfig, @@ -22,8 +19,6 @@ export class Agile { public runtime: Runtime; // Manages and simplifies the subscription to UI-Components public subController: SubController; - // Handles the permanent persistence of Agile Classes - public storages: Storages; // Integrations (UI-Frameworks) that are integrated into the Agile Instance public integrations: Integrations; @@ -47,7 +42,6 @@ export class Agile { * changes in the Runtime to prevent race conditions * - update/rerender subscribed UI-Components through the provided Integrations * such as the [React Integration](https://agile-ts.org/docs/react) - * - integrate with the persistent [Storage](https://agile-ts.org/docs/core/storage) * - provide configuration object * * Each Agile Sub Instance requires an Agile Instance to be instantiated and function properly. @@ -75,9 +69,6 @@ export class Agile { }); this.runtime = new Runtime(this); this.subController = new SubController(this); - this.storages = new Storages(this, { - localStorage: config.localStorage, - }); LogCodeManager.log('10:00:00', [], this); @@ -107,27 +98,6 @@ export class Agile { return this; } - /** - * Registers the specified Storage with AgileTs. - * - * After a successful registration, - * [Agile Sub Instances](https://agile-ts.org/docs/introduction/#agile-sub-instance) such as States - * can be persisted in the external Storage. - * - * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#registerstorage) - * - * @public - * @param storage - Storage to be registered. - * @param config - Configuration object - */ - public registerStorage( - storage: Storage, - config: RegisterConfigInterface = {} - ): this { - this.storages.register(storage, config); - return this; - } - /** * Returns a boolean indicating whether any Integration * has been registered with AgileTs or not. @@ -139,18 +109,6 @@ export class Agile { public hasIntegration(): boolean { return this.integrations.hasIntegration(); } - - /** - * Returns a boolean indicating whether any Storage - * has been registered with AgileTs or not. - * - * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#hasstorage) - * - * @public - */ - public hasStorage(): boolean { - return this.storages.hasStorage(); - } } export type AgileKey = string | number; @@ -163,11 +121,6 @@ export interface CreateAgileConfigInterface * @default true */ waitForMount?: boolean; - /** - * Whether the Local Storage should be registered as a Agile Storage by default. - * @default false - */ - localStorage?: boolean; /** * Whether the Agile Instance should be globally bound (globalThis) * and thus be globally available. diff --git a/packages/core/src/collection/collection.persistent.ts b/packages/core/src/collection/collection.persistent.ts index 2f2b7136..2b2232f1 100644 --- a/packages/core/src/collection/collection.persistent.ts +++ b/packages/core/src/collection/collection.persistent.ts @@ -11,6 +11,7 @@ import { Persistent, PersistentKey, StorageKey, + getStorageManager, } from '../internal'; export class CollectionPersistent< @@ -84,7 +85,7 @@ export class CollectionPersistent< // Check if Collection is already persisted // (indicated by the persistence of 'true' at '_storageItemKey') - const isPersisted = await this.agileInstance().storages.get( + const isPersisted = await getStorageManager()?.get( _storageItemKey, this.config.defaultStorageKey as any ); @@ -207,7 +208,7 @@ export class CollectionPersistent< ); // Set flag in Storage to indicate that the Collection is persisted - this.agileInstance().storages.set(_storageItemKey, true, this.storageKeys); + getStorageManager()?.set(_storageItemKey, true, this.storageKeys); // Persist default Group defaultGroup.persist(defaultGroupStorageKey, { @@ -282,7 +283,7 @@ export class CollectionPersistent< ); // Remove Collection is persisted indicator flag from Storage - this.agileInstance().storages.remove(_storageItemKey, this.storageKeys); + getStorageManager()?.remove(_storageItemKey, this.storageKeys); // Remove default Group from the Storage defaultGroup.persistent?.removePersistedValue(defaultGroupStorageKey); diff --git a/packages/core/src/collection/group/index.ts b/packages/core/src/collection/group/index.ts index 44aef2c5..dc339159 100644 --- a/packages/core/src/collection/group/index.ts +++ b/packages/core/src/collection/group/index.ts @@ -1,5 +1,5 @@ import { - State, + EnhancedState, Collection, DefaultItem, ItemKey, @@ -23,7 +23,7 @@ import { export class Group< DataType extends Object = DefaultItem, ValueType = Array // To extract the Group Type Value in Integration methods like 'useAgile()' -> extends State> { +> extends EnhancedState> { // Collection the Group belongs to collection: () => Collection; diff --git a/packages/core/src/collection/item.ts b/packages/core/src/collection/item.ts index 1be4cebf..db98104f 100644 --- a/packages/core/src/collection/item.ts +++ b/packages/core/src/collection/item.ts @@ -1,5 +1,5 @@ import { - State, + EnhancedState, Collection, StateKey, StateRuntimeJobConfigInterface, @@ -12,7 +12,7 @@ import { defineConfig, } from '../internal'; -export class Item extends State< +export class Item extends EnhancedState< DataType > { // Collection the Group belongs to diff --git a/packages/core/src/collection/selector.ts b/packages/core/src/collection/selector.ts index 937c5458..76795f35 100644 --- a/packages/core/src/collection/selector.ts +++ b/packages/core/src/collection/selector.ts @@ -4,13 +4,13 @@ import { defineConfig, Item, ItemKey, - State, + EnhancedState, StateRuntimeJobConfigInterface, } from '../internal'; export class Selector< DataType extends Object = DefaultItem -> extends State { +> extends EnhancedState { // Collection the Selector belongs to public collection: () => Collection; diff --git a/packages/core/src/computed/index.ts b/packages/core/src/computed/index.ts index 22217093..185dec62 100644 --- a/packages/core/src/computed/index.ts +++ b/packages/core/src/computed/index.ts @@ -12,10 +12,6 @@ import { export * from './computed'; // export * from './computed.tracker'; -export interface CreateComputedConfigInterfaceWithAgile - extends CreateAgileSubInstanceInterface, - CreateComputedConfigInterface {} - /** * Returns a newly created Computed. * @@ -28,7 +24,7 @@ export interface CreateComputedConfigInterfaceWithAgile * Direct dependencies can be States and Collections. * So when, for example, a dependent State value changes, the computed value is recomputed. * - * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#createstate) + * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#createcomputed) * * @public * @param computeFunction - Function to compute the computed value. @@ -84,3 +80,7 @@ export function createComputed( removeProperties(_config, ['agileInstance']) ); } + +export interface CreateComputedConfigInterfaceWithAgile + extends CreateAgileSubInstanceInterface, + CreateComputedConfigInterface {} diff --git a/packages/core/src/internal.ts b/packages/core/src/internal.ts index a1c8852c..f504535e 100644 --- a/packages/core/src/internal.ts +++ b/packages/core/src/internal.ts @@ -36,6 +36,7 @@ export * from './storages/persistent'; // State export * from './state'; export * from './state/state.observer'; +export * from './state/state.enhanced'; export * from './state/state.persistent'; export * from './state/state.runtime.job'; diff --git a/packages/core/src/logCodeManager.ts b/packages/core/src/logCodeManager.ts index 819563fa..70aa95e3 100644 --- a/packages/core/src/logCodeManager.ts +++ b/packages/core/src/logCodeManager.ts @@ -1,3 +1,5 @@ +import { copy } from '@agile-ts/utils'; + // The Log Code Manager keeps track // and manages all important Logs of AgileTs. // @@ -9,7 +11,7 @@ // 00 = General // 10 = Agile // 11 = Storage -// .. +// ... // // --- // 00:|00|:00 second digits are based on the Log Type @@ -23,6 +25,7 @@ const logCodeTypes = { // --- // 00:00:|00| third digits are based on the Log Message (ascending counted) +let allowLogging = true; const niceLogCodeMessages = { // Agile '10:00:00': 'Created new AgileInstance.', @@ -40,6 +43,8 @@ const niceLogCodeMessages = { "Couldn't find Storage '${0}'. " + "The Storage with the key/name '${0}' doesn't exists!", '11:03:02': "Storage with the key/name '${0}' isn't ready yet!", + '11:02:06': + 'By registering a new Storage Manager the old one will be overwritten!', '11:03:03': 'No Storage found to get a value from! Please specify at least one Storage.', '11:03:04': @@ -170,6 +175,16 @@ const logCodeMessages: typeof niceLogCodeMessages = ? niceLogCodeMessages : ({} as any); +/** + * Specifies whether the LogCodeManager is allowed to print any logs. + * + * @internal + * @param logging - Whether the LogCodeManager is allowed to print any logs. + */ +function setAllowLogging(logging: boolean) { + allowLogging = logging; +} + /** * Returns the log message according to the specified log code. * @@ -209,7 +224,7 @@ function log>( ...data: any[] ): void { const logger = LogCodeManager.getLogger(); - if (logger != null && !logger.isActive) return; + if ((logger != null && !logger.isActive) || !allowLogging) return; const logType = logCodeTypes[logCode.substr(3, 2)]; if (typeof logType !== 'string') return; @@ -242,7 +257,7 @@ function logIfTags>( ...data: any[] ): void { const logger = LogCodeManager.getLogger(); - if (logger != null && !logger.isActive) return; + if ((logger != null && !logger.isActive) || !allowLogging) return; const logType = logCodeTypes[logCode.substr(3, 2)]; if (typeof logType !== 'string') return; @@ -255,38 +270,46 @@ function logIfTags>( // Handle logging with Logger logger.if.tag(tags)[logType](getLog(logCode, replacers), ...data); } + /** - * The Log Code Manager keeps track - * and manages all important Logs of AgileTs. + * Creates an extension of the specified LogCodeManager + * and assigns the provided additional log messages to it. * - * @internal + * @param additionalLogs - Log messages to be added to the LogCodeManager. + * @param logCodeManager - LogCodeManager to create an extension from. */ -let tempLogCodeManager: { - getLog: typeof getLog; - log: typeof log; - logCodeLogTypes: typeof logCodeTypes; - logCodeMessages: typeof logCodeMessages; - getLogger: () => any; - logIfTags: typeof logIfTags; -}; +export function assignAdditionalLogs< + NewLogCodeMessages, + OldLogCodeMessages = typeof logCodeMessages +>( + additionalLogs: { [key: string]: string }, + logCodeManager: LogCodeManagerInterface +): LogCodeManagerInterface { + const copiedLogCodeManager = copy(logCodeManager); + copiedLogCodeManager.logCodeMessages = { + ...copiedLogCodeManager.logCodeMessages, + ...additionalLogs, + } as any; + return copiedLogCodeManager as any; +} + +let tempLogCodeManager: LogCodeManagerInterface; if (typeof process === 'object' && process.env.NODE_ENV !== 'production') { + let loggerPackage: any = null; + try { + loggerPackage = require('@agile-ts/logger'); + } catch (e) { + // empty catch block + } + tempLogCodeManager = { getLog, log, logCodeLogTypes: logCodeTypes, logCodeMessages: logCodeMessages, - // Not doing 'logger: loggerPackage?.sharedAgileLogger' - // because only by calling a function (now 'getLogger()') the 'sharedLogger' is refetched - getLogger: () => { - let loggerPackage: any = null; - try { - loggerPackage = require('@agile-ts/logger'); - } catch (e) { - // empty catch block - } - return loggerPackage?.sharedAgileLogger ?? null; - }, + getLogger: loggerPackage.getLogger, logIfTags, + setAllowLogging, }; } else { tempLogCodeManager = { @@ -294,20 +317,45 @@ if (typeof process === 'object' && process.env.NODE_ENV !== 'production') { getLog: (logCode, replacers) => logCode, log, logCodeLogTypes: logCodeTypes, - logCodeMessages: logCodeMessages, - // Not doing 'logger: loggerPackage?.sharedAgileLogger' - // because only by calling a function (now 'getLogger()') the 'sharedLogger' is refetched + logCodeMessages: {} as any, getLogger: () => { return null; }, logIfTags: (tags, logCode, replacers) => { - /* empty */ + /* empty because logs with tags can't be that important */ }, + setAllowLogging, }; } + +/** + * The Log Code Manager keeps track + * and manages all important Logs for the '@agile-ts/core' package. + * + * @internal + */ export const LogCodeManager = tempLogCodeManager; export type LogCodesArrayType = { [K in keyof T]: T[K] extends string ? K : never; }[keyof T] & string; + +export interface LogCodeManagerInterface { + getLog: (logCode: LogCodesArrayType, replacers?: any[]) => string; + log: ( + logCode: LogCodesArrayType, + replacers?: any[], + ...data: any[] + ) => void; + logCodeLogTypes: typeof logCodeTypes; + logCodeMessages: T; + getLogger: () => any; + logIfTags: ( + tags: string[], + logCode: LogCodesArrayType, + replacers?: any[], + ...data: any[] + ) => void; + setAllowLogging: (logging: boolean) => void; +} diff --git a/packages/core/src/runtime/subscription/sub.controller.ts b/packages/core/src/runtime/subscription/sub.controller.ts index 51a76d4b..e83653be 100644 --- a/packages/core/src/runtime/subscription/sub.controller.ts +++ b/packages/core/src/runtime/subscription/sub.controller.ts @@ -331,7 +331,7 @@ export class SubController { } } -interface RegisterSubscriptionConfigInterface +export interface RegisterSubscriptionConfigInterface extends SubscriptionContainerConfigInterface { /** * Whether the Subscription Container shouldn't be ready diff --git a/packages/core/src/shared.ts b/packages/core/src/shared.ts index 604d1c89..a3ec1f87 100644 --- a/packages/core/src/shared.ts +++ b/packages/core/src/shared.ts @@ -1,11 +1,10 @@ -import { Agile, runsOnServer } from './internal'; +import { Agile } from './internal'; /** * Shared Agile Instance that is used when no Agile Instance was specified. */ let sharedAgileInstance = new Agile({ key: 'shared', - localStorage: !runsOnServer(), }); export { sharedAgileInstance as shared }; diff --git a/packages/core/src/state/index.ts b/packages/core/src/state/index.ts index ba8e7e27..6a208125 100644 --- a/packages/core/src/state/index.ts +++ b/packages/core/src/state/index.ts @@ -5,17 +5,15 @@ import { removeProperties, CreateAgileSubInstanceInterface, shared, + EnhancedState, } from '../internal'; export * from './state'; // export * from './state.observer'; +// export * from './state.enhanced'; // export * from './state.persistent'; // export * from './state.runtime.job'; -export interface CreateStateConfigInterfaceWithAgile - extends CreateAgileSubInstanceInterface, - StateConfigInterface {} - /** * Returns a newly created State. * @@ -31,7 +29,7 @@ export interface CreateStateConfigInterfaceWithAgile * @param initialValue - Initial value of the State. * @param config - Configuration object */ -export function createState( +export function createLightState( initialValue: ValueType, config: CreateStateConfigInterfaceWithAgile = {} ): State { @@ -44,3 +42,41 @@ export function createState( removeProperties(config, ['agileInstance']) ); } + +// TODO 'createState' doesn't get entirely treeshaken away (React project) +/** + * Returns a newly created enhanced State. + * + * An enhanced State manages, like a normal State, a piece of Information + * that we need to remember globally at a later point in time. + * While providing a toolkit to use and mutate this piece of Information. + * + * The main difference to a normal State is however + * that an enhanced State provides a wider variety of inbuilt utilities (like a persist, undo, watch functionality) + * but requires a larger bundle size in return. + * + * You can create as many global enhanced States as you need. + * + * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#createstate) + * + * @public + * @param initialValue - Initial value of the State. + * @param config - Configuration object + */ +export function createState( + initialValue: ValueType, + config: CreateStateConfigInterfaceWithAgile = {} +): EnhancedState { + config = defineConfig(config, { + agileInstance: shared, + }); + return new EnhancedState( + config.agileInstance as any, + initialValue, + removeProperties(config, ['agileInstance']) + ); +} + +export interface CreateStateConfigInterfaceWithAgile + extends CreateAgileSubInstanceInterface, + StateConfigInterface {} diff --git a/packages/core/src/state/state.enhanced.ts b/packages/core/src/state/state.enhanced.ts new file mode 100644 index 00000000..fe3a4c67 --- /dev/null +++ b/packages/core/src/state/state.enhanced.ts @@ -0,0 +1,544 @@ +import { + Agile, + defineConfig, + equal, + flatMerge, + generateId, + isFunction, + isValidObject, + LogCodeManager, + notEqual, + PersistentKey, + removeProperties, + State, + StateConfigInterface, + StateIngestConfigInterface, + StateKey, + StatePersistent, + StorageKey, +} from '../internal'; + +export class EnhancedState extends State { + // Whether the State is persisted in an external Storage + public isPersisted = false; + // Manages the permanent persistent in external Storages + public persistent: StatePersistent | undefined; + + // Method for dynamically computing the State value + public computeValueMethod?: ComputeValueMethod; + // Method for dynamically computing the existence of the State + public computeExistsMethod: ComputeExistsMethod; + + // When an interval is active, the 'intervalId' to clear the interval is temporary stored here + public currentInterval?: NodeJS.Timer | number; + + /** + * An enhanced State manages, like a normal State, a piece of Information + * that we need to remember globally at a later point in time. + * While providing a toolkit to use and mutate this piece of Information. + * + * The main difference to a normal State is however + * that an enhanced State provides a wider variety of inbuilt utilities (like a persist, undo, watch functionality) + * but requires a larger bundle size in return. + * + * You can create as many global enhanced States as you need. + * + * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#createstate) + * + * @public + * @param agileInstance - Instance of Agile the State belongs to. + * @param initialValue - Initial value of the State. + * @param config - Configuration object + */ + constructor( + agileInstance: Agile, + initialValue: ValueType, + config: StateConfigInterface = {} + ) { + super(agileInstance, initialValue, config); + this.computeExistsMethod = (v) => { + return v != null; + }; + } + + public setKey(value: StateKey | undefined): this { + const oldKey = this._key; + + // Update State key + super.setKey(value); + + // Update key in Persistent (only if oldKey is equal to persistentKey + // because otherwise the persistentKey is detached from the State key + // -> not managed by State anymore) + if (value != null && this.persistent?._key === oldKey) + this.persistent?.setKey(value); + + return this; + } + + /** + * Undoes the latest State value change. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#undo) + * + * @public + * @param config - Configuration object + */ + public undo(config: StateIngestConfigInterface = {}): this { + this.set(this.previousStateValue, config); + return this; + } + + /** + * Resets the State value to its initial value. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#reset) + * + * @public + * @param config - Configuration object + */ + public reset(config: StateIngestConfigInterface = {}): this { + this.set(this.initialStateValue, config); + return this; + } + + /** + * Merges the specified `targetWithChanges` object into the current State value. + * This merge can differ for different value combinations: + * - If the current State value is an `object`, it does a partial update for the object. + * - If the current State value is an `array` and the specified argument is an array too, + * it concatenates the current State value with the value of the argument. + * - If the current State value is neither an `object` nor an `array`, the patch can't be performed. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#patch) + * + * @public + * @param targetWithChanges - Object to be merged into the current State value. + * @param config - Configuration object + */ + public patch( + targetWithChanges: Object, + config: PatchConfigInterface = {} + ): this { + config = defineConfig(config, { + addNewProperties: true, + }); + + // Check if the given conditions are suitable for a patch action + if (!isValidObject(this.nextStateValue, true)) { + LogCodeManager.log('14:03:02'); + return this; + } + if (!isValidObject(targetWithChanges, true)) { + LogCodeManager.log('00:03:01', ['TargetWithChanges', 'object']); + return this; + } + + // Merge targetWithChanges object into the nextStateValue + if ( + Array.isArray(targetWithChanges) && + Array.isArray(this.nextStateValue) + ) { + this.nextStateValue = [ + ...this.nextStateValue, + ...targetWithChanges, + ] as any; + } else { + this.nextStateValue = flatMerge( + this.nextStateValue, + targetWithChanges, + { addNewProperties: config.addNewProperties } + ); + } + + // Ingest updated 'nextStateValue' into runtime + this.ingest(removeProperties(config, ['addNewProperties'])); + + return this; + } + + /** + * Fires on each State value change. + * + * Returns the key/name identifier of the created watcher callback. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#watch) + * + * @public + * @param callback - A function to be executed on each State value change. + */ + public watch(callback: StateWatcherCallback): string; + /** + * Fires on each State value change. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#watch) + * + * @public + * @param key - Key/Name identifier of the watcher callback. + * @param callback - A function to be executed on each State value change. + */ + public watch(key: string, callback: StateWatcherCallback): this; + public watch( + keyOrCallback: string | StateWatcherCallback, + callback?: StateWatcherCallback + ): this | string { + const generateKey = isFunction(keyOrCallback); + let _callback: StateWatcherCallback; + let key: string; + + if (generateKey) { + key = generateId(); + _callback = keyOrCallback as StateWatcherCallback; + } else { + key = keyOrCallback as string; + _callback = callback as StateWatcherCallback; + } + + if (!isFunction(_callback)) { + LogCodeManager.log('00:03:01', ['Watcher Callback', 'function']); + return this; + } + + this.addSideEffect( + key, + (instance) => { + _callback(instance.value, key); + }, + { weight: 0 } + ); + return generateKey ? key : this; + } + + /** + * Removes a watcher callback with the specified key/name identifier from the State. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#removewatcher) + * + * @public + * @param key - Key/Name identifier of the watcher callback to be removed. + */ + public removeWatcher(key: string): this { + this.removeSideEffect(key); + return this; + } + + /** + * Fires on the initial State value assignment and then destroys itself. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#oninaugurated) + * + * @public + * @param callback - A function to be executed after the first State value assignment. + */ + public onInaugurated(callback: StateWatcherCallback): this { + const watcherKey = 'InauguratedWatcherKey'; + this.watch(watcherKey, (value, key) => { + callback(value, key); + this.removeSideEffect(watcherKey); + }); + return this; + } + + /** + * Repeatedly calls the specified callback function, + * with a fixed time delay between each call. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#interval) + * + * @public + * @param handler - A function to be executed every delay milliseconds. + * @param delay - The time, in milliseconds (thousandths of a second), + * the timer should delay in between executions of the specified function. + */ + public interval( + handler: (value: ValueType) => ValueType, + delay?: number + ): this { + if (!isFunction(handler)) { + LogCodeManager.log('00:03:01', ['Interval Callback', 'function']); + return this; + } + if (this.currentInterval) { + LogCodeManager.log('14:03:03', [], this.currentInterval); + return this; + } + this.currentInterval = setInterval(() => { + this.set(handler(this._value)); + }, delay ?? 1000); + return this; + } + + /** + * Cancels a active timed, repeating action + * which was previously established by a call to `interval()`. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#clearinterval) + * + * @public + */ + public clearInterval(): void { + if (this.currentInterval) { + clearInterval(this.currentInterval as number); + delete this.currentInterval; + } + } + + /** + * Returns a boolean indicating whether the State exists. + * + * It calculates the value based on the `computeExistsMethod()` + * and whether the State is a placeholder. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#exists) + * + * @public + */ + public get exists(): boolean { + return !this.isPlaceholder && this.computeExistsMethod(this.value); + } + + /** + * Defines the method used to compute the existence of the State. + * + * It is retrieved on each `exists()` method call + * to determine whether the State exists or not. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#computeexists) + * + * @public + * @param method - Method to compute the existence of the State. + */ + public computeExists(method: ComputeExistsMethod): this { + if (!isFunction(method)) { + LogCodeManager.log('00:03:01', ['Compute Exists Method', 'function']); + return this; + } + this.computeExistsMethod = method; + return this; + } + + /** + * Defines the method used to compute the value of the State. + * + * It is retrieved on each State value change, + * in order to compute the new State value + * based on the specified compute method. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#computevalue) + * + * @public + * @param method - Method to compute the value of the State. + */ + public computeValue(method: ComputeValueMethod): this { + if (!isFunction(method)) { + LogCodeManager.log('00:03:01', ['Compute Value Method', 'function']); + return this; + } + this.computeValueMethod = method; + + // Initial compute + // (not directly computing it here since it is computed once in the runtime!) + this.set(this.nextStateValue); + + return this; + } + + /** + * Returns a boolean indicating whether the specified value is equal to the current State value. + * + * Equivalent to `===` with the difference that it looks at the value + * and not on the reference in the case of objects. + * + * @public + * @param value - Value to be compared with the current State value. + */ + public is(value: ValueType): boolean { + return equal(value, this.value); + } + + /** + * Returns a boolean indicating whether the specified value is not equal to the current State value. + * + * Equivalent to `!==` with the difference that it looks at the value + * and not on the reference in the case of objects. + * + * @public + * @param value - Value to be compared with the current State value. + */ + public isNot(value: ValueType): boolean { + return notEqual(value, this.value); + } + + /** + * Inverts the current State value. + * + * Some examples are: + * - `'jeff'` -> `'ffej'` + * - `true` -> `false` + * - `[1, 2, 3]` -> `[3, 2, 1]` + * - `10` -> `-10` + * + * @public + */ + public invert(): this { + switch (typeof this.nextStateValue) { + case 'boolean': + this.set(!this.nextStateValue as any); + break; + case 'object': + if (Array.isArray(this.nextStateValue)) + this.set(this.nextStateValue.reverse() as any); + break; + case 'string': + this.set(this.nextStateValue.split('').reverse().join('') as any); + break; + case 'number': + this.set((this.nextStateValue * -1) as any); + break; + default: + LogCodeManager.log('14:03:04', [typeof this.nextStateValue]); + } + return this; + } + + /** + * Preserves the State `value` in the corresponding external Storage. + * + * The State key/name is used as the unique identifier for the Persistent. + * If that is not desired or the State has no unique identifier, + * please specify a separate unique identifier for the Persistent. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#persist) + * + * @public + * @param config - Configuration object + */ + public persist(config?: StatePersistentConfigInterface): this; + /** + * Preserves the State `value` in the corresponding external Storage. + * + * The specified key is used as the unique identifier for the Persistent. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#persist) + * + * @public + * @param key - Key/Name identifier of Persistent. + * @param config - Configuration object + */ + public persist( + key?: PersistentKey, + config?: StatePersistentConfigInterface + ): this; + public persist( + keyOrConfig: PersistentKey | StatePersistentConfigInterface = {}, + config: StatePersistentConfigInterface = {} + ): this { + let _config: StatePersistentConfigInterface; + let key: PersistentKey | undefined; + + if (isValidObject(keyOrConfig)) { + _config = keyOrConfig as StatePersistentConfigInterface; + key = this._key; + } else { + _config = config || {}; + key = keyOrConfig as PersistentKey; + } + + _config = defineConfig(_config, { + loadValue: true, + storageKeys: [], + defaultStorageKey: null as any, + }); + + // Check if State is already persisted + if (this.persistent != null && this.isPersisted) return this; + + // Create Persistent (-> persist value) + this.persistent = new StatePersistent(this, { + instantiate: _config.loadValue, + storageKeys: _config.storageKeys, + key: key, + defaultStorageKey: _config.defaultStorageKey, + }); + + return this; + } + + /** + * Fires immediately after the persisted `value` + * is loaded into the State from a corresponding external Storage. + * + * Registering such callback function makes only sense + * when the State is [persisted](https://agile-ts.org/docs/core/state/methods/#persist). + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#onload) + * + * @public + * @param callback - A function to be executed after the externally persisted `value` was loaded into the State. + */ + public onLoad(callback: (success: boolean) => void): this { + if (!this.persistent) return this; + if (!isFunction(callback)) { + LogCodeManager.log('00:03:01', ['OnLoad Callback', 'function']); + return this; + } + + // Register specified callback + this.persistent.onLoad = callback; + + // If State is already persisted ('isPersisted') fire specified callback immediately + if (this.isPersisted) callback(true); + + return this; + } + + /** + * Returns the persistable value of the State. + * + * @internal + */ + public getPersistableValue(): any { + return this._value; + } +} + +export interface PatchConfigInterface + extends StateIngestConfigInterface, + PatchOptionConfigInterface {} + +export interface PatchOptionConfigInterface { + /** + * Whether to add new properties to the object during the merge. + * @default true + */ + addNewProperties?: boolean; +} + +export interface StatePersistentConfigInterface { + /** + * Whether the Persistent should automatically load + * the persisted value into the State after its instantiation. + * @default true + */ + loadValue?: boolean; + /** + * Key/Name identifier of Storages + * in which the State value should be or is persisted. + * @default [`defaultStorageKey`] + */ + storageKeys?: StorageKey[]; + /** + * Key/Name identifier of the default Storage of the specified Storage keys. + * + * The State value is loaded from the default Storage by default + * and is only loaded from the remaining Storages (`storageKeys`) + * if the loading from the default Storage failed. + * + * @default first index of the specified Storage keys or the AgileTs default Storage key + */ + defaultStorageKey?: StorageKey; +} + +export type StateWatcherCallback = (value: T, key: string) => void; +export type ComputeValueMethod = (value: T) => T; +export type ComputeExistsMethod = (value: T) => boolean; diff --git a/packages/core/src/state/state.observer.ts b/packages/core/src/state/state.observer.ts index ab02dc7c..f2ef181c 100644 --- a/packages/core/src/state/state.observer.ts +++ b/packages/core/src/state/state.observer.ts @@ -15,6 +15,7 @@ import { ObserverKey, defineConfig, } from '../internal'; +import type { EnhancedState } from '../internal'; export class StateObserver extends Observer { // State the Observer belongs to @@ -105,9 +106,9 @@ export class StateObserver extends Observer { config.overwrite = true; } - // Assign next State value to Observer and compute it if necessary - this.nextStateValue = state.computeValueMethod - ? copy(state.computeValueMethod(newStateValue)) + // Assign next State value to Observer and compute it if necessary (enhanced State) + this.nextStateValue = (state as any).computeValueMethod + ? copy((state as any).computeValueMethod(newStateValue)) : copy(newStateValue); // Check if current State value and to assign State value are equal diff --git a/packages/core/src/state/state.persistent.ts b/packages/core/src/state/state.persistent.ts index d978196c..5a1d7f58 100644 --- a/packages/core/src/state/state.persistent.ts +++ b/packages/core/src/state/state.persistent.ts @@ -1,14 +1,15 @@ import { CreatePersistentConfigInterface, defineConfig, + EnhancedState, + getStorageManager, Persistent, PersistentKey, - State, } from '../internal'; export class StatePersistent extends Persistent { // State the Persistent belongs to - public state: () => State; + public state: () => EnhancedState; static storeValueSideEffectKey = 'rebuildStateStorageValue'; @@ -20,7 +21,7 @@ export class StatePersistent extends Persistent { * @param config - Configuration object */ constructor( - state: State, + state: EnhancedState, config: CreatePersistentConfigInterface = {} ) { super(state.agileInstance(), { @@ -72,7 +73,7 @@ export class StatePersistent extends Persistent { const _storageItemKey = storageItemKey ?? this._key; // Load State value from the default Storage - const loadedValue = await this.agileInstance().storages.get( + const loadedValue = await getStorageManager()?.get( _storageItemKey, this.config.defaultStorageKey as any ); @@ -150,7 +151,7 @@ export class StatePersistent extends Persistent { if (!this.ready) return false; const _storageItemKey = storageItemKey || this._key; this.state().removeSideEffect(StatePersistent.storeValueSideEffectKey); - this.agileInstance().storages.remove(_storageItemKey, this.storageKeys); + getStorageManager()?.remove(_storageItemKey, this.storageKeys); this.isPersisted = false; return true; } @@ -184,12 +185,12 @@ export class StatePersistent extends Persistent { * @param config - Configuration object */ public rebuildStorageSideEffect( - state: State, + state: EnhancedState, storageItemKey: PersistentKey, config: { [key: string]: any } = {} ) { if (config['storage'] == null || config.storage) { - this.agileInstance().storages.set( + getStorageManager()?.set( storageItemKey, this.state().getPersistableValue(), this.storageKeys diff --git a/packages/core/src/state/state.ts b/packages/core/src/state/state.ts index 592a4e36..c4b5b0ab 100644 --- a/packages/core/src/state/state.ts +++ b/packages/core/src/state/state.ts @@ -1,20 +1,11 @@ import { Agile, - StorageKey, copy, - flatMerge, - isValidObject, StateObserver, - StatePersistent, Observer, - equal, isFunction, - notEqual, - generateId, - PersistentKey, ComputedTracker, StateIngestConfigInterface, - removeProperties, LogCodeManager, defineConfig, } from '../internal'; @@ -47,19 +38,6 @@ export class State { [key: string]: SideEffectInterface>; } = {}; - // Method for dynamically computing the State value - public computeValueMethod?: ComputeValueMethod; - // Method for dynamically computing the existence of the State - public computeExistsMethod: ComputeExistsMethod; - - // Whether the State is persisted in an external Storage - public isPersisted = false; - // Manages the permanent persistent in external Storages - public persistent: StatePersistent | undefined; - - // When an interval is active, the 'intervalId' to clear the interval is temporary stored here - public currentInterval?: NodeJS.Timer | number; - /** * A State manages a piece of Information * that we need to remember globally at a later point in time. @@ -94,9 +72,6 @@ export class State { this.previousStateValue = copy(initialValue); this.nextStateValue = copy(initialValue); this.isPlaceholder = true; - this.computeExistsMethod = (v) => { - return v != null; - }; // Set State value to specified initial value if (!config.isPlaceholder) this.set(initialValue, { overwrite: true }); @@ -159,8 +134,6 @@ export class State { * @param value - New key/name identifier. */ public setKey(value: StateKey | undefined): this { - const oldKey = this._key; - // Update State key this._key = value; @@ -168,12 +141,6 @@ export class State { for (const observerKey in this.observers) this.observers[observerKey]._key = value; - // Update key in Persistent (only if oldKey is equal to persistentKey - // because otherwise the persistentKey is detached from the State key - // -> not managed by State anymore) - if (value != null && this.persistent?._key === oldKey) - this.persistent?.setKey(value); - return this; } @@ -221,422 +188,6 @@ export class State { return this; } - /** - * Undoes the latest State value change. - * - * [Learn more..](https://agile-ts.org/docs/core/state/methods/#undo) - * - * @public - * @param config - Configuration object - */ - public undo(config: StateIngestConfigInterface = {}): this { - this.set(this.previousStateValue, config); - return this; - } - - /** - * Resets the State value to its initial value. - * - * [Learn more..](https://agile-ts.org/docs/core/state/methods/#reset) - * - * @public - * @param config - Configuration object - */ - public reset(config: StateIngestConfigInterface = {}): this { - this.set(this.initialStateValue, config); - return this; - } - - /** - * Merges the specified `targetWithChanges` object into the current State value. - * This merge can differ for different value combinations: - * - If the current State value is an `object`, it does a partial update for the object. - * - If the current State value is an `array` and the specified argument is an array too, - * it concatenates the current State value with the value of the argument. - * - If the current State value is neither an `object` nor an `array`, the patch can't be performed. - * - * [Learn more..](https://agile-ts.org/docs/core/state/methods/#patch) - * - * @public - * @param targetWithChanges - Object to be merged into the current State value. - * @param config - Configuration object - */ - public patch( - targetWithChanges: Object, - config: PatchConfigInterface = {} - ): this { - config = defineConfig(config, { - addNewProperties: true, - }); - - // Check if the given conditions are suitable for a patch action - if (!isValidObject(this.nextStateValue, true)) { - LogCodeManager.log('14:03:02'); - return this; - } - if (!isValidObject(targetWithChanges, true)) { - LogCodeManager.log('00:03:01', ['TargetWithChanges', 'object']); - return this; - } - - // Merge targetWithChanges object into the nextStateValue - if ( - Array.isArray(targetWithChanges) && - Array.isArray(this.nextStateValue) - ) { - this.nextStateValue = [ - ...this.nextStateValue, - ...targetWithChanges, - ] as any; - } else { - this.nextStateValue = flatMerge( - this.nextStateValue, - targetWithChanges, - { addNewProperties: config.addNewProperties } - ); - } - - // Ingest updated 'nextStateValue' into runtime - this.ingest(removeProperties(config, ['addNewProperties'])); - - return this; - } - - /** - * Fires on each State value change. - * - * Returns the key/name identifier of the created watcher callback. - * - * [Learn more..](https://agile-ts.org/docs/core/state/methods/#watch) - * - * @public - * @param callback - A function to be executed on each State value change. - */ - public watch(callback: StateWatcherCallback): string; - /** - * Fires on each State value change. - * - * [Learn more..](https://agile-ts.org/docs/core/state/methods/#watch) - * - * @public - * @param key - Key/Name identifier of the watcher callback. - * @param callback - A function to be executed on each State value change. - */ - public watch(key: string, callback: StateWatcherCallback): this; - public watch( - keyOrCallback: string | StateWatcherCallback, - callback?: StateWatcherCallback - ): this | string { - const generateKey = isFunction(keyOrCallback); - let _callback: StateWatcherCallback; - let key: string; - - if (generateKey) { - key = generateId(); - _callback = keyOrCallback as StateWatcherCallback; - } else { - key = keyOrCallback as string; - _callback = callback as StateWatcherCallback; - } - - if (!isFunction(_callback)) { - LogCodeManager.log('00:03:01', ['Watcher Callback', 'function']); - return this; - } - - this.addSideEffect( - key, - (instance) => { - _callback(instance.value, key); - }, - { weight: 0 } - ); - return generateKey ? key : this; - } - - /** - * Removes a watcher callback with the specified key/name identifier from the State. - * - * [Learn more..](https://agile-ts.org/docs/core/state/methods/#removewatcher) - * - * @public - * @param key - Key/Name identifier of the watcher callback to be removed. - */ - public removeWatcher(key: string): this { - this.removeSideEffect(key); - return this; - } - - /** - * Fires on the initial State value assignment and then destroys itself. - * - * [Learn more..](https://agile-ts.org/docs/core/state/methods/#oninaugurated) - * - * @public - * @param callback - A function to be executed after the first State value assignment. - */ - public onInaugurated(callback: StateWatcherCallback): this { - const watcherKey = 'InauguratedWatcherKey'; - this.watch(watcherKey, (value, key) => { - callback(value, key); - this.removeSideEffect(watcherKey); - }); - return this; - } - - /** - * Preserves the State `value` in the corresponding external Storage. - * - * The State key/name is used as the unique identifier for the Persistent. - * If that is not desired or the State has no unique identifier, - * please specify a separate unique identifier for the Persistent. - * - * [Learn more..](https://agile-ts.org/docs/core/state/methods/#persist) - * - * @public - * @param config - Configuration object - */ - public persist(config?: StatePersistentConfigInterface): this; - /** - * Preserves the State `value` in the corresponding external Storage. - * - * The specified key is used as the unique identifier for the Persistent. - * - * [Learn more..](https://agile-ts.org/docs/core/state/methods/#persist) - * - * @public - * @param key - Key/Name identifier of Persistent. - * @param config - Configuration object - */ - public persist( - key?: PersistentKey, - config?: StatePersistentConfigInterface - ): this; - public persist( - keyOrConfig: PersistentKey | StatePersistentConfigInterface = {}, - config: StatePersistentConfigInterface = {} - ): this { - let _config: StatePersistentConfigInterface; - let key: PersistentKey | undefined; - - if (isValidObject(keyOrConfig)) { - _config = keyOrConfig as StatePersistentConfigInterface; - key = this._key; - } else { - _config = config || {}; - key = keyOrConfig as PersistentKey; - } - - _config = defineConfig(_config, { - loadValue: true, - storageKeys: [], - defaultStorageKey: null as any, - }); - - // Check if State is already persisted - if (this.persistent != null && this.isPersisted) return this; - - // Create Persistent (-> persist value) - this.persistent = new StatePersistent(this, { - instantiate: _config.loadValue, - storageKeys: _config.storageKeys, - key: key, - defaultStorageKey: _config.defaultStorageKey, - }); - - return this; - } - - /** - * Fires immediately after the persisted `value` - * is loaded into the State from a corresponding external Storage. - * - * Registering such callback function makes only sense - * when the State is [persisted](https://agile-ts.org/docs/core/state/methods/#persist). - * - * [Learn more..](https://agile-ts.org/docs/core/state/methods/#onload) - * - * @public - * @param callback - A function to be executed after the externally persisted `value` was loaded into the State. - */ - public onLoad(callback: (success: boolean) => void): this { - if (!this.persistent) return this; - if (!isFunction(callback)) { - LogCodeManager.log('00:03:01', ['OnLoad Callback', 'function']); - return this; - } - - // Register specified callback - this.persistent.onLoad = callback; - - // If State is already persisted ('isPersisted') fire specified callback immediately - if (this.isPersisted) callback(true); - - return this; - } - - /** - * Repeatedly calls the specified callback function, - * with a fixed time delay between each call. - * - * [Learn more..](https://agile-ts.org/docs/core/state/methods/#interval) - * - * @public - * @param handler - A function to be executed every delay milliseconds. - * @param delay - The time, in milliseconds (thousandths of a second), - * the timer should delay in between executions of the specified function. - */ - public interval( - handler: (value: ValueType) => ValueType, - delay?: number - ): this { - if (!isFunction(handler)) { - LogCodeManager.log('00:03:01', ['Interval Callback', 'function']); - return this; - } - if (this.currentInterval) { - LogCodeManager.log('14:03:03', [], this.currentInterval); - return this; - } - this.currentInterval = setInterval(() => { - this.set(handler(this._value)); - }, delay ?? 1000); - return this; - } - - /** - * Cancels a active timed, repeating action - * which was previously established by a call to `interval()`. - * - * [Learn more..](https://agile-ts.org/docs/core/state/methods/#clearinterval) - * - * @public - */ - public clearInterval(): void { - if (this.currentInterval) { - clearInterval(this.currentInterval as number); - delete this.currentInterval; - } - } - - /** - * Returns a boolean indicating whether the State exists. - * - * It calculates the value based on the `computeExistsMethod()` - * and whether the State is a placeholder. - * - * [Learn more..](https://agile-ts.org/docs/core/state/methods/#exists) - * - * @public - */ - public get exists(): boolean { - return !this.isPlaceholder && this.computeExistsMethod(this.value); - } - - /** - * Defines the method used to compute the existence of the State. - * - * It is retrieved on each `exists()` method call - * to determine whether the State exists or not. - * - * [Learn more..](https://agile-ts.org/docs/core/state/methods/#computeexists) - * - * @public - * @param method - Method to compute the existence of the State. - */ - public computeExists(method: ComputeExistsMethod): this { - if (!isFunction(method)) { - LogCodeManager.log('00:03:01', ['Compute Exists Method', 'function']); - return this; - } - this.computeExistsMethod = method; - return this; - } - - /** - * Defines the method used to compute the value of the State. - * - * It is retrieved on each State value change, - * in order to compute the new State value - * based on the specified compute method. - * - * [Learn more..](https://agile-ts.org/docs/core/state/methods/#computevalue) - * - * @public - * @param method - Method to compute the value of the State. - */ - public computeValue(method: ComputeValueMethod): this { - if (!isFunction(method)) { - LogCodeManager.log('00:03:01', ['Compute Value Method', 'function']); - return this; - } - this.computeValueMethod = method; - - // Initial compute - // (not directly computing it here since it is computed once in the runtime!) - this.set(this.nextStateValue); - - return this; - } - - /** - * Returns a boolean indicating whether the specified value is equal to the current State value. - * - * Equivalent to `===` with the difference that it looks at the value - * and not on the reference in the case of objects. - * - * @public - * @param value - Value to be compared with the current State value. - */ - public is(value: ValueType): boolean { - return equal(value, this.value); - } - - /** - * Returns a boolean indicating whether the specified value is not equal to the current State value. - * - * Equivalent to `!==` with the difference that it looks at the value - * and not on the reference in the case of objects. - * - * @public - * @param value - Value to be compared with the current State value. - */ - public isNot(value: ValueType): boolean { - return notEqual(value, this.value); - } - - /** - * Inverts the current State value. - * - * Some examples are: - * - `'jeff'` -> `'ffej'` - * - `true` -> `false` - * - `[1, 2, 3]` -> `[3, 2, 1]` - * - `10` -> `-10` - * - * @public - */ - public invert(): this { - switch (typeof this.nextStateValue) { - case 'boolean': - this.set(!this.nextStateValue as any); - break; - case 'object': - if (Array.isArray(this.nextStateValue)) - this.set(this.nextStateValue.reverse() as any); - break; - case 'string': - this.set(this.nextStateValue.split('').reverse().join('') as any); - break; - case 'number': - this.set((this.nextStateValue * -1) as any); - break; - default: - LogCodeManager.log('14:03:04', [typeof this.nextStateValue]); - } - return this; - } - /** * * Registers a `callback` function that is executed in the `runtime` @@ -696,15 +247,6 @@ export class State { public hasSideEffect(key: string): boolean { return !!this.sideEffects[key]; } - - /** - * Returns the persistable value of the State. - * - * @internal - */ - public getPersistableValue(): any { - return this._value; - } } export type StateKey = string | number; @@ -735,47 +277,6 @@ export interface StateConfigInterface { isPlaceholder?: boolean; } -export interface PatchConfigInterface - extends StateIngestConfigInterface, - PatchOptionConfigInterface {} - -export interface PatchOptionConfigInterface { - /** - * Whether to add new properties to the object during the merge. - * @default true - */ - addNewProperties?: boolean; -} - -export interface StatePersistentConfigInterface { - /** - * Whether the Persistent should automatically load - * the persisted value into the State after its instantiation. - * @default true - */ - loadValue?: boolean; - /** - * Key/Name identifier of Storages - * in which the State value should be or is persisted. - * @default [`defaultStorageKey`] - */ - storageKeys?: StorageKey[]; - /** - * Key/Name identifier of the default Storage of the specified Storage keys. - * - * The State value is loaded from the default Storage by default - * and is only loaded from the remaining Storages (`storageKeys`) - * if the loading from the default Storage failed. - * - * @default first index of the specified Storage keys or the AgileTs default Storage key - */ - defaultStorageKey?: StorageKey; -} - -export type StateWatcherCallback = (value: T, key: string) => void; -export type ComputeValueMethod = (value: T) => T; -export type ComputeExistsMethod = (value: T) => boolean; - export type SideEffectFunctionType = ( instance: Instance, properties?: { diff --git a/packages/core/src/storages/index.ts b/packages/core/src/storages/index.ts index a8938be1..b373d0fc 100644 --- a/packages/core/src/storages/index.ts +++ b/packages/core/src/storages/index.ts @@ -1,9 +1,23 @@ -import { CreateStorageConfigInterface, Storage } from '../internal'; +import { + CreateStorageConfigInterface, + Storage, + Storages, + shared, + CreateStoragesConfigInterface, + CreateAgileSubInstanceInterface, + defineConfig, + removeProperties, + LogCodeManager, + runsOnServer, +} from '../internal'; export * from './storages'; // export * from './storage'; // export * from './persistent'; +// Handles the permanent persistence of Agile Classes +let storageManager: Storages | null = null; + /** * Returns a newly created Storage. * @@ -22,3 +36,56 @@ export * from './storages'; export function createStorage(config: CreateStorageConfigInterface): Storage { return new Storage(config); } + +/** + * Returns a newly created Storage Manager. + * + * A Storage Manager manages all external Storages for AgileTs + * and provides an interface to easily store, + * load and remove values from multiple external Storages at once. + * + * @param config - Configuration object + */ +export function createStorageManager( + config: CreateStorageManagerConfigInterfaceWithAgile = {} +): Storages { + config = defineConfig(config, { + agileInstance: shared, + }); + return new Storages( + config.agileInstance as any, + removeProperties(config, ['agileInstance']) + ); +} + +/** + * Returns the shared Storage Manager + * or creates a new one when no shared Storage Manager exists. + */ +export function getStorageManager(): Storages { + if (storageManager == null) { + const newStorageManager = createStorageManager({ + localStorage: !runsOnServer(), + }); + assignSharedAgileStorageManager(newStorageManager); + return newStorageManager; + } + return storageManager; +} + +/** + * Assigns the specified Storage Manager + * as default (shared) Storage Manager for all Agile Instances. + * + * @param instance - Storage Manager to be registered as the default Storage Manager. + */ +export const assignSharedAgileStorageManager = (instance: Storages | null) => { + if (storageManager != null) { + LogCodeManager.log('11:02:06', [], storageManager); + } + storageManager = instance; +}; + +export interface CreateStorageManagerConfigInterfaceWithAgile + extends CreateAgileSubInstanceInterface, + CreateStoragesConfigInterface {} diff --git a/packages/core/src/storages/persistent.ts b/packages/core/src/storages/persistent.ts index e4134fca..11deec56 100644 --- a/packages/core/src/storages/persistent.ts +++ b/packages/core/src/storages/persistent.ts @@ -2,6 +2,7 @@ import { Agile, copy, defineConfig, + getStorageManager, LogCodeManager, StorageKey, } from '../internal'; @@ -49,7 +50,6 @@ export class Persistent { storageKeys: [], defaultStorageKey: null as any, }); - this.agileInstance().storages.persistentInstances.add(this); this.config = { defaultStorageKey: config.defaultStorageKey as any }; // Instantiate Persistent @@ -127,6 +127,12 @@ export class Persistent { this._key = this.formatKey(config.key) ?? Persistent.placeHolderKey; this.assignStorageKeys(config.storageKeys, config.defaultStorageKey); this.validatePersistent(); + + // Register Persistent to Storage Manager + const storageManager = getStorageManager(); + if (this._key !== Persistent.placeHolderKey && storageManager != null) { + storageManager.persistentInstances[this._key] = this; + } } /** @@ -155,7 +161,7 @@ export class Persistent { // Check if the Storages exist at the specified Storage keys this.storageKeys.map((key) => { - if (!this.agileInstance().storages.storages[key]) { + if (!getStorageManager()?.storages[key]) { LogCodeManager.log('12:03:02', [this._key, key]); isValid = false; } @@ -180,7 +186,6 @@ export class Persistent { storageKeys: StorageKey[] = [], defaultStorageKey?: StorageKey ): void { - const storages = this.agileInstance().storages; const _storageKeys = copy(storageKeys); // Assign specified default Storage key to the 'storageKeys' array @@ -191,10 +196,10 @@ export class Persistent { // and specify it as the Persistent's default Storage key // if no valid Storage key was provided if (_storageKeys.length <= 0) { - const defaultStorageKey = storages.config.defaultStorageKey; + const defaultStorageKey = getStorageManager()?.config.defaultStorageKey; if (defaultStorageKey != null) { this.config.defaultStorageKey = defaultStorageKey; - _storageKeys.push(storages.config.defaultStorageKey as any); + _storageKeys.push(getStorageManager()?.config.defaultStorageKey as any); } } else { this.config.defaultStorageKey = defaultStorageKey ?? _storageKeys[0]; diff --git a/packages/core/src/storages/storages.ts b/packages/core/src/storages/storages.ts index 498f9bb5..99fcfa9b 100644 --- a/packages/core/src/storages/storages.ts +++ b/packages/core/src/storages/storages.ts @@ -18,7 +18,7 @@ export class Storages { // Registered Storages public storages: { [key: string]: Storage } = {}; // Persistent from Instances (for example States) that were persisted - public persistentInstances: Set = new Set(); + public persistentInstances: { [key: string]: Persistent } = {}; /** * The Storages Class manages all external Storages for an Agile Instance @@ -97,12 +97,15 @@ export class Storages { this.storages[storage.key] = storage; if (config.default) this.config.defaultStorageKey = storage.key; - this.persistentInstances.forEach((persistent) => { + for (const persistentKey of Object.keys(this.persistentInstances)) { + const persistent = this.persistentInstances[persistentKey]; + if (persistent == null) continue; + // Revalidate Persistent, which contains key/name identifier of the newly registered Storage if (persistent.storageKeys.includes(storage.key)) { const isValid = persistent.validatePersistent(); if (isValid) persistent.initialLoading(); - return; + continue; } // If Persistent has no default Storage key, @@ -113,7 +116,7 @@ export class Storages { const isValid = persistent.validatePersistent(); if (isValid) persistent.initialLoading(); } - }); + } LogCodeManager.log('13:00:00', [storage.key], storage); diff --git a/packages/core/tests/integration/collection.persistent.integration.test.ts b/packages/core/tests/integration/collection.persistent.integration.test.ts index bac84d63..c9a85992 100644 --- a/packages/core/tests/integration/collection.persistent.integration.test.ts +++ b/packages/core/tests/integration/collection.persistent.integration.test.ts @@ -1,4 +1,11 @@ -import { Agile, Item, createStorage, createCollection } from '../../src'; +import { + Agile, + Item, + createStorage, + createCollection, + createStorageManager, + assignSharedAgileStorageManager, +} from '../../src'; import { LogMock } from '../helper/logMock'; describe('Collection Persist Function Tests', () => { @@ -28,8 +35,11 @@ describe('Collection Persist Function Tests', () => { LogMock.mockLogs(); jest.clearAllMocks(); - App = new Agile({ localStorage: false }); - App.registerStorage( + App = new Agile(); + + const storageManager = createStorageManager({ localStorage: false }); + assignSharedAgileStorageManager(storageManager); + storageManager.register( createStorage({ key: 'testStorage', prefix: 'test', diff --git a/packages/core/tests/unit/agile.test.ts b/packages/core/tests/unit/agile.test.ts index ee68d7ee..40682716 100644 --- a/packages/core/tests/unit/agile.test.ts +++ b/packages/core/tests/unit/agile.test.ts @@ -1,11 +1,4 @@ -import { - Agile, - Runtime, - SubController, - Integrations, - Storage, - Storages, -} from '../../src'; +import { Agile, Runtime, SubController, Integrations } from '../../src'; import testIntegration from '../helper/test.integration'; import { LogMock } from '../helper/logMock'; @@ -25,11 +18,6 @@ jest.mock('../../src/runtime/subscription/sub.controller', () => { SubController: jest.fn(), }; }); -jest.mock('../../src/storages', () => { - return { - Storages: jest.fn(), - }; -}); // https://gist.github.com/virgs/d9c50e878fc69832c01f8085f2953f12 // https://medium.com/@masonlgoetz/mock-static-class-methods-in-jest-1ceda967b47f @@ -55,7 +43,6 @@ describe('Agile Tests', () => { const SubControllerMock = SubController as jest.MockedClass< typeof SubController >; - const StoragesMock = Storages as jest.MockedClass; const IntegrationsMock = Integrations as jest.MockedClass< typeof Integrations >; @@ -66,7 +53,6 @@ describe('Agile Tests', () => { // Clear specified mocks RuntimeMock.mockClear(); SubControllerMock.mockClear(); - StoragesMock.mockClear(); IntegrationsMock.mockClear(); // Reset globalThis @@ -93,10 +79,6 @@ describe('Agile Tests', () => { // expect(agile.runtime).toBeInstanceOf(Runtime); // Because 'Runtime' is completely overwritten with a mock (mockImplementation) expect(SubControllerMock).toHaveBeenCalledWith(agile); expect(agile.subController).toBeInstanceOf(SubController); - expect(StoragesMock).toHaveBeenCalledWith(agile, { - localStorage: false, - }); - expect(agile.storages).toBeInstanceOf(Storages); // Check if Agile Instance got bound globally expect(globalThis[Agile.globalKey]).toBeUndefined(); @@ -106,7 +88,6 @@ describe('Agile Tests', () => { const agile = new Agile({ waitForMount: false, bucket: false, - localStorage: true, bindGlobal: true, key: 'jeff', autoIntegrate: false, @@ -125,10 +106,6 @@ describe('Agile Tests', () => { // expect(agile.runtime).toBeInstanceOf(Runtime); // Because 'Runtime' is completely overwritten with a mock (mockImplementation) expect(SubControllerMock).toHaveBeenCalledWith(agile); expect(agile.subController).toBeInstanceOf(SubController); - expect(StoragesMock).toHaveBeenCalledWith(agile, { - localStorage: true, - }); - expect(agile.storages).toBeInstanceOf(Storages); // Check if Agile Instance got bound globally expect(globalThis[Agile.globalKey]).toBe(agile); @@ -174,39 +151,6 @@ describe('Agile Tests', () => { }); }); - describe('registerStorage function tests', () => { - beforeEach(() => { - agile.storages.register = jest.fn(); - }); - - it('should register provided Storage', () => { - const dummyStorage = new Storage({ - prefix: 'test', - methods: { - get: () => { - /* empty function */ - }, - set: () => { - /* empty function */ - }, - remove: () => { - /* empty function */ - }, - }, - key: 'myTestStorage', - }); - - const returnedAgile = agile.registerStorage(dummyStorage, { - default: false, - }); - - expect(returnedAgile).toBe(agile); - expect(agile.storages.register).toHaveBeenCalledWith(dummyStorage, { - default: false, - }); - }); - }); - describe('hasIntegration function tests', () => { it('should check if Agile has any registered Integration', () => { agile.hasIntegration(); @@ -214,17 +158,5 @@ describe('Agile Tests', () => { expect(agile.integrations.hasIntegration).toHaveBeenCalled(); }); }); - - describe('hasStorage function tests', () => { - beforeEach(() => { - agile.storages.hasStorage = jest.fn(); - }); - - it('should check if Agile has any registered Storage', () => { - agile.hasStorage(); - - expect(agile.storages.hasStorage).toHaveBeenCalled(); - }); - }); }); }); diff --git a/packages/core/tests/unit/collection/collection.persistent.test.ts b/packages/core/tests/unit/collection/collection.persistent.test.ts index 838587fd..b4f64ef1 100644 --- a/packages/core/tests/unit/collection/collection.persistent.test.ts +++ b/packages/core/tests/unit/collection/collection.persistent.test.ts @@ -7,8 +7,12 @@ import { StatePersistent, Group, Item, + assignSharedAgileStorageManager, + createStorageManager, + Storages, } from '../../../src'; import { LogMock } from '../../helper/logMock'; +import waitForExpect from 'wait-for-expect'; describe('CollectionPersistent Tests', () => { interface ItemInterface { @@ -18,15 +22,20 @@ describe('CollectionPersistent Tests', () => { let dummyAgile: Agile; let dummyCollection: Collection; + let storageManager: Storages; beforeEach(() => { LogMock.mockLogs(); - dummyAgile = new Agile({ localStorage: false }); + dummyAgile = new Agile(); dummyCollection = new Collection(dummyAgile, { key: 'dummyCollectionKey', }); + // Register Storage Manager + storageManager = createStorageManager(); + assignSharedAgileStorageManager(storageManager); + jest.spyOn(CollectionPersistent.prototype, 'instantiatePersistent'); jest.spyOn(CollectionPersistent.prototype, 'initialLoading'); @@ -169,7 +178,7 @@ describe('CollectionPersistent Tests', () => { key: 'collectionPersistentKey', storageKeys: ['dummyStorage'], }); - dummyAgile.registerStorage( + storageManager.register( new Storage({ key: 'dummyStorage', methods: { @@ -226,8 +235,10 @@ describe('CollectionPersistent Tests', () => { it('should call initialLoad in parent and set Collection.isPersisted to true', async () => { await collectionPersistent.initialLoading(); - expect(Persistent.prototype.initialLoading).toHaveBeenCalled(); - expect(dummyCollection.isPersisted).toBeTruthy(); + await waitForExpect(() => { + expect(Persistent.prototype.initialLoading).toHaveBeenCalled(); + expect(dummyCollection.isPersisted).toBeTruthy(); + }); }); }); @@ -266,7 +277,7 @@ describe('CollectionPersistent Tests', () => { ); dummyCollection.assignItem = jest.fn(); - dummyAgile.storages.get = jest.fn(); + storageManager.get = jest.fn(); }); it( @@ -277,7 +288,7 @@ describe('CollectionPersistent Tests', () => { dummyCollection.data = { ['3']: dummyItem3, }; - dummyAgile.storages.get = jest + storageManager.get = jest .fn() .mockReturnValueOnce(Promise.resolve(true)); dummyDefaultGroup._value = ['3']; @@ -285,7 +296,7 @@ describe('CollectionPersistent Tests', () => { const response = await collectionPersistent.loadPersistedValue(); expect(response).toBeTruthy(); - expect(dummyAgile.storages.get).toHaveBeenCalledWith( + expect(storageManager.get).toHaveBeenCalledWith( collectionPersistent._key, collectionPersistent.config.defaultStorageKey ); @@ -338,7 +349,7 @@ describe('CollectionPersistent Tests', () => { collectionPersistent.ready = true; dummyCollection.data = {}; dummyCollection.size = 0; - dummyAgile.storages.get = jest + storageManager.get = jest .fn() .mockReturnValueOnce(Promise.resolve(true)); placeholderItem1.persist = jest.fn(function () { @@ -373,7 +384,7 @@ describe('CollectionPersistent Tests', () => { const response = await collectionPersistent.loadPersistedValue(); expect(response).toBeTruthy(); - expect(dummyAgile.storages.get).toHaveBeenCalledWith( + expect(storageManager.get).toHaveBeenCalledWith( collectionPersistent._key, collectionPersistent.config.defaultStorageKey ); @@ -417,9 +428,10 @@ describe('CollectionPersistent Tests', () => { expect( placeholderItem1?.persistent?.loadPersistedValue ).toHaveBeenCalledTimes(1); - expect( - dummyCollection.assignItem - ).toHaveBeenCalledWith(placeholderItem1, { overwrite: true }); + expect(dummyCollection.assignItem).toHaveBeenCalledWith( + placeholderItem1, + { overwrite: true } + ); expect(placeholderItem1.isPersisted).toBeTruthy(); // Placeholder Item 2 @@ -486,7 +498,7 @@ describe('CollectionPersistent Tests', () => { ['3']: dummyItem3, }; dummyCollection.size = 1; - dummyAgile.storages.get = jest + storageManager.get = jest .fn() .mockReturnValueOnce(Promise.resolve(true)); placeholderItem1.persist = jest.fn(function () { @@ -507,7 +519,7 @@ describe('CollectionPersistent Tests', () => { ); expect(response).toBeTruthy(); - expect(dummyAgile.storages.get).toHaveBeenCalledWith( + expect(storageManager.get).toHaveBeenCalledWith( 'dummyKey', collectionPersistent.config.defaultStorageKey ); @@ -568,9 +580,10 @@ describe('CollectionPersistent Tests', () => { expect( placeholderItem1?.persistent?.loadPersistedValue ).toHaveBeenCalledTimes(1); - expect( - dummyCollection.assignItem - ).toHaveBeenCalledWith(placeholderItem1, { overwrite: true }); + expect(dummyCollection.assignItem).toHaveBeenCalledWith( + placeholderItem1, + { overwrite: true } + ); expect(placeholderItem1.isPersisted).toBeTruthy(); expect(collectionPersistent.setupSideEffects).toHaveBeenCalledWith( @@ -581,14 +594,14 @@ describe('CollectionPersistent Tests', () => { it("shouldn't load default Group and its Items if Collection flag isn't persisted", async () => { collectionPersistent.ready = true; - dummyAgile.storages.get = jest + storageManager.get = jest .fn() .mockReturnValueOnce(Promise.resolve(undefined)); const response = await collectionPersistent.loadPersistedValue(); expect(response).toBeFalsy(); - expect(dummyAgile.storages.get).toHaveBeenCalledWith( + expect(storageManager.get).toHaveBeenCalledWith( collectionPersistent._key, collectionPersistent.config.defaultStorageKey ); @@ -612,7 +625,7 @@ describe('CollectionPersistent Tests', () => { const response = await collectionPersistent.loadPersistedValue(); expect(response).toBeFalsy(); - expect(dummyAgile.storages.get).not.toHaveBeenCalled(); + expect(storageManager.get).not.toHaveBeenCalled(); expect(dummyCollection.getDefaultGroup).not.toHaveBeenCalled(); expect(dummyDefaultGroup.persist).not.toHaveBeenCalled(); @@ -629,7 +642,7 @@ describe('CollectionPersistent Tests', () => { it("shouldn't load default Group and its Items if Collection has no defaultGroup", async () => { collectionPersistent.ready = true; - dummyAgile.storages.get = jest + storageManager.get = jest .fn() .mockReturnValueOnce(Promise.resolve(true)); dummyCollection.getDefaultGroup = jest.fn(() => undefined); @@ -637,7 +650,7 @@ describe('CollectionPersistent Tests', () => { const response = await collectionPersistent.loadPersistedValue(); expect(response).toBeFalsy(); - expect(dummyAgile.storages.get).toHaveBeenCalledWith( + expect(storageManager.get).toHaveBeenCalledWith( collectionPersistent._key, collectionPersistent.config.defaultStorageKey ); @@ -682,7 +695,7 @@ describe('CollectionPersistent Tests', () => { () => dummyDefaultGroup as any ); - dummyAgile.storages.set = jest.fn(); + storageManager.set = jest.fn(); }); it('should persist default Group and its Items (persistentKey)', async () => { @@ -691,7 +704,7 @@ describe('CollectionPersistent Tests', () => { const response = await collectionPersistent.persistValue(); expect(response).toBeTruthy(); - expect(dummyAgile.storages.set).toHaveBeenCalledWith( + expect(storageManager.set).toHaveBeenCalledWith( collectionPersistent._key, true, collectionPersistent.storageKeys @@ -743,7 +756,7 @@ describe('CollectionPersistent Tests', () => { const response = await collectionPersistent.persistValue('dummyKey'); expect(response).toBeTruthy(); - expect(dummyAgile.storages.set).toHaveBeenCalledWith( + expect(storageManager.set).toHaveBeenCalledWith( 'dummyKey', true, collectionPersistent.storageKeys @@ -789,7 +802,7 @@ describe('CollectionPersistent Tests', () => { const response = await collectionPersistent.persistValue('dummyKey'); expect(response).toBeFalsy(); - expect(dummyAgile.storages.set).not.toHaveBeenCalled(); + expect(storageManager.set).not.toHaveBeenCalled(); expect(dummyCollection.getDefaultGroup).not.toHaveBeenCalled(); expect(dummyDefaultGroup.persist).not.toHaveBeenCalled(); @@ -808,7 +821,7 @@ describe('CollectionPersistent Tests', () => { const response = await collectionPersistent.persistValue(); expect(response).toBeFalsy(); - expect(dummyAgile.storages.set).not.toHaveBeenCalled(); + expect(storageManager.set).not.toHaveBeenCalled(); expect(dummyCollection.getDefaultGroup).toHaveBeenCalled(); expect(dummyDefaultGroup.persist).not.toHaveBeenCalled(); @@ -840,9 +853,7 @@ 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 } @@ -919,7 +930,7 @@ describe('CollectionPersistent Tests', () => { if (dummyItem3.persistent) dummyItem3.persistent.removePersistedValue = jest.fn(); - dummyAgile.storages.remove = jest.fn(); + storageManager.remove = jest.fn(); }); it('should remove persisted default Group and its Items from Storage (persistentKey)', async () => { @@ -928,7 +939,7 @@ describe('CollectionPersistent Tests', () => { const response = await collectionPersistent.removePersistedValue(); expect(response).toBeTruthy(); - expect(dummyAgile.storages.remove).toHaveBeenCalledWith( + expect(storageManager.remove).toHaveBeenCalledWith( collectionPersistent._key, collectionPersistent.storageKeys ); @@ -974,7 +985,7 @@ describe('CollectionPersistent Tests', () => { ); expect(response).toBeTruthy(); - expect(dummyAgile.storages.remove).toHaveBeenCalledWith( + expect(storageManager.remove).toHaveBeenCalledWith( 'dummyKey', collectionPersistent.storageKeys ); @@ -1012,7 +1023,7 @@ describe('CollectionPersistent Tests', () => { const response = await collectionPersistent.removePersistedValue(); expect(response).toBeFalsy(); - expect(dummyAgile.storages.remove).not.toHaveBeenCalled(); + expect(storageManager.remove).not.toHaveBeenCalled(); expect(dummyCollection.getDefaultGroup).not.toHaveBeenCalled(); expect( @@ -1037,7 +1048,7 @@ describe('CollectionPersistent Tests', () => { const response = await collectionPersistent.removePersistedValue(); expect(response).toBeFalsy(); - expect(dummyAgile.storages.remove).not.toHaveBeenCalled(); + expect(storageManager.remove).not.toHaveBeenCalled(); expect(dummyCollection.getDefaultGroup).toHaveBeenCalled(); expect( diff --git a/packages/core/tests/unit/collection/collection.test.ts b/packages/core/tests/unit/collection/collection.test.ts index 086079f4..86a019a1 100644 --- a/packages/core/tests/unit/collection/collection.test.ts +++ b/packages/core/tests/unit/collection/collection.test.ts @@ -24,7 +24,7 @@ describe('Collection Tests', () => { beforeEach(() => { LogMock.mockLogs(); - dummyAgile = new Agile({ localStorage: false }); + dummyAgile = new Agile(); jest.spyOn(Collection.prototype, 'initSelectors'); jest.spyOn(Collection.prototype, 'initGroups'); @@ -264,10 +264,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); @@ -1286,6 +1286,24 @@ describe('Collection Tests', () => { }); }); + describe('select function tests', () => { + beforeEach(() => { + collection.createSelector = jest.fn(); + }); + it( + 'should call createSelector with the specified itemKey ' + + 'as key of the Selector and as selected item key', + () => { + collection.select('test'); + + expect(collection.createSelector).toHaveBeenCalledWith( + 'test', + 'test' + ); + } + ); + }); + describe('hasSelector function tests', () => { let dummySelector: Selector; 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 a295262e..488147b1 100644 --- a/packages/core/tests/unit/collection/group/group.observer.test.ts +++ b/packages/core/tests/unit/collection/group/group.observer.test.ts @@ -26,7 +26,7 @@ describe('GroupObserver Tests', () => { beforeEach(() => { LogMock.mockLogs(); - dummyAgile = new Agile({ localStorage: false }); + dummyAgile = new Agile(); dummyCollection = new Collection(dummyAgile); dummyGroup = new Group(dummyCollection, [], { key: 'dummyGroup', diff --git a/packages/core/tests/unit/collection/group/group.test.ts b/packages/core/tests/unit/collection/group/group.test.ts index 2a7525f1..17574a02 100644 --- a/packages/core/tests/unit/collection/group/group.test.ts +++ b/packages/core/tests/unit/collection/group/group.test.ts @@ -5,9 +5,9 @@ import { StateObserver, ComputedTracker, Item, - State, CollectionPersistent, GroupObserver, + EnhancedState, } from '../../../../src'; import { LogMock } from '../../../helper/logMock'; @@ -23,7 +23,7 @@ describe('Group Tests', () => { beforeEach(() => { LogMock.mockLogs(); - dummyAgile = new Agile({ localStorage: false }); + dummyAgile = new Agile(); dummyCollection = new Collection(dummyAgile, { key: 'dummyCollection', }); @@ -407,13 +407,13 @@ describe('Group Tests', () => { describe('persist function tests', () => { beforeEach(() => { - jest.spyOn(State.prototype, 'persist'); + jest.spyOn(EnhancedState.prototype, 'persist'); }); it('should persist Group with formatted groupKey (default config)', () => { group.persist(); - expect(State.prototype.persist).toHaveBeenCalledWith( + expect(EnhancedState.prototype.persist).toHaveBeenCalledWith( CollectionPersistent.getGroupStorageKey( group._key, dummyCollection._key @@ -433,7 +433,7 @@ describe('Group Tests', () => { defaultStorageKey: 'test1', }); - expect(State.prototype.persist).toHaveBeenCalledWith( + expect(EnhancedState.prototype.persist).toHaveBeenCalledWith( CollectionPersistent.getGroupStorageKey( group._key, dummyCollection._key @@ -449,7 +449,7 @@ describe('Group Tests', () => { it('should persist Group with formatted specified key (default config)', () => { group.persist('dummyKey'); - expect(State.prototype.persist).toHaveBeenCalledWith( + expect(EnhancedState.prototype.persist).toHaveBeenCalledWith( CollectionPersistent.getGroupStorageKey( 'dummyKey', dummyCollection._key @@ -469,7 +469,7 @@ describe('Group Tests', () => { defaultStorageKey: 'test1', }); - expect(State.prototype.persist).toHaveBeenCalledWith( + expect(EnhancedState.prototype.persist).toHaveBeenCalledWith( CollectionPersistent.getGroupStorageKey( 'dummyKey', dummyCollection._key @@ -485,21 +485,27 @@ describe('Group Tests', () => { it('should persist Group with groupKey (config.followCollectionPersistKeyPattern = false)', () => { group.persist({ followCollectionPersistKeyPattern: false }); - expect(State.prototype.persist).toHaveBeenCalledWith(group._key, { - loadValue: true, - storageKeys: [], - defaultStorageKey: null, - }); + expect(EnhancedState.prototype.persist).toHaveBeenCalledWith( + group._key, + { + loadValue: true, + storageKeys: [], + defaultStorageKey: null, + } + ); }); it('should persist Group with specified key (config.followCollectionPersistKeyPattern = false)', () => { group.persist('dummyKey', { followCollectionPersistKeyPattern: false }); - expect(State.prototype.persist).toHaveBeenCalledWith('dummyKey', { - loadValue: true, - storageKeys: [], - defaultStorageKey: null, - }); + expect(EnhancedState.prototype.persist).toHaveBeenCalledWith( + 'dummyKey', + { + loadValue: true, + storageKeys: [], + defaultStorageKey: null, + } + ); }); }); diff --git a/packages/core/tests/unit/collection/index.test.ts b/packages/core/tests/unit/collection/index.test.ts new file mode 100644 index 00000000..50aa234a --- /dev/null +++ b/packages/core/tests/unit/collection/index.test.ts @@ -0,0 +1,62 @@ +import { + Agile, + assignSharedAgileInstance, + Collection, + createCollection, +} from '../../../src'; +import { LogMock } from '../../helper/logMock'; + +jest.mock('../../../src/collection/collection'); + +describe('Collection Index', () => { + let sharedAgileInstance: Agile; + + beforeEach(() => { + LogMock.mockLogs(); + + sharedAgileInstance = new Agile(); + assignSharedAgileInstance(sharedAgileInstance); + + jest.clearAllMocks(); + }); + + describe('createCollection function tests', () => { + const CollectionMock = Collection as jest.MockedClass; + + beforeEach(() => { + CollectionMock.mockClear(); + }); + + it('should create Collection with the shared Agile Instance', () => { + const collectionConfig = { + selectors: ['test', 'test1'], + groups: ['test2', 'test10'], + defaultGroupKey: 'frank', + key: 'myCoolCollection', + }; + + const collection = createCollection(collectionConfig); + + expect(collection).toBeInstanceOf(Collection); + expect(CollectionMock).toHaveBeenCalledWith( + sharedAgileInstance, + collectionConfig + ); + }); + + it('should create Collection with a specified Agile Instance', () => { + const agile = new Agile(); + const collectionConfig = { + selectors: ['test', 'test1'], + groups: ['test2', 'test10'], + defaultGroupKey: 'frank', + key: 'myCoolCollection', + }; + + const collection = createCollection(collectionConfig, agile); + + expect(collection).toBeInstanceOf(Collection); + expect(CollectionMock).toHaveBeenCalledWith(agile, collectionConfig); + }); + }); +}); diff --git a/packages/core/tests/unit/collection/item.test.ts b/packages/core/tests/unit/collection/item.test.ts index 38538da9..b5fdeb4f 100644 --- a/packages/core/tests/unit/collection/item.test.ts +++ b/packages/core/tests/unit/collection/item.test.ts @@ -3,7 +3,7 @@ import { Collection, Agile, StateObserver, - State, + EnhancedState, CollectionPersistent, } from '../../../src'; import { LogMock } from '../../helper/logMock'; @@ -20,7 +20,7 @@ describe('Item Tests', () => { beforeEach(() => { LogMock.mockLogs(); - dummyAgile = new Agile({ localStorage: false }); + dummyAgile = new Agile(); dummyCollection = new Collection(dummyAgile); jest.spyOn(Item.prototype, 'addRebuildGroupThatIncludeItemKeySideEffect'); @@ -145,13 +145,13 @@ describe('Item Tests', () => { beforeEach(() => { item.removeSideEffect = jest.fn(); item.patch = jest.fn(); - jest.spyOn(State.prototype, 'setKey'); + jest.spyOn(EnhancedState.prototype, 'setKey'); }); it('should call State setKey, add rebuildGroup sideEffect to Item and patch newItemKey into Item (default config)', () => { item.setKey('myNewKey'); - expect(State.prototype.setKey).toHaveBeenCalledWith('myNewKey'); + expect(EnhancedState.prototype.setKey).toHaveBeenCalledWith('myNewKey'); expect(item.removeSideEffect).toHaveBeenCalledWith( Item.updateGroupSideEffectKey ); @@ -184,7 +184,7 @@ describe('Item Tests', () => { force: true, }); - expect(State.prototype.setKey).toHaveBeenCalledWith('myNewKey'); + expect(EnhancedState.prototype.setKey).toHaveBeenCalledWith('myNewKey'); expect(item.removeSideEffect).toHaveBeenCalledWith( Item.updateGroupSideEffectKey ); @@ -210,13 +210,13 @@ describe('Item Tests', () => { describe('persist function tests', () => { beforeEach(() => { - jest.spyOn(State.prototype, 'persist'); + jest.spyOn(EnhancedState.prototype, 'persist'); }); it('should persist Item with formatted itemKey (default config)', () => { item.persist(); - expect(State.prototype.persist).toHaveBeenCalledWith( + expect(EnhancedState.prototype.persist).toHaveBeenCalledWith( CollectionPersistent.getItemStorageKey( item._key, dummyCollection._key @@ -236,7 +236,7 @@ describe('Item Tests', () => { defaultStorageKey: 'test1', }); - expect(State.prototype.persist).toHaveBeenCalledWith( + expect(EnhancedState.prototype.persist).toHaveBeenCalledWith( CollectionPersistent.getItemStorageKey( item._key, dummyCollection._key @@ -252,7 +252,7 @@ describe('Item Tests', () => { it('should persist Item with formatted specified key (default config)', () => { item.persist('dummyKey'); - expect(State.prototype.persist).toHaveBeenCalledWith( + expect(EnhancedState.prototype.persist).toHaveBeenCalledWith( CollectionPersistent.getItemStorageKey( 'dummyKey', dummyCollection._key @@ -272,7 +272,7 @@ describe('Item Tests', () => { defaultStorageKey: 'test1', }); - expect(State.prototype.persist).toHaveBeenCalledWith( + expect(EnhancedState.prototype.persist).toHaveBeenCalledWith( CollectionPersistent.getItemStorageKey( 'dummyKey', dummyCollection._key @@ -288,21 +288,27 @@ describe('Item Tests', () => { it('should persist Item with itemKey (config.followCollectionPersistKeyPattern = false)', () => { item.persist({ followCollectionPersistKeyPattern: false }); - expect(State.prototype.persist).toHaveBeenCalledWith(item._key, { - loadValue: true, - storageKeys: [], - defaultStorageKey: null, - }); + expect(EnhancedState.prototype.persist).toHaveBeenCalledWith( + item._key, + { + loadValue: true, + storageKeys: [], + defaultStorageKey: null, + } + ); }); it('should persist Item with specified key (config.followCollectionPersistKeyPattern = false)', () => { item.persist('dummyKey', { followCollectionPersistKeyPattern: false }); - expect(State.prototype.persist).toHaveBeenCalledWith('dummyKey', { - loadValue: true, - storageKeys: [], - defaultStorageKey: null, - }); + expect(EnhancedState.prototype.persist).toHaveBeenCalledWith( + 'dummyKey', + { + loadValue: true, + storageKeys: [], + defaultStorageKey: null, + } + ); }); }); @@ -315,9 +321,7 @@ describe('Item Tests', () => { it('should add rebuildGroupThatIncludeItemKey sideEffect to Item', () => { item.addRebuildGroupThatIncludeItemKeySideEffect('itemKey'); - expect( - item.addSideEffect - ).toHaveBeenCalledWith( + expect(item.addSideEffect).toHaveBeenCalledWith( Item.updateGroupSideEffectKey, expect.any(Function), { weight: 100 } diff --git a/packages/core/tests/unit/collection/selector.test.ts b/packages/core/tests/unit/collection/selector.test.ts index 68378e32..45ef999f 100644 --- a/packages/core/tests/unit/collection/selector.test.ts +++ b/packages/core/tests/unit/collection/selector.test.ts @@ -13,7 +13,7 @@ describe('Selector Tests', () => { beforeEach(() => { LogMock.mockLogs(); - dummyAgile = new Agile({ localStorage: false }); + dummyAgile = new Agile(); dummyCollection = new Collection(dummyAgile); jest.spyOn(Selector.prototype, 'select'); @@ -258,17 +258,13 @@ describe('Selector Tests', () => { overwrite: false, storage: true, }); - expect( - selector.addSideEffect - ).toHaveBeenCalledWith( + expect(selector.addSideEffect).toHaveBeenCalledWith( Selector.rebuildItemSideEffectKey, expect.any(Function), { weight: 90 } ); - expect( - dummyItem2.addSideEffect - ).toHaveBeenCalledWith( + expect(dummyItem2.addSideEffect).toHaveBeenCalledWith( Selector.rebuildSelectorSideEffectKey, expect.any(Function), { weight: 100 } @@ -306,17 +302,13 @@ describe('Selector Tests', () => { overwrite: true, storage: true, }); - expect( - selector.addSideEffect - ).toHaveBeenCalledWith( + expect(selector.addSideEffect).toHaveBeenCalledWith( Selector.rebuildItemSideEffectKey, expect.any(Function), { weight: 90 } ); - expect( - dummyItem2.addSideEffect - ).toHaveBeenCalledWith( + expect(dummyItem2.addSideEffect).toHaveBeenCalledWith( Selector.rebuildSelectorSideEffectKey, expect.any(Function), { weight: 100 } @@ -367,17 +359,13 @@ describe('Selector Tests', () => { overwrite: false, storage: true, }); - expect( - selector.addSideEffect - ).toHaveBeenCalledWith( + expect(selector.addSideEffect).toHaveBeenCalledWith( Selector.rebuildItemSideEffectKey, expect.any(Function), { weight: 90 } ); - expect( - dummyItem1.addSideEffect - ).toHaveBeenCalledWith( + expect(dummyItem1.addSideEffect).toHaveBeenCalledWith( Selector.rebuildSelectorSideEffectKey, expect.any(Function), { weight: 100 } @@ -428,17 +416,13 @@ describe('Selector Tests', () => { overwrite: false, storage: true, }); - expect( - selector.addSideEffect - ).toHaveBeenCalledWith( + expect(selector.addSideEffect).toHaveBeenCalledWith( Selector.rebuildItemSideEffectKey, expect.any(Function), { weight: 90 } ); - expect( - dummyItem2.addSideEffect - ).toHaveBeenCalledWith( + expect(dummyItem2.addSideEffect).toHaveBeenCalledWith( Selector.rebuildSelectorSideEffectKey, expect.any(Function), { weight: 100 } @@ -470,17 +454,13 @@ describe('Selector Tests', () => { overwrite: true, storage: true, }); - expect( - selector.addSideEffect - ).toHaveBeenCalledWith( + expect(selector.addSideEffect).toHaveBeenCalledWith( Selector.rebuildItemSideEffectKey, expect.any(Function), { weight: 90 } ); - expect( - dummyItem2.addSideEffect - ).toHaveBeenCalledWith( + expect(dummyItem2.addSideEffect).toHaveBeenCalledWith( Selector.rebuildSelectorSideEffectKey, expect.any(Function), { weight: 100 } @@ -512,17 +492,13 @@ describe('Selector Tests', () => { overwrite: false, storage: true, }); - expect( - selector.addSideEffect - ).toHaveBeenCalledWith( + expect(selector.addSideEffect).toHaveBeenCalledWith( Selector.rebuildItemSideEffectKey, expect.any(Function), { weight: 90 } ); - expect( - dummyItem2.addSideEffect - ).toHaveBeenCalledWith( + expect(dummyItem2.addSideEffect).toHaveBeenCalledWith( Selector.rebuildSelectorSideEffectKey, expect.any(Function), { weight: 100 } diff --git a/packages/core/tests/unit/computed/computed.test.ts b/packages/core/tests/unit/computed/computed.test.ts index 160ddd2c..59b1d3ed 100644 --- a/packages/core/tests/unit/computed/computed.test.ts +++ b/packages/core/tests/unit/computed/computed.test.ts @@ -16,7 +16,7 @@ describe('Computed Tests', () => { beforeEach(() => { LogMock.mockLogs(); - dummyAgile = new Agile({ localStorage: false }); + dummyAgile = new Agile(); jest.spyOn(Computed.prototype, 'recompute'); jest.spyOn(Utils, 'extractRelevantObservers'); @@ -55,10 +55,6 @@ describe('Computed Tests', () => { ); expect(computed.observers['value']._key).toBeUndefined(); expect(computed.sideEffects).toStrictEqual({}); - expect(computed.computeValueMethod).toBeUndefined(); - expect(computed.computeExistsMethod).toBeInstanceOf(Function); - expect(computed.isPersisted).toBeFalsy(); - expect(computed.persistent).toBeUndefined(); }); it('should create Computed with a not async compute method (specific config)', () => { @@ -123,10 +119,6 @@ describe('Computed Tests', () => { ]); expect(computed.observers['value']._key).toBe('coolComputed'); expect(computed.sideEffects).toStrictEqual({}); - expect(computed.computeValueMethod).toBeUndefined(); - expect(computed.computeExistsMethod).toBeInstanceOf(Function); - expect(computed.isPersisted).toBeFalsy(); - expect(computed.persistent).toBeUndefined(); }); it('should create Computed with an async compute method (default config)', () => { @@ -160,10 +152,6 @@ describe('Computed Tests', () => { ); expect(computed.observers['value']._key).toBeUndefined(); expect(computed.sideEffects).toStrictEqual({}); - expect(computed.computeValueMethod).toBeUndefined(); - expect(computed.computeExistsMethod).toBeInstanceOf(Function); - expect(computed.isPersisted).toBeFalsy(); - expect(computed.persistent).toBeUndefined(); }); describe('Computed Function Tests', () => { diff --git a/packages/core/tests/unit/computed/computed.tracker.test.ts b/packages/core/tests/unit/computed/computed.tracker.test.ts index 20e09dc1..97f970ce 100644 --- a/packages/core/tests/unit/computed/computed.tracker.test.ts +++ b/packages/core/tests/unit/computed/computed.tracker.test.ts @@ -7,7 +7,7 @@ describe('ComputedTracker Tests', () => { beforeEach(() => { LogMock.mockLogs(); - dummyAgile = new Agile({ localStorage: false }); + dummyAgile = new Agile(); // Reset ComputedTracker (because it works static) ComputedTracker.isTracking = false; diff --git a/packages/core/tests/unit/computed/index.test.ts b/packages/core/tests/unit/computed/index.test.ts new file mode 100644 index 00000000..262ba1c0 --- /dev/null +++ b/packages/core/tests/unit/computed/index.test.ts @@ -0,0 +1,86 @@ +import { + Agile, + assignSharedAgileInstance, + Computed, + createComputed, +} from '../../../src'; +import { LogMock } from '../../helper/logMock'; + +jest.mock('../../../src/computed/computed'); + +describe('Computed Index', () => { + let sharedAgileInstance: Agile; + + beforeEach(() => { + LogMock.mockLogs(); + + sharedAgileInstance = new Agile(); + assignSharedAgileInstance(sharedAgileInstance); + + jest.clearAllMocks(); + }); + + describe('createComputed function tests', () => { + const ComputedMock = Computed as jest.MockedClass; + const computedFunction = () => { + // empty + }; + + beforeEach(() => { + ComputedMock.mockClear(); + }); + + it('should create Computed with the shared Agile Instance (default config)', () => { + const response = createComputed(computedFunction, ['dummyDep' as any]); + + expect(response).toBeInstanceOf(Computed); + expect(ComputedMock).toHaveBeenCalledWith( + sharedAgileInstance, + computedFunction, + { + computedDeps: ['dummyDep' as any], + } + ); + }); + + it('should create Computed with the shared Agile Instance (specific config)', () => { + const computedConfig = { + key: 'jeff', + isPlaceholder: false, + computedDeps: ['dummyDep' as any], + autodetect: true, + }; + + const response = createComputed(computedFunction, computedConfig); + + expect(response).toBeInstanceOf(Computed); + expect(ComputedMock).toHaveBeenCalledWith( + sharedAgileInstance, + computedFunction, + computedConfig + ); + }); + + it('should create Computed with a specified Agile Instance (specific config)', () => { + const agile = new Agile(); + const computedConfig = { + key: 'jeff', + isPlaceholder: false, + computedDeps: ['dummyDep' as any], + autodetect: true, + }; + + const response = createComputed(computedFunction, { + ...computedConfig, + ...{ agileInstance: agile }, + }); + + expect(response).toBeInstanceOf(Computed); + expect(ComputedMock).toHaveBeenCalledWith( + agile, + computedFunction, + computedConfig + ); + }); + }); +}); diff --git a/packages/core/tests/unit/integrations/integrations.test.ts b/packages/core/tests/unit/integrations/integrations.test.ts index a7598d61..845fff56 100644 --- a/packages/core/tests/unit/integrations/integrations.test.ts +++ b/packages/core/tests/unit/integrations/integrations.test.ts @@ -9,7 +9,7 @@ describe('Integrations Tests', () => { beforeEach(() => { LogMock.mockLogs(); - dummyAgile = new Agile({ localStorage: false }); + dummyAgile = new Agile(); dummyIntegration1 = new Integration({ key: 'dummyIntegration1', }); diff --git a/packages/core/tests/unit/runtime/runtime.job.test.ts b/packages/core/tests/unit/runtime/runtime.job.test.ts index b01c3765..9d0845f2 100644 --- a/packages/core/tests/unit/runtime/runtime.job.test.ts +++ b/packages/core/tests/unit/runtime/runtime.job.test.ts @@ -9,7 +9,7 @@ describe('RuntimeJob Tests', () => { beforeEach(() => { LogMock.mockLogs(); - dummyAgile = new Agile({ localStorage: false }); + dummyAgile = new Agile(); dummyIntegration = new Integration({ key: 'myIntegration', }); diff --git a/packages/core/tests/unit/runtime/runtime.test.ts b/packages/core/tests/unit/runtime/runtime.test.ts index 5bcc3014..9601a7cc 100644 --- a/packages/core/tests/unit/runtime/runtime.test.ts +++ b/packages/core/tests/unit/runtime/runtime.test.ts @@ -17,7 +17,7 @@ describe('Runtime Tests', () => { beforeEach(() => { LogMock.mockLogs(); - dummyAgile = new Agile({ localStorage: false }); + dummyAgile = new Agile(); jest.clearAllMocks(); }); diff --git a/packages/core/tests/unit/runtime/subscription/sub.controller.test.ts b/packages/core/tests/unit/runtime/subscription/sub.controller.test.ts index 6d73d851..72063f9d 100644 --- a/packages/core/tests/unit/runtime/subscription/sub.controller.test.ts +++ b/packages/core/tests/unit/runtime/subscription/sub.controller.test.ts @@ -14,7 +14,7 @@ describe('SubController Tests', () => { beforeEach(() => { LogMock.mockLogs(); - dummyAgile = new Agile({ localStorage: false }); + dummyAgile = new Agile(); jest.clearAllMocks(); }); @@ -215,10 +215,11 @@ describe('SubController Tests', () => { const dummyIntegration = () => { /* empty function */ }; - const callbackSubscriptionContainer = subController.createCallbackSubscriptionContainer( - dummyIntegration, - [dummyObserver1, dummyObserver2] - ); + const callbackSubscriptionContainer = + subController.createCallbackSubscriptionContainer(dummyIntegration, [ + dummyObserver1, + dummyObserver2, + ]); callbackSubscriptionContainer.removeSubscription = jest.fn(); subController.unsubscribe(callbackSubscriptionContainer); @@ -240,10 +241,11 @@ describe('SubController Tests', () => { const dummyIntegration: any = { dummy: 'integration', }; - const componentSubscriptionContainer = subController.createComponentSubscriptionContainer( - dummyIntegration, - [dummyObserver1, dummyObserver2] - ); + const componentSubscriptionContainer = + subController.createComponentSubscriptionContainer(dummyIntegration, [ + dummyObserver1, + dummyObserver2, + ]); componentSubscriptionContainer.removeSubscription = jest.fn(); subController.unsubscribe(componentSubscriptionContainer); @@ -269,15 +271,17 @@ describe('SubController Tests', () => { dummy: 'integration', componentSubscriptionContainers: [], }; - const componentSubscriptionContainer = subController.createComponentSubscriptionContainer( - dummyIntegration, - [dummyObserver1, dummyObserver2] - ); + const componentSubscriptionContainer = + subController.createComponentSubscriptionContainer( + dummyIntegration, + [dummyObserver1, dummyObserver2] + ); componentSubscriptionContainer.removeSubscription = jest.fn(); - const componentSubscriptionContainer2 = subController.createComponentSubscriptionContainer( - dummyIntegration, - [dummyObserver1, dummyObserver2] - ); + const componentSubscriptionContainer2 = + subController.createComponentSubscriptionContainer( + dummyIntegration, + [dummyObserver1, dummyObserver2] + ); componentSubscriptionContainer2.removeSubscription = jest.fn(); subController.unsubscribe(dummyIntegration); @@ -320,11 +324,12 @@ describe('SubController Tests', () => { dummy: 'integration', }; - const componentSubscriptionContainer = subController.createComponentSubscriptionContainer( - dummyIntegration, - [dummyObserver1, dummyObserver2], - { waitForMount: false } - ); + const componentSubscriptionContainer = + subController.createComponentSubscriptionContainer( + dummyIntegration, + [dummyObserver1, dummyObserver2], + { waitForMount: false } + ); expect(componentSubscriptionContainer).toBeInstanceOf( ComponentSubscriptionContainer @@ -362,11 +367,12 @@ describe('SubController Tests', () => { componentSubscriptionContainers: [], }; - const componentSubscriptionContainer = subController.createComponentSubscriptionContainer( - dummyIntegration, - [dummyObserver1, dummyObserver2], - { waitForMount: false } - ); + const componentSubscriptionContainer = + subController.createComponentSubscriptionContainer( + dummyIntegration, + [dummyObserver1, dummyObserver2], + { waitForMount: false } + ); expect( dummyIntegration.componentSubscriptionContainers @@ -383,11 +389,12 @@ describe('SubController Tests', () => { dummy: 'integration', }; - const componentSubscriptionContainer = subController.createComponentSubscriptionContainer( - dummyIntegration, - [dummyObserver1, dummyObserver2], - { waitForMount: false, componentId: 'testID', key: 'dummyKey' } - ); + const componentSubscriptionContainer = + subController.createComponentSubscriptionContainer( + dummyIntegration, + [dummyObserver1, dummyObserver2], + { waitForMount: false, componentId: 'testID', key: 'dummyKey' } + ); expect(componentSubscriptionContainer).toBeInstanceOf( ComponentSubscriptionContainer @@ -420,11 +427,12 @@ describe('SubController Tests', () => { dummy: 'integration', }; - const componentSubscriptionContainer = subController.createComponentSubscriptionContainer( - dummyIntegration, - [dummyObserver1, dummyObserver2], - { waitForMount: true } - ); + const componentSubscriptionContainer = + subController.createComponentSubscriptionContainer( + dummyIntegration, + [dummyObserver1, dummyObserver2], + { waitForMount: true } + ); expect(componentSubscriptionContainer).toBeInstanceOf( ComponentSubscriptionContainer @@ -453,11 +461,12 @@ describe('SubController Tests', () => { }; subController.mount(dummyIntegration); - const componentSubscriptionContainer = subController.createComponentSubscriptionContainer( - dummyIntegration, - [dummyObserver1, dummyObserver2], - { waitForMount: true } - ); + const componentSubscriptionContainer = + subController.createComponentSubscriptionContainer( + dummyIntegration, + [dummyObserver1, dummyObserver2], + { waitForMount: true } + ); expect(componentSubscriptionContainer).toBeInstanceOf( ComponentSubscriptionContainer @@ -487,10 +496,11 @@ describe('SubController Tests', () => { /* empty function */ }; - const callbackSubscriptionContainer = subController.createCallbackSubscriptionContainer( - dummyIntegration, - [dummyObserver1, dummyObserver2] - ); + const callbackSubscriptionContainer = + subController.createCallbackSubscriptionContainer(dummyIntegration, [ + dummyObserver1, + dummyObserver2, + ]); expect(callbackSubscriptionContainer).toBeInstanceOf( CallbackSubscriptionContainer @@ -520,15 +530,16 @@ describe('SubController Tests', () => { /* empty function */ }; - const callbackSubscriptionContainer = subController.createCallbackSubscriptionContainer( - dummyIntegration, - [dummyObserver1, dummyObserver2], - { - waitForMount: false, - componentId: 'testID', - key: 'dummyKey', - } - ); + const callbackSubscriptionContainer = + subController.createCallbackSubscriptionContainer( + dummyIntegration, + [dummyObserver1, dummyObserver2], + { + waitForMount: false, + componentId: 'testID', + key: 'dummyKey', + } + ); expect(callbackSubscriptionContainer).toBeInstanceOf( CallbackSubscriptionContainer @@ -557,10 +568,11 @@ describe('SubController Tests', () => { beforeEach(() => { dummyAgile.config.waitForMount = true; - componentSubscriptionContainer = subController.createComponentSubscriptionContainer( - dummyIntegration, - [dummyObserver1, dummyObserver2] - ); + componentSubscriptionContainer = + subController.createComponentSubscriptionContainer(dummyIntegration, [ + dummyObserver1, + dummyObserver2, + ]); }); it( @@ -585,10 +597,11 @@ describe('SubController Tests', () => { beforeEach(() => { dummyAgile.config.waitForMount = true; - componentSubscriptionContainer = subController.createComponentSubscriptionContainer( - dummyIntegration, - [dummyObserver1, dummyObserver2] - ); + componentSubscriptionContainer = + subController.createComponentSubscriptionContainer(dummyIntegration, [ + dummyObserver1, + dummyObserver2, + ]); subController.mount(dummyIntegration); }); diff --git a/packages/core/tests/unit/shared.test.ts b/packages/core/tests/unit/shared.test.ts index e93d0d97..b8d5e048 100644 --- a/packages/core/tests/unit/shared.test.ts +++ b/packages/core/tests/unit/shared.test.ts @@ -3,27 +3,12 @@ import { Collection, Computed, shared, - State, - Storage, - createStorage, - createState, createCollection, createComputed, assignSharedAgileInstance, } from '../../src'; import { LogMock } from '../helper/logMock'; -jest.mock('../../src/storages/storage'); -jest.mock('../../src/collection/collection'); -jest.mock('../../src/computed/computed'); - -// https://github.com/facebook/jest/issues/5023 -jest.mock('../../src/state/state', () => { - return { - State: jest.fn(), - }; -}); - describe('Shared Tests', () => { let sharedAgileInstance: Agile; @@ -45,168 +30,4 @@ describe('Shared Tests', () => { expect(shared).toBe(newAgileInstance); }); }); - - describe('createStorage function tests', () => { - const StorageMock = Storage as jest.MockedClass; - - beforeEach(() => { - StorageMock.mockClear(); - }); - - it('should create Storage', () => { - const storageConfig = { - prefix: 'test', - methods: { - get: () => { - /* empty function */ - }, - set: () => { - /* empty function */ - }, - remove: () => { - /* empty function */ - }, - }, - key: 'myTestStorage', - }; - - const storage = createStorage(storageConfig); - - expect(storage).toBeInstanceOf(Storage); - expect(StorageMock).toHaveBeenCalledWith(storageConfig); - }); - }); - - describe('createState function tests', () => { - const StateMock = State as jest.MockedClass; - - it('should create State with the shared Agile Instance', () => { - const state = createState('testValue', { - key: 'myCoolState', - }); - - expect(state).toBeInstanceOf(State); - expect(StateMock).toHaveBeenCalledWith(sharedAgileInstance, 'testValue', { - key: 'myCoolState', - }); - }); - - it('should create State with a specified Agile Instance', () => { - const agile = new Agile(); - - const state = createState('testValue', { - key: 'myCoolState', - agileInstance: agile, - }); - - expect(state).toBeInstanceOf(State); - expect(StateMock).toHaveBeenCalledWith(agile, 'testValue', { - key: 'myCoolState', - }); - }); - }); - - describe('createCollection function tests', () => { - const CollectionMock = Collection as jest.MockedClass; - - beforeEach(() => { - CollectionMock.mockClear(); - }); - - it('should create Collection with the shared Agile Instance', () => { - const collectionConfig = { - selectors: ['test', 'test1'], - groups: ['test2', 'test10'], - defaultGroupKey: 'frank', - key: 'myCoolCollection', - }; - - const collection = createCollection(collectionConfig); - - expect(collection).toBeInstanceOf(Collection); - expect(CollectionMock).toHaveBeenCalledWith( - sharedAgileInstance, - collectionConfig - ); - }); - - it('should create Collection with a specified Agile Instance', () => { - const agile = new Agile(); - const collectionConfig = { - selectors: ['test', 'test1'], - groups: ['test2', 'test10'], - defaultGroupKey: 'frank', - key: 'myCoolCollection', - }; - - const collection = createCollection(collectionConfig, agile); - - expect(collection).toBeInstanceOf(Collection); - expect(CollectionMock).toHaveBeenCalledWith(agile, collectionConfig); - }); - }); - - describe('createComputed function tests', () => { - const ComputedMock = Computed as jest.MockedClass; - const computedFunction = () => { - // empty - }; - - beforeEach(() => { - ComputedMock.mockClear(); - }); - - it('should create Computed with the shared Agile Instance (default config)', () => { - const response = createComputed(computedFunction, ['dummyDep' as any]); - - expect(response).toBeInstanceOf(Computed); - expect(ComputedMock).toHaveBeenCalledWith( - sharedAgileInstance, - computedFunction, - { - computedDeps: ['dummyDep' as any], - } - ); - }); - - it('should create Computed with the shared Agile Instance (specific config)', () => { - const computedConfig = { - key: 'jeff', - isPlaceholder: false, - computedDeps: ['dummyDep' as any], - autodetect: true, - }; - - const response = createComputed(computedFunction, computedConfig); - - expect(response).toBeInstanceOf(Computed); - expect(ComputedMock).toHaveBeenCalledWith( - sharedAgileInstance, - computedFunction, - computedConfig - ); - }); - - it('should create Computed with a specified Agile Instance (specific config)', () => { - const agile = new Agile(); - const computedConfig = { - key: 'jeff', - isPlaceholder: false, - computedDeps: ['dummyDep' as any], - autodetect: true, - }; - - const response = createComputed(computedFunction, { - ...computedConfig, - ...{ agileInstance: agile }, - }); - - expect(response).toBeInstanceOf(Computed); - expect(ComputedMock).toHaveBeenCalledWith( - agile, - computedFunction, - computedConfig - ); - }); - }); }); diff --git a/packages/core/tests/unit/state/index.test.ts b/packages/core/tests/unit/state/index.test.ts new file mode 100644 index 00000000..7899b192 --- /dev/null +++ b/packages/core/tests/unit/state/index.test.ts @@ -0,0 +1,108 @@ +// https://github.com/facebook/jest/issues/5023 +import { + Agile, + assignSharedAgileInstance, + createState, + createLightState, + State, + EnhancedState, +} from '../../../src'; +import { LogMock } from '../../helper/logMock'; + +// https://github.com/facebook/jest/issues/5023 +jest.mock('../../../src/state/state', () => { + return { + State: jest.fn(), + }; +}); +// https://github.com/facebook/jest/issues/5023 +jest.mock('../../../src/state/state.enhanced', () => { + return { + EnhancedState: jest.fn(), + }; +}); + +describe('State Index', () => { + let sharedAgileInstance: Agile; + + beforeEach(() => { + LogMock.mockLogs(); + + sharedAgileInstance = new Agile(); + assignSharedAgileInstance(sharedAgileInstance); + + jest.clearAllMocks(); + }); + + describe('createState function tests', () => { + const EnhancedStateMock = EnhancedState as jest.MockedClass< + typeof EnhancedState + >; + + beforeEach(() => { + EnhancedStateMock.mockClear(); + }); + + it('should create enhanced State with the shared Agile Instance', () => { + const state = createState('testValue', { + key: 'myCoolState', + }); + + // expect(state).toBeInstanceOf(EnhancedState); // Because 'State' is completely overwritten with a mock (mockImplementation) + expect(EnhancedStateMock).toHaveBeenCalledWith( + sharedAgileInstance, + 'testValue', + { + key: 'myCoolState', + } + ); + }); + + it('should create enhanced State with a specified Agile Instance', () => { + const agile = new Agile(); + + const state = createState('testValue', { + key: 'myCoolState', + agileInstance: agile, + }); + + // expect(state).toBeInstanceOf(EnhancedState); // Because 'State' is completely overwritten with a mock (mockImplementation) + expect(EnhancedStateMock).toHaveBeenCalledWith(agile, 'testValue', { + key: 'myCoolState', + }); + }); + }); + + describe('createLightState function tests', () => { + const StateMock = State as jest.MockedClass; + + beforeEach(() => { + StateMock.mockClear(); + }); + + it('should create State with the shared Agile Instance', () => { + const state = createLightState('testValue', { + key: 'myCoolState', + }); + + // expect(state).toBeInstanceOf(State); // Because 'State' is completely overwritten with a mock (mockImplementation) + expect(StateMock).toHaveBeenCalledWith(sharedAgileInstance, 'testValue', { + key: 'myCoolState', + }); + }); + + it('should create State with a specified Agile Instance', () => { + const agile = new Agile(); + + const state = createLightState('testValue', { + key: 'myCoolState', + agileInstance: agile, + }); + + // expect(state).toBeInstanceOf(State); // Because 'State' is completely overwritten with a mock (mockImplementation) + expect(StateMock).toHaveBeenCalledWith(agile, 'testValue', { + key: 'myCoolState', + }); + }); + }); +}); diff --git a/packages/core/tests/unit/state/state.enhanced.test.ts b/packages/core/tests/unit/state/state.enhanced.test.ts new file mode 100644 index 00000000..7c80127c --- /dev/null +++ b/packages/core/tests/unit/state/state.enhanced.test.ts @@ -0,0 +1,811 @@ +import { + State, + Agile, + StateObserver, + Observer, + StatePersistent, + EnhancedState, +} from '../../../src'; +import * as Utils from '@agile-ts/utils'; +import { LogMock } from '../../helper/logMock'; + +jest.mock('../../../src/state/state.persistent'); + +describe('Enhanced State Tests', () => { + let dummyAgile: Agile; + + beforeEach(() => { + LogMock.mockLogs(); + + dummyAgile = new Agile(); + + jest.spyOn(State.prototype, 'set'); + + jest.clearAllMocks(); + }); + + it('should create Enhanced State and should call initial set (default config)', () => { + // Overwrite select once to not call it + jest + .spyOn(EnhancedState.prototype, 'set') + .mockReturnValueOnce(undefined as any); + + const state = new EnhancedState(dummyAgile, 'coolValue'); + + expect(state.isPersisted).toBeFalsy(); + expect(state.persistent).toBeUndefined(); + expect(state.computeValueMethod).toBeUndefined(); + expect(state.computeExistsMethod).toBeInstanceOf(Function); + expect(state.currentInterval).toBeUndefined(); + + // Check if State was called with correct parameters + expect(state._key).toBeUndefined(); + expect(state.isSet).toBeFalsy(); + expect(state.isPlaceholder).toBeTruthy(); + expect(state.initialStateValue).toBe('coolValue'); + expect(state._value).toBe('coolValue'); + expect(state.previousStateValue).toBe('coolValue'); + expect(state.nextStateValue).toBe('coolValue'); + expect(state.observers['value']).toBeInstanceOf(StateObserver); + expect(Array.from(state.observers['value'].dependents)).toStrictEqual([]); + expect(state.observers['value']._key).toBeUndefined(); + expect(state.sideEffects).toStrictEqual({}); + }); + + it('should create Enhanced State and should call initial set (specific config)', () => { + // Overwrite select once to not call it + jest + .spyOn(EnhancedState.prototype, 'set') + .mockReturnValueOnce(undefined as any); + + const dummyObserver = new Observer(dummyAgile); + + const state = new EnhancedState(dummyAgile, 'coolValue', { + key: 'coolState', + dependents: [dummyObserver], + }); + + expect(state.isPersisted).toBeFalsy(); + expect(state.persistent).toBeUndefined(); + expect(state.computeValueMethod).toBeUndefined(); + expect(state.computeExistsMethod).toBeInstanceOf(Function); + expect(state.currentInterval).toBeUndefined(); + + // Check if State was called with correct parameters + expect(state._key).toBe('coolState'); + expect(state.isSet).toBeFalsy(); + expect(state.isPlaceholder).toBeTruthy(); + expect(state.initialStateValue).toBe('coolValue'); + expect(state._value).toBe('coolValue'); + expect(state.previousStateValue).toBe('coolValue'); + expect(state.nextStateValue).toBe('coolValue'); + expect(state.observers['value']).toBeInstanceOf(StateObserver); + expect(Array.from(state.observers['value'].dependents)).toStrictEqual([ + dummyObserver, + ]); + expect(state.observers['value']._key).toBe('coolState'); + expect(state.sideEffects).toStrictEqual({}); + }); + + it("should create Enhanced State and shouldn't call initial set (config.isPlaceholder = true)", () => { + // Overwrite select once to not call it + jest + .spyOn(EnhancedState.prototype, 'set') + .mockReturnValueOnce(undefined as any); + + const state = new EnhancedState(dummyAgile, 'coolValue', { + isPlaceholder: true, + }); + + expect(state.isPersisted).toBeFalsy(); + expect(state.persistent).toBeUndefined(); + expect(state.computeValueMethod).toBeUndefined(); + expect(state.computeExistsMethod).toBeInstanceOf(Function); + expect(state.currentInterval).toBeUndefined(); + + // Check if State was called with correct parameters + expect(state._key).toBeUndefined(); + expect(state.isSet).toBeFalsy(); + expect(state.isPlaceholder).toBeTruthy(); + expect(state.initialStateValue).toBe('coolValue'); + expect(state._value).toBe('coolValue'); + expect(state.previousStateValue).toBe('coolValue'); + expect(state.nextStateValue).toBe('coolValue'); + expect(state.observers['value']).toBeInstanceOf(StateObserver); + expect(Array.from(state.observers['value'].dependents)).toStrictEqual([]); + expect(state.observers['value']._key).toBeUndefined(); + expect(state.sideEffects).toStrictEqual({}); + }); + + describe('State Function Tests', () => { + let numberState: EnhancedState; + let objectState: EnhancedState<{ name: string; age: number }>; + let arrayState: EnhancedState; + let booleanState: EnhancedState; + + beforeEach(() => { + numberState = new EnhancedState(dummyAgile, 10, { + key: 'numberStateKey', + }); + objectState = new EnhancedState<{ name: string; age: number }>( + dummyAgile, + { name: 'jeff', age: 10 }, + { + key: 'objectStateKey', + } + ); + arrayState = new EnhancedState(dummyAgile, ['jeff'], { + key: 'arrayStateKey', + }); + booleanState = new EnhancedState(dummyAgile, false, { + key: 'booleanStateKey', + }); + }); + + describe('setKey function tests', () => { + // TODO + }); + + describe('undo function tests', () => { + beforeEach(() => { + numberState.set = jest.fn(); + }); + + it('should assign previousStateValue to currentValue (default config)', () => { + numberState.previousStateValue = 99; + + numberState.undo(); + + expect(numberState.set).toHaveBeenCalledWith( + numberState.previousStateValue, + {} + ); + }); + + it('should assign previousStateValue to currentValue (specific config)', () => { + numberState.previousStateValue = 99; + + numberState.undo({ + force: true, + storage: false, + }); + + expect(numberState.set).toHaveBeenCalledWith( + numberState.previousStateValue, + { + force: true, + storage: false, + } + ); + }); + }); + + describe('reset function tests', () => { + beforeEach(() => { + numberState.set = jest.fn(); + }); + + it('should assign initialStateValue to currentValue (default config)', () => { + numberState.initialStateValue = 99; + + numberState.reset(); + + expect(numberState.set).toHaveBeenCalledWith( + numberState.initialStateValue, + {} + ); + }); + + it('should assign initialStateValue to currentValue (specific config)', () => { + numberState.initialStateValue = 99; + + numberState.reset({ + force: true, + storage: false, + }); + + expect(numberState.set).toHaveBeenCalledWith( + numberState.initialStateValue, + { + force: true, + storage: false, + } + ); + }); + }); + + describe('patch function tests', () => { + beforeEach(() => { + objectState.ingest = jest.fn(); + numberState.ingest = jest.fn(); + arrayState.ingest = jest.fn(); + jest.spyOn(Utils, 'flatMerge'); + }); + + it("shouldn't patch specified object value into a not object based State (default config)", () => { + numberState.patch({ changed: 'object' }); + + LogMock.hasLoggedCode('14:03:02'); + expect(objectState.ingest).not.toHaveBeenCalled(); + }); + + it("shouldn't patch specified non object value into a object based State (default config)", () => { + objectState.patch('number' as any); + + LogMock.hasLoggedCode('00:03:01', ['TargetWithChanges', 'object']); + expect(objectState.ingest).not.toHaveBeenCalled(); + }); + + it('should patch specified object value into a object based State (default config)', () => { + objectState.patch({ name: 'frank' }); + + expect(Utils.flatMerge).toHaveBeenCalledWith( + { age: 10, name: 'jeff' }, + { name: 'frank' }, + { addNewProperties: true } + ); + expect(objectState.nextStateValue).toStrictEqual({ + age: 10, + name: 'frank', + }); + expect(objectState.ingest).toHaveBeenCalledWith({}); + }); + + it('should patch specified object value into a object based State (specific config)', () => { + objectState.patch( + { name: 'frank' }, + { + addNewProperties: false, + background: true, + force: true, + overwrite: true, + sideEffects: { + enabled: false, + }, + } + ); + + expect(Utils.flatMerge).toHaveBeenCalledWith( + { age: 10, name: 'jeff' }, + { name: 'frank' }, + { addNewProperties: false } + ); + expect(objectState.nextStateValue).toStrictEqual({ + age: 10, + name: 'frank', + }); + expect(objectState.ingest).toHaveBeenCalledWith({ + background: true, + force: true, + overwrite: true, + sideEffects: { + enabled: false, + }, + }); + }); + + it('should patch specified array value into a array based State (default config)', () => { + arrayState.patch(['hi']); + + expect(Utils.flatMerge).not.toHaveBeenCalled(); + expect(arrayState.nextStateValue).toStrictEqual(['jeff', 'hi']); + expect(arrayState.ingest).toHaveBeenCalledWith({}); + }); + + it('should patch specified array value into a object based State', () => { + objectState.patch(['hi'], { addNewProperties: true }); + + expect(Utils.flatMerge).toHaveBeenCalledWith( + { age: 10, name: 'jeff' }, + ['hi'], + { addNewProperties: true } + ); + expect(objectState.nextStateValue).toStrictEqual({ + 0: 'hi', + age: 10, + name: 'jeff', + }); + expect(objectState.ingest).toHaveBeenCalledWith({}); + }); + }); + + describe('watch function tests', () => { + let dummyCallbackFunction; + + beforeEach(() => { + jest.spyOn(numberState, 'addSideEffect'); + dummyCallbackFunction = jest.fn(); + }); + + it('should add passed watcherFunction to watchers at passed key', () => { + const response = numberState.watch('dummyKey', dummyCallbackFunction); + + expect(response).toBe(numberState); + expect(numberState.addSideEffect).toHaveBeenCalledWith( + 'dummyKey', + expect.any(Function), + { weight: 0 } + ); + + // Test whether registered callback function is called + numberState.sideEffects['dummyKey'].callback(numberState); + expect(dummyCallbackFunction).toHaveBeenCalledWith( + numberState._value, + 'dummyKey' + ); + }); + + it('should add passed watcherFunction to watchers at random key if no key passed and return that generated key', () => { + jest.spyOn(Utils, 'generateId').mockReturnValue('randomKey'); + + const response = numberState.watch(dummyCallbackFunction); + + expect(response).toBe('randomKey'); + expect(numberState.addSideEffect).toHaveBeenCalledWith( + 'randomKey', + expect.any(Function), + { weight: 0 } + ); + expect(Utils.generateId).toHaveBeenCalled(); + + // Test whether registered callback function is called + numberState.sideEffects['randomKey'].callback(numberState); + expect(dummyCallbackFunction).toHaveBeenCalledWith( + numberState._value, + 'randomKey' + ); + }); + + it("shouldn't add passed invalid watcherFunction to watchers at passed key", () => { + const response = numberState.watch( + 'dummyKey', + 'noFunction hehe' as any + ); + + expect(response).toBe(numberState); + expect(numberState.addSideEffect).not.toHaveBeenCalled(); + LogMock.hasLoggedCode('00:03:01', ['Watcher Callback', 'function']); + }); + }); + + describe('removeWatcher function tests', () => { + beforeEach(() => { + jest.spyOn(numberState, 'removeSideEffect'); + }); + + it('should remove watcher at key from State', () => { + numberState.removeWatcher('dummyKey'); + + expect(numberState.removeSideEffect).toHaveBeenCalledWith('dummyKey'); + }); + }); + + describe('onInaugurated function tests', () => { + let dummyCallbackFunction; + + beforeEach(() => { + jest.spyOn(numberState, 'watch'); + jest.spyOn(numberState, 'removeSideEffect'); + dummyCallbackFunction = jest.fn(); + }); + + it('should add watcher called InauguratedWatcherKey to State', () => { + numberState.onInaugurated(dummyCallbackFunction); + + expect(numberState.watch).toHaveBeenCalledWith( + 'InauguratedWatcherKey', + expect.any(Function) + ); + }); + + it('should remove itself after invoking', () => { + numberState.onInaugurated(dummyCallbackFunction); + + // Call Inaugurated Watcher + numberState.sideEffects['InauguratedWatcherKey'].callback(numberState); + + expect(dummyCallbackFunction).toHaveBeenCalledWith( + numberState.value, + 'InauguratedWatcherKey' + ); + expect(numberState.removeSideEffect).toHaveBeenCalledWith( + 'InauguratedWatcherKey' + ); + }); + }); + + describe('persist function tests', () => { + it('should create persistent with StateKey (default config)', () => { + numberState.persist(); + + expect(numberState.persistent).toBeInstanceOf(StatePersistent); + expect(StatePersistent).toHaveBeenCalledWith(numberState, { + instantiate: true, + storageKeys: [], + key: numberState._key, + defaultStorageKey: null, + }); + }); + + it('should create persistent with StateKey (specific config)', () => { + numberState.persist({ + storageKeys: ['test1', 'test2'], + loadValue: false, + defaultStorageKey: 'test1', + }); + + expect(numberState.persistent).toBeInstanceOf(StatePersistent); + expect(StatePersistent).toHaveBeenCalledWith(numberState, { + instantiate: false, + storageKeys: ['test1', 'test2'], + key: numberState._key, + defaultStorageKey: 'test1', + }); + }); + + it('should create persistent with passed Key (default config)', () => { + numberState.persist('passedKey'); + + expect(numberState.persistent).toBeInstanceOf(StatePersistent); + expect(StatePersistent).toHaveBeenCalledWith(numberState, { + instantiate: true, + storageKeys: [], + key: 'passedKey', + defaultStorageKey: null, + }); + }); + + it('should create persistent with passed Key (specific config)', () => { + numberState.persist('passedKey', { + storageKeys: ['test1', 'test2'], + loadValue: false, + defaultStorageKey: 'test1', + }); + + expect(numberState.persistent).toBeInstanceOf(StatePersistent); + expect(StatePersistent).toHaveBeenCalledWith(numberState, { + instantiate: false, + storageKeys: ['test1', 'test2'], + key: 'passedKey', + defaultStorageKey: 'test1', + }); + }); + + it("shouldn't overwrite existing Persistent", () => { + const dummyPersistent = new StatePersistent(numberState); + numberState.persistent = dummyPersistent; + numberState.isPersisted = true; + jest.clearAllMocks(); + + numberState.persist('newPersistentKey'); + + expect(numberState.persistent).toBe(dummyPersistent); + // expect(numberState.persistent._key).toBe("newPersistentKey"); // Can not test because of Mocking Persistent + expect(StatePersistent).not.toHaveBeenCalled(); + }); + }); + + describe('onLoad function tests', () => { + const dummyCallbackFunction = jest.fn(); + + it("should set onLoad function if State is persisted and shouldn't call it initially (state.isPersisted = false)", () => { + numberState.persistent = new StatePersistent(numberState); + numberState.isPersisted = false; + + numberState.onLoad(dummyCallbackFunction); + + expect(numberState.persistent.onLoad).toBe(dummyCallbackFunction); + expect(dummyCallbackFunction).not.toHaveBeenCalled(); + LogMock.hasNotLogged('warn'); + }); + + it('should set onLoad function if State is persisted and should call it initially (state.isPersisted = true)', () => { + numberState.persistent = new StatePersistent(numberState); + numberState.isPersisted = true; + + numberState.onLoad(dummyCallbackFunction); + + expect(numberState.persistent.onLoad).toBe(dummyCallbackFunction); + expect(dummyCallbackFunction).toHaveBeenCalledWith(true); + LogMock.hasNotLogged('warn'); + }); + + it("shouldn't set onLoad function if State isn't persisted", () => { + numberState.onLoad(dummyCallbackFunction); + + expect(numberState?.persistent?.onLoad).toBeUndefined(); + expect(dummyCallbackFunction).not.toHaveBeenCalled(); + LogMock.hasNotLogged('warn'); + }); + + it("shouldn't set invalid onLoad callback function", () => { + numberState.persistent = new StatePersistent(numberState); + numberState.isPersisted = false; + + numberState.onLoad(10 as any); + + expect(numberState?.persistent?.onLoad).toBeUndefined(); + LogMock.hasLoggedCode('00:03:01', ['OnLoad Callback', 'function']); + }); + }); + + describe('interval function tests', () => { + const dummyCallbackFunction = jest.fn(); + const dummyCallbackFunction2 = jest.fn(); + + beforeEach(() => { + jest.useFakeTimers(); + numberState.set = jest.fn(); + }); + + afterEach(() => { + jest.clearAllTimers(); + }); + + it('should create an interval (without custom milliseconds)', () => { + dummyCallbackFunction.mockReturnValueOnce(10); + + numberState.interval(dummyCallbackFunction); + + jest.runTimersToTime(1000); // travel 1000s in time -> execute interval + + expect(setInterval).toHaveBeenCalledTimes(1); + expect(setInterval).toHaveBeenLastCalledWith( + expect.any(Function), + 1000 + ); + expect(dummyCallbackFunction).toHaveBeenCalledWith(numberState._value); + expect(numberState.set).toHaveBeenCalledWith(10); + expect(numberState.currentInterval).toEqual({ + id: expect.anything(), + ref: expect.anything(), + unref: expect.anything(), + }); + LogMock.hasNotLogged('warn'); + }); + + it('should create an interval (with custom milliseconds)', () => { + dummyCallbackFunction.mockReturnValueOnce(10); + + numberState.interval(dummyCallbackFunction, 2000); + + jest.runTimersToTime(2000); // travel 2000 in time -> execute interval + + expect(setInterval).toHaveBeenCalledTimes(1); + expect(setInterval).toHaveBeenLastCalledWith( + expect.any(Function), + 2000 + ); + expect(dummyCallbackFunction).toHaveBeenCalledWith(numberState._value); + expect(numberState.set).toHaveBeenCalledWith(10); + expect(numberState.currentInterval).toEqual({ + id: expect.anything(), + ref: expect.anything(), + unref: expect.anything(), + }); + LogMock.hasNotLogged('warn'); + }); + + it("shouldn't be able to create second interval and print warning", () => { + numberState.interval(dummyCallbackFunction, 3000); + const currentInterval = numberState.currentInterval; + numberState.interval(dummyCallbackFunction2); + + expect(setInterval).toHaveBeenCalledTimes(1); + expect(setInterval).toHaveBeenLastCalledWith( + expect.any(Function), + 3000 + ); + expect(numberState.currentInterval).toStrictEqual(currentInterval); + LogMock.hasLoggedCode('14:03:03', [], numberState.currentInterval); + }); + + it("shouldn't set invalid interval callback function", () => { + numberState.interval(10 as any); + + expect(setInterval).not.toHaveBeenCalled(); + expect(numberState.currentInterval).toBeUndefined(); + LogMock.hasLoggedCode('00:03:01', ['Interval Callback', 'function']); + }); + }); + + describe('clearInterval function tests', () => { + const dummyCallbackFunction = jest.fn(); + + beforeEach(() => { + jest.useFakeTimers(); + numberState.set = jest.fn(); + }); + + afterEach(() => { + jest.clearAllTimers(); + }); + + it('should clear existing interval', () => { + numberState.interval(dummyCallbackFunction); + const currentInterval = numberState.currentInterval; + + numberState.clearInterval(); + + expect(clearInterval).toHaveBeenCalledTimes(1); + expect(clearInterval).toHaveBeenLastCalledWith(currentInterval); + expect(numberState.currentInterval).toBeUndefined(); + }); + + it("shouldn't clear not existing interval", () => { + numberState.clearInterval(); + + expect(clearInterval).not.toHaveBeenCalled(); + expect(numberState.currentInterval).toBeUndefined(); + }); + }); + + describe('exists get function tests', () => { + it('should return true if State is no placeholder and computeExistsMethod returns true', () => { + numberState.computeExistsMethod = jest.fn().mockReturnValueOnce(true); + numberState.isPlaceholder = false; + + expect(numberState.exists).toBeTruthy(); + expect(numberState.computeExistsMethod).toHaveBeenCalledWith( + numberState.value + ); + }); + + it('should return false if State is no placeholder and computeExistsMethod returns false', () => { + numberState.computeExistsMethod = jest.fn().mockReturnValueOnce(false); + numberState.isPlaceholder = false; + + expect(numberState.exists).toBeFalsy(); + expect(numberState.computeExistsMethod).toHaveBeenCalledWith( + numberState.value + ); + }); + + it('should return false if State is placeholder"', () => { + numberState.computeExistsMethod = jest.fn(() => true); + numberState.isPlaceholder = true; + + expect(numberState.exists).toBeFalsy(); + expect(numberState.computeExistsMethod).not.toHaveBeenCalled(); // since isPlaceholder gets checked first + }); + }); + + describe('computeExists function tests', () => { + it('should assign passed function to computeExistsMethod', () => { + const computeMethod = (value) => value === null; + + numberState.computeExists(computeMethod); + + expect(numberState.computeExistsMethod).toBe(computeMethod); + LogMock.hasNotLogged('warn'); + }); + + it("shouldn't assign passed invalid function to computeExistsMethod", () => { + numberState.computeExists(10 as any); + + expect(numberState.computeExistsMethod).toBeInstanceOf(Function); + LogMock.hasLoggedCode('00:03:01', [ + 'Compute Exists Method', + 'function', + ]); + }); + }); + + describe('is function tests', () => { + beforeEach(() => { + jest.spyOn(Utils, 'equal'); + }); + + it('should return true if passed value is equal to the current StateValue', () => { + const response = numberState.is(10); + + expect(response).toBeTruthy(); + expect(Utils.equal).toHaveBeenCalledWith(10, numberState._value); + }); + + it('should return false if passed value is not equal to the current StateValue', () => { + const response = numberState.is(20); + + expect(response).toBeFalsy(); + expect(Utils.equal).toHaveBeenCalledWith(20, numberState._value); + }); + }); + + describe('isNot function tests', () => { + beforeEach(() => { + jest.spyOn(Utils, 'notEqual'); + }); + + it('should return false if passed value is equal to the current StateValue', () => { + const response = numberState.isNot(10); + + expect(response).toBeFalsy(); + expect(Utils.notEqual).toHaveBeenCalledWith(10, numberState._value); + }); + + it('should return true if passed value is not equal to the current StateValue', () => { + const response = numberState.isNot(20); + + expect(response).toBeTruthy(); + expect(Utils.notEqual).toHaveBeenCalledWith(20, numberState._value); + }); + }); + + describe('invert function tests', () => { + let dummyState: EnhancedState; + + beforeEach(() => { + dummyState = new EnhancedState(dummyAgile, null); + + dummyState.set = jest.fn(); + }); + + it('should invert value of the type boolean', () => { + dummyState.nextStateValue = false; + + dummyState.invert(); + + expect(dummyState.set).toHaveBeenCalledWith(true); + }); + + it('should invert value of the type number', () => { + dummyState.nextStateValue = 10; + + dummyState.invert(); + + expect(dummyState.set).toHaveBeenCalledWith(-10); + }); + + it('should invert value of the type array', () => { + dummyState.nextStateValue = ['1', '2', '3']; + + dummyState.invert(); + + expect(dummyState.set).toHaveBeenCalledWith(['3', '2', '1']); + }); + + it('should invert value of the type string', () => { + dummyState.nextStateValue = 'jeff'; + + dummyState.invert(); + + expect(dummyState.set).toHaveBeenCalledWith('ffej'); + }); + + it("shouldn't invert not invertible types like function, null, undefined, object", () => { + dummyState.nextStateValue = () => { + // empty + }; + + dummyState.invert(); + + expect(dummyState.set).not.toHaveBeenCalled(); + LogMock.hasLoggedCode('14:03:04', ['function']); + }); + }); + + describe('computeValue function tests', () => { + beforeEach(() => { + numberState.set = jest.fn(); + }); + + it('should assign passed function to computeValueMethod and compute State value initially', () => { + const computeMethod = () => 10; + + numberState.computeValue(computeMethod); + + expect(numberState.set).toHaveBeenCalledWith(10); + expect(numberState.computeValueMethod).toBe(computeMethod); + LogMock.hasNotLogged('warn'); + }); + + it("shouldn't assign passed invalid function to computeValueMethod", () => { + numberState.computeValue(10 as any); + + expect(numberState.set).not.toHaveBeenCalled(); + expect(numberState.computeValueMethod).toBeUndefined(); + LogMock.hasLoggedCode('00:03:01', ['Compute Value Method', 'function']); + }); + }); + }); +}); diff --git a/packages/core/tests/unit/state/state.observer.test.ts b/packages/core/tests/unit/state/state.observer.test.ts index 53ebd1dd..e6e64367 100644 --- a/packages/core/tests/unit/state/state.observer.test.ts +++ b/packages/core/tests/unit/state/state.observer.test.ts @@ -3,10 +3,10 @@ import { Computed, StateRuntimeJob, Observer, - State, StateObserver, - StatePersistent, + EnhancedState, SubscriptionContainer, + State, } from '../../../src'; import * as Utils from '@agile-ts/utils'; import { LogMock } from '../../helper/logMock'; @@ -19,8 +19,10 @@ describe('StateObserver Tests', () => { beforeEach(() => { LogMock.mockLogs(); - dummyAgile = new Agile({ localStorage: false }); - dummyState = new State(dummyAgile, 'dummyValue', { key: 'dummyState' }); + dummyAgile = new Agile(); + dummyState = new State(dummyAgile, 'dummyValue', { + key: 'dummyState', + }); jest.clearAllMocks(); }); @@ -317,7 +319,8 @@ describe('StateObserver Tests', () => { 'should ingest the State into the Runtime and compute its new value ' + 'if the State has a set compute function (default config)', () => { - dummyState.computeValueMethod = (value) => `cool value '${value}'`; + (dummyState as EnhancedState).computeValueMethod = (value) => + `cool value '${value}'`; stateObserver.ingestValue('updatedDummyValue'); @@ -341,8 +344,6 @@ describe('StateObserver Tests', () => { dummyJob = new StateRuntimeJob(stateObserver, { key: 'dummyJob', }); - dummyState.persistent = new StatePersistent(dummyState); - dummyState.isPersisted = true; stateObserver.sideEffects = jest.fn(); }); diff --git a/packages/core/tests/unit/state/state.persistent.test.ts b/packages/core/tests/unit/state/state.persistent.test.ts index 65603523..1fa45381 100644 --- a/packages/core/tests/unit/state/state.persistent.test.ts +++ b/packages/core/tests/unit/state/state.persistent.test.ts @@ -1,21 +1,30 @@ import { Agile, - State, StatePersistent, Storage, Persistent, + EnhancedState, + Storages, + assignSharedAgileStorageManager, + createStorageManager, } from '../../../src'; import { LogMock } from '../../helper/logMock'; +import waitForExpect from 'wait-for-expect'; describe('StatePersistent Tests', () => { let dummyAgile: Agile; - let dummyState: State; + let dummyState: EnhancedState; + let storageManager: Storages; beforeEach(() => { LogMock.mockLogs(); - dummyAgile = new Agile({ localStorage: false }); - dummyState = new State(dummyAgile, 'dummyValue'); + dummyAgile = new Agile(); + dummyState = new EnhancedState(dummyAgile, 'dummyValue'); + + // Register Storage Manager + storageManager = createStorageManager(); + assignSharedAgileStorageManager(storageManager); jest.spyOn(StatePersistent.prototype, 'instantiatePersistent'); jest.spyOn(StatePersistent.prototype, 'initialLoading'); @@ -122,7 +131,7 @@ describe('StatePersistent Tests', () => { key: 'statePersistentKey', storageKeys: ['dummyStorage'], }); - dummyAgile.registerStorage( + storageManager.register( new Storage({ key: 'dummyStorage', methods: { @@ -142,8 +151,10 @@ describe('StatePersistent Tests', () => { it('should initialLoad and set isPersisted in State to true', async () => { await statePersistent.initialLoading(); - expect(Persistent.prototype.initialLoading).toHaveBeenCalled(); - expect(dummyState.isPersisted).toBeTruthy(); + await waitForExpect(() => { + expect(Persistent.prototype.initialLoading).toHaveBeenCalled(); + expect(dummyState.isPersisted).toBeTruthy(); + }); }); }); @@ -158,14 +169,14 @@ describe('StatePersistent Tests', () => { 'and apply it to the State if the loading was successful', async () => { statePersistent.ready = true; - dummyAgile.storages.get = jest.fn(() => + storageManager.get = jest.fn(() => Promise.resolve('dummyValue' as any) ); const response = await statePersistent.loadPersistedValue(); expect(response).toBeTruthy(); - expect(dummyAgile.storages.get).toHaveBeenCalledWith( + expect(storageManager.get).toHaveBeenCalledWith( statePersistent._key, statePersistent.config.defaultStorageKey ); @@ -184,14 +195,12 @@ describe('StatePersistent Tests', () => { "and apply it to the State if the loading wasn't successful", async () => { statePersistent.ready = true; - dummyAgile.storages.get = jest.fn(() => - Promise.resolve(undefined as any) - ); + storageManager.get = jest.fn(() => Promise.resolve(undefined as any)); const response = await statePersistent.loadPersistedValue(); expect(response).toBeFalsy(); - expect(dummyAgile.storages.get).toHaveBeenCalledWith( + expect(storageManager.get).toHaveBeenCalledWith( statePersistent._key, statePersistent.config.defaultStorageKey ); @@ -205,14 +214,14 @@ describe('StatePersistent Tests', () => { 'and apply it to the State if the loading was successful', async () => { statePersistent.ready = true; - dummyAgile.storages.get = jest.fn(() => + storageManager.get = jest.fn(() => Promise.resolve('dummyValue' as any) ); const response = await statePersistent.loadPersistedValue('coolKey'); expect(response).toBeTruthy(); - expect(dummyAgile.storages.get).toHaveBeenCalledWith( + expect(storageManager.get).toHaveBeenCalledWith( 'coolKey', statePersistent.config.defaultStorageKey ); @@ -231,14 +240,12 @@ describe('StatePersistent Tests', () => { "if Persistent isn't ready yet", async () => { statePersistent.ready = false; - dummyAgile.storages.get = jest.fn(() => - Promise.resolve(undefined as any) - ); + storageManager.get = jest.fn(() => Promise.resolve(undefined as any)); const response = await statePersistent.loadPersistedValue(); expect(response).toBeFalsy(); - expect(dummyAgile.storages.get).not.toHaveBeenCalled(); + expect(storageManager.get).not.toHaveBeenCalled(); expect(dummyState.set).not.toHaveBeenCalled(); expect(statePersistent.setupSideEffects).not.toHaveBeenCalled(); } @@ -308,9 +315,7 @@ describe('StatePersistent Tests', () => { () => { statePersistent.setupSideEffects(); - expect( - dummyState.addSideEffect - ).toHaveBeenCalledWith( + expect(dummyState.addSideEffect).toHaveBeenCalledWith( StatePersistent.storeValueSideEffectKey, expect.any(Function), { weight: 0 } @@ -364,7 +369,7 @@ describe('StatePersistent Tests', () => { describe('removePersistedValue function tests', () => { beforeEach(() => { dummyState.removeSideEffect = jest.fn(); - dummyAgile.storages.remove = jest.fn(); + storageManager.remove = jest.fn(); statePersistent.isPersisted = true; }); @@ -378,7 +383,7 @@ describe('StatePersistent Tests', () => { expect(dummyState.removeSideEffect).toHaveBeenCalledWith( StatePersistent.storeValueSideEffectKey ); - expect(dummyAgile.storages.remove).toHaveBeenCalledWith( + expect(storageManager.remove).toHaveBeenCalledWith( statePersistent._key, statePersistent.storageKeys ); @@ -394,7 +399,7 @@ describe('StatePersistent Tests', () => { expect(dummyState.removeSideEffect).toHaveBeenCalledWith( StatePersistent.storeValueSideEffectKey ); - expect(dummyAgile.storages.remove).toHaveBeenCalledWith( + expect(storageManager.remove).toHaveBeenCalledWith( 'coolKey', statePersistent.storageKeys ); @@ -408,7 +413,7 @@ describe('StatePersistent Tests', () => { expect(response).toBeFalsy(); expect(dummyState.removeSideEffect).not.toHaveBeenCalled(); - expect(dummyAgile.storages.remove).not.toHaveBeenCalled(); + expect(storageManager.remove).not.toHaveBeenCalled(); expect(statePersistent.isPersisted).toBeTruthy(); }); }); @@ -450,13 +455,13 @@ describe('StatePersistent Tests', () => { describe('rebuildStorageSideEffect function tests', () => { beforeEach(() => { - dummyAgile.storages.set = jest.fn(); + storageManager.set = jest.fn(); }); it('should store current State value in the corresponding Storage (default config)', () => { statePersistent.rebuildStorageSideEffect(dummyState, 'coolKey'); - expect(dummyAgile.storages.set).toHaveBeenCalledWith( + expect(storageManager.set).toHaveBeenCalledWith( 'coolKey', dummyState.getPersistableValue(), statePersistent.storageKeys @@ -468,7 +473,7 @@ describe('StatePersistent Tests', () => { storage: false, }); - expect(dummyAgile.storages.set).not.toHaveBeenCalled(); + expect(storageManager.set).not.toHaveBeenCalled(); }); }); }); 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 85df6e56..fc2d2461 100644 --- a/packages/core/tests/unit/state/state.runtime.job.test.ts +++ b/packages/core/tests/unit/state/state.runtime.job.test.ts @@ -18,7 +18,7 @@ describe('RuntimeJob Tests', () => { beforeEach(() => { LogMock.mockLogs(); - dummyAgile = new Agile({ localStorage: false }); + dummyAgile = new Agile(); dummyIntegration = new Integration({ key: 'myIntegration', }); diff --git a/packages/core/tests/unit/state/state.test.ts b/packages/core/tests/unit/state/state.test.ts index 4155c813..1b46a1aa 100644 --- a/packages/core/tests/unit/state/state.test.ts +++ b/packages/core/tests/unit/state/state.test.ts @@ -3,10 +3,8 @@ import { Agile, StateObserver, Observer, - StatePersistent, ComputedTracker, } from '../../../src'; -import * as Utils from '@agile-ts/utils'; import { LogMock } from '../../helper/logMock'; jest.mock('../../../src/state/state.persistent'); @@ -17,7 +15,7 @@ describe('State Tests', () => { beforeEach(() => { LogMock.mockLogs(); - dummyAgile = new Agile({ localStorage: false }); + dummyAgile = new Agile(); jest.spyOn(State.prototype, 'set'); @@ -42,10 +40,6 @@ describe('State Tests', () => { expect(Array.from(state.observers['value'].dependents)).toStrictEqual([]); expect(state.observers['value']._key).toBeUndefined(); expect(state.sideEffects).toStrictEqual({}); - expect(state.computeValueMethod).toBeUndefined(); - expect(state.computeExistsMethod).toBeInstanceOf(Function); - expect(state.isPersisted).toBeFalsy(); - expect(state.persistent).toBeUndefined(); }); it('should create State and should call initial set (specific config)', () => { @@ -73,10 +67,6 @@ describe('State Tests', () => { ]); expect(state.observers['value']._key).toBe('coolState'); expect(state.sideEffects).toStrictEqual({}); - expect(state.computeValueMethod).toBeUndefined(); - expect(state.computeExistsMethod).toBeInstanceOf(Function); - expect(state.isPersisted).toBeFalsy(); - expect(state.persistent).toBeUndefined(); }); it("should create State and shouldn't call initial set (config.isPlaceholder = true)", () => { @@ -97,10 +87,6 @@ describe('State Tests', () => { expect(Array.from(state.observers['value'].dependents)).toStrictEqual([]); expect(state.observers['value']._key).toBeUndefined(); expect(state.sideEffects).toStrictEqual({}); - expect(state.computeValueMethod).toBeUndefined(); - expect(state.computeExistsMethod).toBeInstanceOf(Function); - expect(state.isPersisted).toBeFalsy(); - expect(state.persistent).toBeUndefined(); }); describe('State Function Tests', () => { @@ -175,45 +161,31 @@ describe('State Tests', () => { beforeEach(() => { dummyOutputObserver = new StateObserver(numberState, { key: 'oldKey' }); - numberState.persistent = new StatePersistent(numberState); numberState.observers['output'] = dummyOutputObserver; - - numberState.persistent.setKey = jest.fn(); }); it('should update existing Key in all instances', () => { - if (numberState.persistent) - numberState.persistent._key = 'numberStateKey'; - numberState.setKey('newKey'); expect(numberState._key).toBe('newKey'); expect(numberState.observers['value']._key).toBe('newKey'); expect(numberState.observers['output']._key).toBe('newKey'); - expect(numberState.persistent?.setKey).toHaveBeenCalledWith('newKey'); }); it("should update existing Key in all instances except persistent if the StateKey and PersistKey aren't equal", () => { - if (numberState.persistent) numberState.persistent._key = 'randomKey'; - numberState.setKey('newKey'); expect(numberState._key).toBe('newKey'); expect(numberState.observers['value']._key).toBe('newKey'); expect(numberState.observers['output']._key).toBe('newKey'); - expect(numberState.persistent?.setKey).not.toHaveBeenCalled(); }); it('should update existing Key in all instances except persistent if new StateKey is undefined', () => { - if (numberState.persistent) - numberState.persistent._key = 'numberStateKey'; - numberState.setKey(undefined); expect(numberState._key).toBeUndefined(); expect(numberState.observers['value']._key).toBeUndefined(); expect(numberState.observers['output']._key).toBeUndefined(); - expect(numberState.persistent?.setKey).not.toHaveBeenCalled(); }); }); @@ -277,9 +249,10 @@ describe('State Tests', () => { LogMock.hasNotLogged('warn'); LogMock.hasNotLogged('error'); - expect( - numberState.observers['value'].ingestValue - ).toHaveBeenCalledWith('coolValue', { force: false }); + expect(numberState.observers['value'].ingestValue).toHaveBeenCalledWith( + 'coolValue', + { force: false } + ); }); }); @@ -307,668 +280,6 @@ describe('State Tests', () => { }); }); - describe('undo function tests', () => { - beforeEach(() => { - numberState.set = jest.fn(); - }); - - it('should assign previousStateValue to currentValue (default config)', () => { - numberState.previousStateValue = 99; - - numberState.undo(); - - expect(numberState.set).toHaveBeenCalledWith( - numberState.previousStateValue, - {} - ); - }); - - it('should assign previousStateValue to currentValue (specific config)', () => { - numberState.previousStateValue = 99; - - numberState.undo({ - force: true, - storage: false, - }); - - expect(numberState.set).toHaveBeenCalledWith( - numberState.previousStateValue, - { - force: true, - storage: false, - } - ); - }); - }); - - describe('reset function tests', () => { - beforeEach(() => { - numberState.set = jest.fn(); - }); - - it('should assign initialStateValue to currentValue (default config)', () => { - numberState.initialStateValue = 99; - - numberState.reset(); - - expect(numberState.set).toHaveBeenCalledWith( - numberState.initialStateValue, - {} - ); - }); - - it('should assign initialStateValue to currentValue (specific config)', () => { - numberState.initialStateValue = 99; - - numberState.reset({ - force: true, - storage: false, - }); - - expect(numberState.set).toHaveBeenCalledWith( - numberState.initialStateValue, - { - force: true, - storage: false, - } - ); - }); - }); - - describe('patch function tests', () => { - beforeEach(() => { - objectState.ingest = jest.fn(); - numberState.ingest = jest.fn(); - arrayState.ingest = jest.fn(); - jest.spyOn(Utils, 'flatMerge'); - }); - - it("shouldn't patch specified object value into a not object based State (default config)", () => { - numberState.patch({ changed: 'object' }); - - LogMock.hasLoggedCode('14:03:02'); - expect(objectState.ingest).not.toHaveBeenCalled(); - }); - - it("shouldn't patch specified non object value into a object based State (default config)", () => { - objectState.patch('number' as any); - - LogMock.hasLoggedCode('00:03:01', ['TargetWithChanges', 'object']); - expect(objectState.ingest).not.toHaveBeenCalled(); - }); - - it('should patch specified object value into a object based State (default config)', () => { - objectState.patch({ name: 'frank' }); - - expect(Utils.flatMerge).toHaveBeenCalledWith( - { age: 10, name: 'jeff' }, - { name: 'frank' }, - { addNewProperties: true } - ); - expect(objectState.nextStateValue).toStrictEqual({ - age: 10, - name: 'frank', - }); - expect(objectState.ingest).toHaveBeenCalledWith({}); - }); - - it('should patch specified object value into a object based State (specific config)', () => { - objectState.patch( - { name: 'frank' }, - { - addNewProperties: false, - background: true, - force: true, - overwrite: true, - sideEffects: { - enabled: false, - }, - } - ); - - expect(Utils.flatMerge).toHaveBeenCalledWith( - { age: 10, name: 'jeff' }, - { name: 'frank' }, - { addNewProperties: false } - ); - expect(objectState.nextStateValue).toStrictEqual({ - age: 10, - name: 'frank', - }); - expect(objectState.ingest).toHaveBeenCalledWith({ - background: true, - force: true, - overwrite: true, - sideEffects: { - enabled: false, - }, - }); - }); - - it('should patch specified array value into a array based State (default config)', () => { - arrayState.patch(['hi']); - - expect(Utils.flatMerge).not.toHaveBeenCalled(); - expect(arrayState.nextStateValue).toStrictEqual(['jeff', 'hi']); - expect(arrayState.ingest).toHaveBeenCalledWith({}); - }); - - it('should patch specified array value into a object based State', () => { - objectState.patch(['hi'], { addNewProperties: true }); - - expect(Utils.flatMerge).toHaveBeenCalledWith( - { age: 10, name: 'jeff' }, - ['hi'], - { addNewProperties: true } - ); - expect(objectState.nextStateValue).toStrictEqual({ - 0: 'hi', - age: 10, - name: 'jeff', - }); - expect(objectState.ingest).toHaveBeenCalledWith({}); - }); - }); - - describe('watch function tests', () => { - let dummyCallbackFunction; - - beforeEach(() => { - jest.spyOn(numberState, 'addSideEffect'); - dummyCallbackFunction = jest.fn(); - }); - - it('should add passed watcherFunction to watchers at passed key', () => { - const response = numberState.watch('dummyKey', dummyCallbackFunction); - - expect(response).toBe(numberState); - expect(numberState.addSideEffect).toHaveBeenCalledWith( - 'dummyKey', - expect.any(Function), - { weight: 0 } - ); - - // Test whether registered callback function is called - numberState.sideEffects['dummyKey'].callback(numberState); - expect(dummyCallbackFunction).toHaveBeenCalledWith( - numberState._value, - 'dummyKey' - ); - }); - - it('should add passed watcherFunction to watchers at random key if no key passed and return that generated key', () => { - jest.spyOn(Utils, 'generateId').mockReturnValue('randomKey'); - - const response = numberState.watch(dummyCallbackFunction); - - expect(response).toBe('randomKey'); - expect(numberState.addSideEffect).toHaveBeenCalledWith( - 'randomKey', - expect.any(Function), - { weight: 0 } - ); - expect(Utils.generateId).toHaveBeenCalled(); - - // Test whether registered callback function is called - numberState.sideEffects['randomKey'].callback(numberState); - expect(dummyCallbackFunction).toHaveBeenCalledWith( - numberState._value, - 'randomKey' - ); - }); - - it("shouldn't add passed invalid watcherFunction to watchers at passed key", () => { - const response = numberState.watch( - 'dummyKey', - 'noFunction hehe' as any - ); - - expect(response).toBe(numberState); - expect(numberState.addSideEffect).not.toHaveBeenCalled(); - LogMock.hasLoggedCode('00:03:01', ['Watcher Callback', 'function']); - }); - }); - - describe('removeWatcher function tests', () => { - beforeEach(() => { - jest.spyOn(numberState, 'removeSideEffect'); - }); - - it('should remove watcher at key from State', () => { - numberState.removeWatcher('dummyKey'); - - expect(numberState.removeSideEffect).toHaveBeenCalledWith('dummyKey'); - }); - }); - - describe('onInaugurated function tests', () => { - let dummyCallbackFunction; - - beforeEach(() => { - jest.spyOn(numberState, 'watch'); - jest.spyOn(numberState, 'removeSideEffect'); - dummyCallbackFunction = jest.fn(); - }); - - it('should add watcher called InauguratedWatcherKey to State', () => { - numberState.onInaugurated(dummyCallbackFunction); - - expect(numberState.watch).toHaveBeenCalledWith( - 'InauguratedWatcherKey', - expect.any(Function) - ); - }); - - it('should remove itself after invoking', () => { - numberState.onInaugurated(dummyCallbackFunction); - - // Call Inaugurated Watcher - numberState.sideEffects['InauguratedWatcherKey'].callback(numberState); - - expect(dummyCallbackFunction).toHaveBeenCalledWith( - numberState.value, - 'InauguratedWatcherKey' - ); - expect(numberState.removeSideEffect).toHaveBeenCalledWith( - 'InauguratedWatcherKey' - ); - }); - }); - - describe('persist function tests', () => { - it('should create persistent with StateKey (default config)', () => { - numberState.persist(); - - expect(numberState.persistent).toBeInstanceOf(StatePersistent); - expect(StatePersistent).toHaveBeenCalledWith(numberState, { - instantiate: true, - storageKeys: [], - key: numberState._key, - defaultStorageKey: null, - }); - }); - - it('should create persistent with StateKey (specific config)', () => { - numberState.persist({ - storageKeys: ['test1', 'test2'], - loadValue: false, - defaultStorageKey: 'test1', - }); - - expect(numberState.persistent).toBeInstanceOf(StatePersistent); - expect(StatePersistent).toHaveBeenCalledWith(numberState, { - instantiate: false, - storageKeys: ['test1', 'test2'], - key: numberState._key, - defaultStorageKey: 'test1', - }); - }); - - it('should create persistent with passed Key (default config)', () => { - numberState.persist('passedKey'); - - expect(numberState.persistent).toBeInstanceOf(StatePersistent); - expect(StatePersistent).toHaveBeenCalledWith(numberState, { - instantiate: true, - storageKeys: [], - key: 'passedKey', - defaultStorageKey: null, - }); - }); - - it('should create persistent with passed Key (specific config)', () => { - numberState.persist('passedKey', { - storageKeys: ['test1', 'test2'], - loadValue: false, - defaultStorageKey: 'test1', - }); - - expect(numberState.persistent).toBeInstanceOf(StatePersistent); - expect(StatePersistent).toHaveBeenCalledWith(numberState, { - instantiate: false, - storageKeys: ['test1', 'test2'], - key: 'passedKey', - defaultStorageKey: 'test1', - }); - }); - - it("shouldn't overwrite existing Persistent", () => { - const dummyPersistent = new StatePersistent(numberState); - numberState.persistent = dummyPersistent; - numberState.isPersisted = true; - jest.clearAllMocks(); - - numberState.persist('newPersistentKey'); - - expect(numberState.persistent).toBe(dummyPersistent); - // expect(numberState.persistent._key).toBe("newPersistentKey"); // Can not test because of Mocking Persistent - expect(StatePersistent).not.toHaveBeenCalled(); - }); - }); - - describe('onLoad function tests', () => { - const dummyCallbackFunction = jest.fn(); - - it("should set onLoad function if State is persisted and shouldn't call it initially (state.isPersisted = false)", () => { - numberState.persistent = new StatePersistent(numberState); - numberState.isPersisted = false; - - numberState.onLoad(dummyCallbackFunction); - - expect(numberState.persistent.onLoad).toBe(dummyCallbackFunction); - expect(dummyCallbackFunction).not.toHaveBeenCalled(); - LogMock.hasNotLogged('warn'); - }); - - it('should set onLoad function if State is persisted and should call it initially (state.isPersisted = true)', () => { - numberState.persistent = new StatePersistent(numberState); - numberState.isPersisted = true; - - numberState.onLoad(dummyCallbackFunction); - - expect(numberState.persistent.onLoad).toBe(dummyCallbackFunction); - expect(dummyCallbackFunction).toHaveBeenCalledWith(true); - LogMock.hasNotLogged('warn'); - }); - - it("shouldn't set onLoad function if State isn't persisted", () => { - numberState.onLoad(dummyCallbackFunction); - - expect(numberState?.persistent?.onLoad).toBeUndefined(); - expect(dummyCallbackFunction).not.toHaveBeenCalled(); - LogMock.hasNotLogged('warn'); - }); - - it("shouldn't set invalid onLoad callback function", () => { - numberState.persistent = new StatePersistent(numberState); - numberState.isPersisted = false; - - numberState.onLoad(10 as any); - - expect(numberState?.persistent?.onLoad).toBeUndefined(); - LogMock.hasLoggedCode('00:03:01', ['OnLoad Callback', 'function']); - }); - }); - - describe('interval function tests', () => { - const dummyCallbackFunction = jest.fn(); - const dummyCallbackFunction2 = jest.fn(); - - beforeEach(() => { - jest.useFakeTimers(); - numberState.set = jest.fn(); - }); - - afterEach(() => { - jest.clearAllTimers(); - }); - - it('should create an interval (without custom milliseconds)', () => { - dummyCallbackFunction.mockReturnValueOnce(10); - - numberState.interval(dummyCallbackFunction); - - jest.runTimersToTime(1000); // travel 1000s in time -> execute interval - - expect(setInterval).toHaveBeenCalledTimes(1); - expect(setInterval).toHaveBeenLastCalledWith( - expect.any(Function), - 1000 - ); - expect(dummyCallbackFunction).toHaveBeenCalledWith(numberState._value); - expect(numberState.set).toHaveBeenCalledWith(10); - expect(numberState.currentInterval).toEqual({ - id: expect.anything(), - ref: expect.anything(), - unref: expect.anything(), - }); - LogMock.hasNotLogged('warn'); - }); - - it('should create an interval (with custom milliseconds)', () => { - dummyCallbackFunction.mockReturnValueOnce(10); - - numberState.interval(dummyCallbackFunction, 2000); - - jest.runTimersToTime(2000); // travel 2000 in time -> execute interval - - expect(setInterval).toHaveBeenCalledTimes(1); - expect(setInterval).toHaveBeenLastCalledWith( - expect.any(Function), - 2000 - ); - expect(dummyCallbackFunction).toHaveBeenCalledWith(numberState._value); - expect(numberState.set).toHaveBeenCalledWith(10); - expect(numberState.currentInterval).toEqual({ - id: expect.anything(), - ref: expect.anything(), - unref: expect.anything(), - }); - LogMock.hasNotLogged('warn'); - }); - - it("shouldn't be able to create second interval and print warning", () => { - numberState.interval(dummyCallbackFunction, 3000); - const currentInterval = numberState.currentInterval; - numberState.interval(dummyCallbackFunction2); - - expect(setInterval).toHaveBeenCalledTimes(1); - expect(setInterval).toHaveBeenLastCalledWith( - expect.any(Function), - 3000 - ); - expect(numberState.currentInterval).toStrictEqual(currentInterval); - LogMock.hasLoggedCode('14:03:03', [], numberState.currentInterval); - }); - - it("shouldn't set invalid interval callback function", () => { - numberState.interval(10 as any); - - expect(setInterval).not.toHaveBeenCalled(); - expect(numberState.currentInterval).toBeUndefined(); - LogMock.hasLoggedCode('00:03:01', ['Interval Callback', 'function']); - }); - }); - - describe('clearInterval function tests', () => { - const dummyCallbackFunction = jest.fn(); - - beforeEach(() => { - jest.useFakeTimers(); - numberState.set = jest.fn(); - }); - - afterEach(() => { - jest.clearAllTimers(); - }); - - it('should clear existing interval', () => { - numberState.interval(dummyCallbackFunction); - const currentInterval = numberState.currentInterval; - - numberState.clearInterval(); - - expect(clearInterval).toHaveBeenCalledTimes(1); - expect(clearInterval).toHaveBeenLastCalledWith(currentInterval); - expect(numberState.currentInterval).toBeUndefined(); - }); - - it("shouldn't clear not existing interval", () => { - numberState.clearInterval(); - - expect(clearInterval).not.toHaveBeenCalled(); - expect(numberState.currentInterval).toBeUndefined(); - }); - }); - - describe('exists get function tests', () => { - it('should return true if State is no placeholder and computeExistsMethod returns true', () => { - numberState.computeExistsMethod = jest.fn().mockReturnValueOnce(true); - numberState.isPlaceholder = false; - - expect(numberState.exists).toBeTruthy(); - expect(numberState.computeExistsMethod).toHaveBeenCalledWith( - numberState.value - ); - }); - - it('should return false if State is no placeholder and computeExistsMethod returns false', () => { - numberState.computeExistsMethod = jest.fn().mockReturnValueOnce(false); - numberState.isPlaceholder = false; - - expect(numberState.exists).toBeFalsy(); - expect(numberState.computeExistsMethod).toHaveBeenCalledWith( - numberState.value - ); - }); - - it('should return false if State is placeholder"', () => { - numberState.computeExistsMethod = jest.fn(() => true); - numberState.isPlaceholder = true; - - expect(numberState.exists).toBeFalsy(); - expect(numberState.computeExistsMethod).not.toHaveBeenCalled(); // since isPlaceholder gets checked first - }); - }); - - describe('computeExists function tests', () => { - it('should assign passed function to computeExistsMethod', () => { - const computeMethod = (value) => value === null; - - numberState.computeExists(computeMethod); - - expect(numberState.computeExistsMethod).toBe(computeMethod); - LogMock.hasNotLogged('warn'); - }); - - it("shouldn't assign passed invalid function to computeExistsMethod", () => { - numberState.computeExists(10 as any); - - expect(numberState.computeExistsMethod).toBeInstanceOf(Function); - LogMock.hasLoggedCode('00:03:01', [ - 'Compute Exists Method', - 'function', - ]); - }); - }); - - describe('is function tests', () => { - beforeEach(() => { - jest.spyOn(Utils, 'equal'); - }); - - it('should return true if passed value is equal to the current StateValue', () => { - const response = numberState.is(10); - - expect(response).toBeTruthy(); - expect(Utils.equal).toHaveBeenCalledWith(10, numberState._value); - }); - - it('should return false if passed value is not equal to the current StateValue', () => { - const response = numberState.is(20); - - expect(response).toBeFalsy(); - expect(Utils.equal).toHaveBeenCalledWith(20, numberState._value); - }); - }); - - describe('isNot function tests', () => { - beforeEach(() => { - jest.spyOn(Utils, 'notEqual'); - }); - - it('should return false if passed value is equal to the current StateValue', () => { - const response = numberState.isNot(10); - - expect(response).toBeFalsy(); - expect(Utils.notEqual).toHaveBeenCalledWith(10, numberState._value); - }); - - it('should return true if passed value is not equal to the current StateValue', () => { - const response = numberState.isNot(20); - - expect(response).toBeTruthy(); - expect(Utils.notEqual).toHaveBeenCalledWith(20, numberState._value); - }); - }); - - describe('invert function tests', () => { - let dummyState: State; - - beforeEach(() => { - dummyState = new State(dummyAgile, null); - - dummyState.set = jest.fn(); - }); - - it('should invert value of the type boolean', () => { - dummyState.nextStateValue = false; - - dummyState.invert(); - - expect(dummyState.set).toHaveBeenCalledWith(true); - }); - - it('should invert value of the type number', () => { - dummyState.nextStateValue = 10; - - dummyState.invert(); - - expect(dummyState.set).toHaveBeenCalledWith(-10); - }); - - it('should invert value of the type array', () => { - dummyState.nextStateValue = ['1', '2', '3']; - - dummyState.invert(); - - expect(dummyState.set).toHaveBeenCalledWith(['3', '2', '1']); - }); - - it('should invert value of the type string', () => { - dummyState.nextStateValue = 'jeff'; - - dummyState.invert(); - - expect(dummyState.set).toHaveBeenCalledWith('ffej'); - }); - - it("shouldn't invert not invertible types like function, null, undefined, object", () => { - dummyState.nextStateValue = () => { - // empty - }; - - dummyState.invert(); - - expect(dummyState.set).not.toHaveBeenCalled(); - LogMock.hasLoggedCode('14:03:04', ['function']); - }); - }); - - describe('computeValue function tests', () => { - beforeEach(() => { - numberState.set = jest.fn(); - }); - - it('should assign passed function to computeValueMethod and compute State value initially', () => { - const computeMethod = () => 10; - - numberState.computeValue(computeMethod); - - expect(numberState.set).toHaveBeenCalledWith(10); - expect(numberState.computeValueMethod).toBe(computeMethod); - LogMock.hasNotLogged('warn'); - }); - - it("shouldn't assign passed invalid function to computeValueMethod", () => { - numberState.computeValue(10 as any); - - expect(numberState.set).not.toHaveBeenCalled(); - expect(numberState.computeValueMethod).toBeUndefined(); - LogMock.hasLoggedCode('00:03:01', ['Compute Value Method', 'function']); - }); - }); - describe('addSideEffect function tests', () => { const sideEffectFunction = () => { /* empty function */ diff --git a/packages/core/tests/unit/storages/index.test.ts b/packages/core/tests/unit/storages/index.test.ts new file mode 100644 index 00000000..dde743f3 --- /dev/null +++ b/packages/core/tests/unit/storages/index.test.ts @@ -0,0 +1,165 @@ +import { + Agile, + Storages, + Storage, + assignSharedAgileInstance, +} from '../../../src'; +import * as StorageIndex from '../../../src/storages/index'; +import { LogMock } from '../../helper/logMock'; +jest.mock('../../../src/storages/storages'); +jest.mock('../../../src/storages/storage'); + +describe('Storages Index', () => { + let sharedAgileInstance: Agile; + + beforeEach(() => { + LogMock.mockLogs(); + + sharedAgileInstance = new Agile(); + assignSharedAgileInstance(sharedAgileInstance); + + // Reset Storage Manager + StorageIndex.assignSharedAgileStorageManager(null); + + jest.clearAllMocks(); + }); + + describe('createStorage function tests', () => { + const StorageMock = Storage as jest.MockedClass; + + beforeEach(() => { + StorageMock.mockClear(); + }); + + it('should create Storage', () => { + const storageConfig = { + prefix: 'test', + methods: { + get: () => { + /* empty function */ + }, + set: () => { + /* empty function */ + }, + remove: () => { + /* empty function */ + }, + }, + key: 'myTestStorage', + }; + + const storage = StorageIndex.createStorage(storageConfig); + + expect(storage).toBeInstanceOf(Storage); + expect(StorageMock).toHaveBeenCalledWith(storageConfig); + }); + }); + + describe('createStorageManager function tests', () => { + const StoragesMock = Storages as jest.MockedClass; + + beforeEach(() => { + StoragesMock.mockClear(); + }); + + it('should create Storage Manager (Storages) with the shared Agile Instance', () => { + const storageManager = StorageIndex.createStorageManager({ + localStorage: true, + }); + + expect(storageManager).toBeInstanceOf(Storages); + expect(StoragesMock).toHaveBeenCalledWith(sharedAgileInstance, { + localStorage: true, + }); + }); + + it('should create Storage Manager (Storages) with a specified Agile Instance', () => { + const agile = new Agile(); + + const storageManager = StorageIndex.createStorageManager({ + agileInstance: agile, + localStorage: true, + }); + + expect(storageManager).toBeInstanceOf(Storages); + expect(StoragesMock).toHaveBeenCalledWith(agile, { localStorage: true }); + }); + }); + + describe('getStorageManager function tests', () => { + beforeEach(() => { + StorageIndex.assignSharedAgileStorageManager(null); + + jest.spyOn(StorageIndex, 'assignSharedAgileStorageManager'); + jest.spyOn(StorageIndex, 'createStorageManager'); + }); + + it('should return shared Storage Manager', () => { + const createdStorageManager = new Storages(sharedAgileInstance, { + localStorage: false, + }); + StorageIndex.assignSharedAgileStorageManager(createdStorageManager); + jest.clearAllMocks(); + + const returnedStorageManager = StorageIndex.getStorageManager(); + + expect(returnedStorageManager).toBeInstanceOf(Storages); + expect(returnedStorageManager).toBe(createdStorageManager); + expect(StorageIndex.createStorageManager).not.toHaveBeenCalled(); + expect( + StorageIndex.assignSharedAgileStorageManager + ).not.toHaveBeenCalled(); + }); + + // TODO doesn't work although it should 100% work?! + // it( + // 'should return newly created Storage Manager ' + + // 'if no shared Storage Manager was registered yet', + // () => { + // const createdStorageManager = new Storages(sharedAgileInstance, { + // localStorage: false, + // }); + // jest + // .spyOn(StorageIndex, 'createStorageManager') + // .mockReturnValueOnce(createdStorageManager); + // + // const returnedStorageManager = StorageIndex.getStorageManager(); + // + // expect(returnedStorageManager).toBeInstanceOf(Storages); + // expect(returnedStorageManager).toBe(createdStorageManager); + // expect(StorageIndex.createStorageManager).toHaveBeenCalledWith({ + // localStorage: false, + // }); + // expect( + // StorageIndex.assignSharedAgileStorageManager + // ).toHaveBeenCalledWith(createdStorageManager); + // } + // ); + }); + + describe('assignSharedAgileStorageManager function tests', () => { + it('should assign the specified Storage Manager as shared Storage Manager', () => { + const storageManager = new Storages(sharedAgileInstance); + + StorageIndex.assignSharedAgileStorageManager(storageManager); + + expect(StorageIndex.getStorageManager()).toBe(storageManager); + LogMock.hasNotLoggedCode('11:02:06'); + }); + + it( + 'should assign the specified Storage Manager as shared Storage Manager' + + 'and print warning if a shared Storage Manager is already set', + () => { + const oldStorageManager = new Storages(sharedAgileInstance); + StorageIndex.assignSharedAgileStorageManager(oldStorageManager); + const storageManager = new Storages(sharedAgileInstance); + + StorageIndex.assignSharedAgileStorageManager(storageManager); + + expect(StorageIndex.getStorageManager()).toBe(storageManager); + LogMock.hasLoggedCode('11:02:06', [], oldStorageManager); + } + ); + }); +}); diff --git a/packages/core/tests/unit/storages/persistent.test.ts b/packages/core/tests/unit/storages/persistent.test.ts index a0dd3407..44d09106 100644 --- a/packages/core/tests/unit/storages/persistent.test.ts +++ b/packages/core/tests/unit/storages/persistent.test.ts @@ -1,13 +1,26 @@ -import { Agile, Persistent, Storage, createStorage } from '../../../src'; +import { + Agile, + Persistent, + Storage, + createStorage, + Storages, + assignSharedAgileStorageManager, + createStorageManager, +} from '../../../src'; import { LogMock } from '../../helper/logMock'; describe('Persistent Tests', () => { let dummyAgile: Agile; + let storageManager: Storages; beforeEach(() => { LogMock.mockLogs(); - dummyAgile = new Agile({ localStorage: false }); + dummyAgile = new Agile(); + + // Register Storage Manager + storageManager = createStorageManager(); + assignSharedAgileStorageManager(storageManager); jest.spyOn(Persistent.prototype, 'instantiatePersistent'); @@ -28,9 +41,6 @@ describe('Persistent Tests', () => { key: undefined, defaultStorageKey: null, }); - expect( - dummyAgile.storages.persistentInstances.has(persistent) - ).toBeTruthy(); expect(persistent._key).toBe(Persistent.placeHolderKey); expect(persistent.ready).toBeFalsy(); @@ -58,9 +68,6 @@ describe('Persistent Tests', () => { key: 'persistentKey', defaultStorageKey: 'test1', }); - expect( - dummyAgile.storages.persistentInstances.has(persistent) - ).toBeTruthy(); expect(persistent._key).toBe(Persistent.placeHolderKey); expect(persistent.ready).toBeFalsy(); @@ -80,9 +87,6 @@ describe('Persistent Tests', () => { expect(persistent).toBeInstanceOf(Persistent); expect(persistent.instantiatePersistent).not.toHaveBeenCalled(); - expect( - dummyAgile.storages.persistentInstances.has(persistent) - ).toBeTruthy(); expect(persistent._key).toBe(Persistent.placeHolderKey); expect(persistent.ready).toBeFalsy(); @@ -188,25 +192,59 @@ describe('Persistent Tests', () => { }); describe('instantiatePersistent function tests', () => { - it('should call assign key to formatKey and call assignStorageKeys, validatePersistent', () => { + beforeEach(() => { jest.spyOn(persistent, 'formatKey'); jest.spyOn(persistent, 'assignStorageKeys'); jest.spyOn(persistent, 'validatePersistent'); + }); - persistent.instantiatePersistent({ - key: 'persistentKey', - storageKeys: ['myName', 'is', 'jeff'], - defaultStorageKey: 'jeff', - }); + it( + 'should call formatKey, assignStorageKeys, validatePersistent ' + + 'and add Persistent to the shared Storage Manager if Persistent has a valid key', + () => { + persistent.instantiatePersistent({ + key: 'persistentKey', + storageKeys: ['myName', 'is', 'jeff'], + defaultStorageKey: 'jeff', + }); + + expect(persistent._key).toBe('persistentKey'); + expect(persistent.formatKey).toHaveBeenCalledWith('persistentKey'); + expect(persistent.assignStorageKeys).toHaveBeenCalledWith( + ['myName', 'is', 'jeff'], + 'jeff' + ); + expect(persistent.validatePersistent).toHaveBeenCalled(); + expect(storageManager.persistentInstances).toHaveProperty( + 'persistentKey' + ); + expect(storageManager.persistentInstances['persistentKey']).toBe( + persistent + ); + } + ); - expect(persistent._key).toBe('persistentKey'); - expect(persistent.formatKey).toHaveBeenCalledWith('persistentKey'); - expect(persistent.assignStorageKeys).toHaveBeenCalledWith( - ['myName', 'is', 'jeff'], - 'jeff' - ); - expect(persistent.validatePersistent).toHaveBeenCalled(); - }); + it( + 'should call formatKey, assignStorageKeys, validatePersistent ' + + "and shouldn't add Persistent to the shared Storage Manager if Persistent has no valid key", + () => { + persistent.instantiatePersistent({ + storageKeys: ['myName', 'is', 'jeff'], + defaultStorageKey: 'jeff', + }); + + expect(persistent._key).toBe(Persistent.placeHolderKey); + expect(persistent.formatKey).toHaveBeenCalledWith(undefined); + expect(persistent.assignStorageKeys).toHaveBeenCalledWith( + ['myName', 'is', 'jeff'], + 'jeff' + ); + expect(persistent.validatePersistent).toHaveBeenCalled(); + expect(storageManager.persistentInstances).not.toHaveProperty( + 'persistentKey' + ); + } + ); }); describe('validatePersistent function tests', () => { @@ -262,7 +300,7 @@ describe('Persistent Tests', () => { }); it('should return true if set key and set StorageKeys', () => { - dummyAgile.storages.register( + storageManager.register( createStorage({ key: 'test', methods: { @@ -337,7 +375,7 @@ describe('Persistent Tests', () => { 'should try to get default StorageKey from Agile if no StorageKey was specified ' + 'and assign it as StorageKey, if it is a valid StorageKey', () => { - dummyAgile.storages.register( + storageManager.register( new Storage({ key: 'storage1', methods: { diff --git a/packages/core/tests/unit/storages/storages.test.ts b/packages/core/tests/unit/storages/storages.test.ts index 139a7e29..9f09d002 100644 --- a/packages/core/tests/unit/storages/storages.test.ts +++ b/packages/core/tests/unit/storages/storages.test.ts @@ -1,4 +1,10 @@ -import { Storages, Agile, Storage, Persistent } from '../../../src'; +import { + Storages, + Agile, + Storage, + Persistent, + assignSharedAgileStorageManager, +} from '../../../src'; import { LogMock } from '../../helper/logMock'; describe('Storages Tests', () => { @@ -7,7 +13,7 @@ describe('Storages Tests', () => { beforeEach(() => { LogMock.mockLogs(); - dummyAgile = new Agile({ localStorage: false }); + dummyAgile = new Agile(); jest.spyOn(Storages.prototype, 'instantiateLocalStorage'); @@ -19,7 +25,7 @@ describe('Storages Tests', () => { expect(storages.config).toStrictEqual({ defaultStorageKey: null }); expect(storages.storages).toStrictEqual({}); - expect(storages.persistentInstances.size).toBe(0); + expect(storages.persistentInstances).toStrictEqual({}); expect(storages.instantiateLocalStorage).not.toHaveBeenCalled(); }); @@ -31,7 +37,7 @@ describe('Storages Tests', () => { expect(storages.config).toStrictEqual({ defaultStorageKey: 'jeff' }); expect(storages.storages).toStrictEqual({}); - expect(storages.persistentInstances.size).toBe(0); + expect(storages.persistentInstances).toStrictEqual({}); expect(storages.instantiateLocalStorage).toHaveBeenCalled(); }); @@ -44,7 +50,8 @@ describe('Storages Tests', () => { beforeEach(() => { storages = new Storages(dummyAgile); - dummyAgile.storages = storages; + assignSharedAgileStorageManager(storages); + dummyStorageMethods = { get: jest.fn(), set: jest.fn(), @@ -183,7 +190,7 @@ describe('Storages Tests', () => { expect(response).toBeTruthy(); }); - it('should revalidate and initial load Persistents that have no defined defaultStorage', () => { + it('should revalidate and initial load persistent Instances that have no defined defaultStorage', () => { const dummyPersistent1 = new Persistent(dummyAgile, { key: 'dummyPersistent1', }); diff --git a/packages/core/tests/unit/utils.test.ts b/packages/core/tests/unit/utils.test.ts index 96decb42..46e8aafd 100644 --- a/packages/core/tests/unit/utils.test.ts +++ b/packages/core/tests/unit/utils.test.ts @@ -16,7 +16,7 @@ describe('Utils Tests', () => { beforeEach(() => { LogMock.mockLogs(); - dummyAgile = new Agile({ localStorage: false }); + dummyAgile = new Agile(); // @ts-ignore | Reset globalThis globalThis = {}; @@ -118,13 +118,11 @@ describe('Utils Tests', () => { // State with multiple Observer dummyStateWithMultipleObserver = new State(dummyAgile, null); dummyStateValueObserver = new StateObserver(dummyState); - dummyStateWithMultipleObserver.observers[ - 'value' - ] = dummyStateValueObserver; + dummyStateWithMultipleObserver.observers['value'] = + dummyStateValueObserver; dummyStateRandomObserver = new StateObserver(dummyState); - dummyStateWithMultipleObserver.observers[ - 'random' - ] = dummyStateRandomObserver; + dummyStateWithMultipleObserver.observers['random'] = + dummyStateRandomObserver; // Collection dummyCollection = new Collection(dummyAgile); @@ -216,13 +214,11 @@ describe('Utils Tests', () => { // State with multiple Observer dummyStateWithMultipleObserver = new State(dummyAgile, null); dummyStateValueObserver = new StateObserver(dummyState); - dummyStateWithMultipleObserver.observers[ - 'value' - ] = dummyStateValueObserver; + dummyStateWithMultipleObserver.observers['value'] = + dummyStateValueObserver; dummyStateRandomObserver = new StateObserver(dummyState); - dummyStateWithMultipleObserver.observers[ - 'random' - ] = dummyStateRandomObserver; + dummyStateWithMultipleObserver.observers['random'] = + dummyStateRandomObserver; // Collection dummyCollection = new Collection(dummyAgile); diff --git a/packages/event/src/event.observer.ts b/packages/event/src/event/event.observer.ts similarity index 97% rename from packages/event/src/event.observer.ts rename to packages/event/src/event/event.observer.ts index 97da3216..1baa1303 100644 --- a/packages/event/src/event.observer.ts +++ b/packages/event/src/event/event.observer.ts @@ -4,7 +4,7 @@ import { ObserverKey, SubscriptionContainer, } from '@agile-ts/core'; -import { Event } from './internal'; +import { Event } from '../internal'; export class EventObserver extends Observer { public event: () => Event; diff --git a/packages/event/src/event.job.ts b/packages/event/src/event/event.runtime.job.ts similarity index 90% rename from packages/event/src/event.job.ts rename to packages/event/src/event/event.runtime.job.ts index f192cadd..dc7547a3 100644 --- a/packages/event/src/event.job.ts +++ b/packages/event/src/event/event.runtime.job.ts @@ -1,4 +1,4 @@ -export class EventJob { +export class EventRuntimeJob { public payload: PayloadType; public creationTimestamp: number; public keys?: string[]; diff --git a/packages/event/src/event.ts b/packages/event/src/event/event.ts similarity index 97% rename from packages/event/src/event.ts rename to packages/event/src/event/event.ts index b54660f9..403ff902 100644 --- a/packages/event/src/event.ts +++ b/packages/event/src/event/event.ts @@ -5,7 +5,7 @@ import { LogCodeManager, Observer, } from '@agile-ts/core'; -import { EventObserver, EventJob } from './internal'; +import { EventObserver, EventRuntimeJob } from '../internal'; import { defineConfig } from '@agile-ts/utils'; export class Event { @@ -21,7 +21,7 @@ export class Event { public observer: EventObserver; public currentTimeout: any; // Timeout that is active right now (delayed Event) - public queue: Array = []; // Queue of delayed Events + public queue: Array = []; // Queue of delayed Events // @ts-ignore public payload: PayloadType; // Holds type of Payload so that it can be read external (never defined) @@ -251,7 +251,7 @@ export class Event { * @param keys - Keys of Callback Functions that get triggered (Note: if not passed all registered Events will be triggered) */ public delayedTrigger(payload: PayloadType, delay: number, keys?: string[]) { - const eventJob = new EventJob(payload, keys); + const eventJob = new EventRuntimeJob(payload, keys); // Execute Event no matter if another event is currently active if (this.config.overlap) { @@ -268,7 +268,7 @@ export class Event { } // Executes EventJob and calls itself again if queue isn't empty to execute the next EventJob - const looper = (eventJob: EventJob) => { + const looper = (eventJob: EventRuntimeJob) => { this.currentTimeout = setTimeout(() => { this.currentTimeout = undefined; this.normalTrigger(eventJob.payload, eventJob.keys); diff --git a/packages/event/src/shared.ts b/packages/event/src/event/index.ts similarity index 67% rename from packages/event/src/shared.ts rename to packages/event/src/event/index.ts index fae03ce1..ebd8344f 100644 --- a/packages/event/src/shared.ts +++ b/packages/event/src/event/index.ts @@ -1,14 +1,14 @@ import { - CreateAgileSubInstanceInterface, - removeProperties, - shared, - defineConfig, -} from '@agile-ts/core'; -import { - Event, CreateEventConfigInterface, DefaultEventPayload, -} from './internal'; + Event, +} from './event'; +import { defineConfig, removeProperties } from '@agile-ts/utils'; +import { CreateAgileSubInstanceInterface, shared } from '@agile-ts/core'; + +export * from './event'; +// export * from './event.observer'; +// export * from './event.job'; export function createEvent( config: CreateEventConfigInterfaceWithAgile = {} diff --git a/packages/event/src/index.ts b/packages/event/src/index.ts index f3396fb7..3cb08d20 100644 --- a/packages/event/src/index.ts +++ b/packages/event/src/index.ts @@ -1,5 +1,4 @@ import { Event } from './internal'; export * from './internal'; -export { useEvent } from './hooks/useEvent'; export default Event; diff --git a/packages/event/src/internal.ts b/packages/event/src/internal.ts index 695b0fbb..758f4fe4 100644 --- a/packages/event/src/internal.ts +++ b/packages/event/src/internal.ts @@ -5,9 +5,6 @@ // !! All internal Agile Editor modules must be imported from here!! // Event -export * from './event.job'; -export * from './event.observer'; +export * from './event/event.runtime.job'; +export * from './event/event.observer'; export * from './event'; - -// Shared -export * from './shared'; diff --git a/packages/event/src/hooks/useEvent.ts b/packages/event/src/react/hooks/useEvent.ts similarity index 95% rename from packages/event/src/hooks/useEvent.ts rename to packages/event/src/react/hooks/useEvent.ts index 5a1d3168..3ef41a70 100644 --- a/packages/event/src/hooks/useEvent.ts +++ b/packages/event/src/react/hooks/useEvent.ts @@ -5,7 +5,7 @@ import { LogCodeManager, SubscriptionContainerKeyType, } from '@agile-ts/core'; -import { Event, EventCallbackFunction } from '../internal'; +import { Event, EventCallbackFunction } from '../../internal'; import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'; export function useEvent>( diff --git a/packages/event/src/hooks/useIsomorphicLayoutEffect.ts b/packages/event/src/react/hooks/useIsomorphicLayoutEffect.ts similarity index 100% rename from packages/event/src/hooks/useIsomorphicLayoutEffect.ts rename to packages/event/src/react/hooks/useIsomorphicLayoutEffect.ts diff --git a/packages/event/src/react/index.ts b/packages/event/src/react/index.ts new file mode 100644 index 00000000..1501534e --- /dev/null +++ b/packages/event/src/react/index.ts @@ -0,0 +1 @@ +export { useEvent } from './hooks/useEvent'; diff --git a/packages/event/tests/unit/event.job.test.ts b/packages/event/tests/unit/event/event.job.test.ts similarity index 74% rename from packages/event/tests/unit/event.job.test.ts rename to packages/event/tests/unit/event/event.job.test.ts index 90397943..0e67edda 100644 --- a/packages/event/tests/unit/event.job.test.ts +++ b/packages/event/tests/unit/event/event.job.test.ts @@ -1,5 +1,5 @@ -import { EventJob } from '../../src'; -import { LogMock } from '../../../core/tests/helper/logMock'; +import { EventRuntimeJob } from '../../../src'; +import { LogMock } from '../../../../core/tests/helper/logMock'; describe('EventJob Tests', () => { beforeEach(() => { @@ -8,7 +8,7 @@ describe('EventJob Tests', () => { }); it('should create EventJob (without keys)', () => { - const eventJob = new EventJob('myPayload'); + const eventJob = new EventRuntimeJob('myPayload'); expect(eventJob.payload).toBe('myPayload'); expect(eventJob.creationTimestamp).toBeCloseTo( @@ -19,7 +19,10 @@ describe('EventJob Tests', () => { }); it('should create EventJob (with keys)', () => { - const eventJob = new EventJob('myPayload', ['dummyKey1', 'dummyKey2']); + const eventJob = new EventRuntimeJob('myPayload', [ + 'dummyKey1', + 'dummyKey2', + ]); expect(eventJob.payload).toBe('myPayload'); expect(eventJob.creationTimestamp).toBeCloseTo( diff --git a/packages/event/tests/unit/event.observer.test.ts b/packages/event/tests/unit/event/event.observer.test.ts similarity index 92% rename from packages/event/tests/unit/event.observer.test.ts rename to packages/event/tests/unit/event/event.observer.test.ts index b700a72f..2ee5d487 100644 --- a/packages/event/tests/unit/event.observer.test.ts +++ b/packages/event/tests/unit/event/event.observer.test.ts @@ -1,6 +1,6 @@ -import { EventObserver, Event } from '../../src'; +import { EventObserver, Event } from '../../../src'; import { Agile, Observer, SubscriptionContainer } from '@agile-ts/core'; -import { LogMock } from '../../../core/tests/helper/logMock'; +import { LogMock } from '../../../../core/tests/helper/logMock'; describe('EventObserver Tests', () => { let dummyAgile: Agile; @@ -9,7 +9,7 @@ describe('EventObserver Tests', () => { beforeEach(() => { LogMock.mockLogs(); - dummyAgile = new Agile({ localStorage: false }); + dummyAgile = new Agile(); dummyEvent = new Event(dummyAgile); jest.clearAllMocks(); diff --git a/packages/event/tests/unit/event.test.ts b/packages/event/tests/unit/event/event.test.ts similarity index 98% rename from packages/event/tests/unit/event.test.ts rename to packages/event/tests/unit/event/event.test.ts index bd369952..a719ce06 100644 --- a/packages/event/tests/unit/event.test.ts +++ b/packages/event/tests/unit/event/event.test.ts @@ -1,7 +1,7 @@ -import { Event, EventObserver } from '../../src'; +import { Event, EventObserver } from '../../../src'; import { Agile, Observer } from '@agile-ts/core'; import * as Utils from '@agile-ts/utils'; -import { LogMock } from '../../../core/tests/helper/logMock'; +import { LogMock } from '../../../../core/tests/helper/logMock'; describe('Event Tests', () => { let dummyAgile: Agile; @@ -9,7 +9,7 @@ describe('Event Tests', () => { beforeEach(() => { LogMock.mockLogs(); - dummyAgile = new Agile({ localStorage: false }); + dummyAgile = new Agile(); jest.clearAllMocks(); }); diff --git a/packages/event/tests/unit/shared.test.ts b/packages/event/tests/unit/event/index.test.ts similarity index 88% rename from packages/event/tests/unit/shared.test.ts rename to packages/event/tests/unit/event/index.test.ts index 7b5719a0..9d8a6b6a 100644 --- a/packages/event/tests/unit/shared.test.ts +++ b/packages/event/tests/unit/event/index.test.ts @@ -1,8 +1,8 @@ import { Agile, assignSharedAgileInstance } from '@agile-ts/core'; -import { Event, createEvent } from '../../src'; -import { LogMock } from '../../../core/tests/helper/logMock'; +import { Event, createEvent } from '../../../src'; +import { LogMock } from '../../../../core/tests/helper/logMock'; -jest.mock('../../src/event'); +jest.mock('../../../src/event/event'); describe('Shared Tests', () => { let sharedAgileInstance: Agile; diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts index 00aa3e3e..a76a7efc 100644 --- a/packages/logger/src/index.ts +++ b/packages/logger/src/index.ts @@ -1,6 +1,9 @@ import { CreateLoggerConfigInterface, Logger } from './logger'; import { defineConfig } from '@agile-ts/utils'; +export * from './logger'; +export default Logger; + const defaultLogConfig = { prefix: 'Agile', active: true, @@ -9,9 +12,6 @@ const defaultLogConfig = { allowedTags: ['runtime', 'storage', 'subscription', 'multieditor'], }; -/** - * Shared Agile Logger. - */ let sharedAgileLogger = new Logger(defaultLogConfig); /** @@ -19,8 +19,7 @@ let sharedAgileLogger = new Logger(defaultLogConfig); * * @param config - Configuration object */ -// https://stackoverflow.com/questions/32558514/javascript-es6-export-const-vs-export-let -function assignSharedAgileLoggerConfig( +export function assignSharedAgileLoggerConfig( config: CreateLoggerConfigInterface = {} ): Logger { config = defineConfig(config, defaultLogConfig); @@ -28,6 +27,9 @@ function assignSharedAgileLoggerConfig( return sharedAgileLogger; } -export { sharedAgileLogger, assignSharedAgileLoggerConfig }; -export * from './logger'; -export default Logger; +/** + * Returns the shared Agile Logger. + */ +export function getLogger(): Logger { + return sharedAgileLogger; +} diff --git a/packages/multieditor/src/item.ts b/packages/multieditor/src/item.ts index b2199b4b..32493150 100644 --- a/packages/multieditor/src/item.ts +++ b/packages/multieditor/src/item.ts @@ -1,11 +1,11 @@ import { - State, StateRuntimeJobConfigInterface, defineConfig, + EnhancedState, } from '@agile-ts/core'; import { MultiEditor, Validator, Status, ItemKey } from './internal'; -export class Item extends State { +export class Item extends EnhancedState { public editor: () => MultiEditor; public isValid = false; diff --git a/packages/multieditor/src/multieditor/index.ts b/packages/multieditor/src/multieditor/index.ts new file mode 100644 index 00000000..5353b95c --- /dev/null +++ b/packages/multieditor/src/multieditor/index.ts @@ -0,0 +1,22 @@ +import { defineConfig } from '@agile-ts/utils'; +import { Agile, shared } from '@agile-ts/core'; +import { EditorConfig, MultiEditor } from '../internal'; + +export * from './multieditor'; + +export function createMultieditor< + DataType = any, + SubmitReturnType = void, + OnSubmitConfigType = any +>( + config: EditorConfig, + agileInstance: Agile = shared +): MultiEditor { + config = defineConfig(config, { + agileInstance: shared, + }); + return new MultiEditor( + config, + agileInstance as any + ); +} diff --git a/packages/multieditor/src/multieditor.ts b/packages/multieditor/src/multieditor/multieditor.ts similarity index 99% rename from packages/multieditor/src/multieditor.ts rename to packages/multieditor/src/multieditor/multieditor.ts index a686a6c0..3e000768 100644 --- a/packages/multieditor/src/multieditor.ts +++ b/packages/multieditor/src/multieditor/multieditor.ts @@ -13,7 +13,7 @@ import { StatusType, StatusInterface, ValidationMethodInterface, -} from './internal'; +} from '../internal'; export class MultiEditor< DataType = any, diff --git a/packages/react/package.json b/packages/react/package.json index 20eb51d1..2e03a774 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@agile-ts/react", - "version": "0.1.2", + "version": "0.2.0-alpha.1", "author": "BennoDev", "license": "MIT", "homepage": "https://agile-ts.org/", diff --git a/packages/react/src/hocs/AgileHOC.ts b/packages/react/src/hocs/AgileHOC.ts index 99e161d2..01b787ed 100644 --- a/packages/react/src/hocs/AgileHOC.ts +++ b/packages/react/src/hocs/AgileHOC.ts @@ -8,9 +8,9 @@ import Agile, { flatMerge, extractRelevantObservers, normalizeArray, - LogCodeManager, } from '@agile-ts/core'; import type { Collection } from '@agile-ts/core'; // Only import Collection and Group type for better Treeshaking +import { LogCodeManager } from '../logCodeManager'; /** * A Higher order Component for binding the most relevant value of multiple Agile Instances @@ -57,10 +57,7 @@ export function AgileHOC( } } if (!agileInstance || !agileInstance.subController) { - LogCodeManager.getLogger()?.error( - 'Failed to subscribe Component with deps', - deps - ); + LogCodeManager.log('32:03:00', [deps]); return reactComponent; } @@ -93,9 +90,8 @@ const createHOC = ( public agileInstance: Agile; public waitForMount: boolean; - public componentSubscriptionContainers: Array< - ComponentSubscriptionContainer - > = []; // Represents all Subscription Container subscribed to this Component (set by subController) + public componentSubscriptionContainers: Array = + []; // Represents all Subscription Container subscribed to this Component (set by subController) public agileProps = {}; // Props of subscribed Agile Instances (are merged into the normal props) constructor(props: any) { @@ -235,9 +231,8 @@ const formatDepsWithIndicator = ( export class AgileReactComponent extends React.Component { // @ts-ignore public agileInstance: Agile; - public componentSubscriptionContainers: Array< - ComponentSubscriptionContainer - > = []; + public componentSubscriptionContainers: Array = + []; public agileProps = {}; constructor(props: any) { diff --git a/packages/react/src/hooks/useAgile.ts b/packages/react/src/hooks/useAgile.ts index 56d51419..15d33408 100644 --- a/packages/react/src/hooks/useAgile.ts +++ b/packages/react/src/hooks/useAgile.ts @@ -1,30 +1,18 @@ -import React from 'react'; -import Agile, { - getAgileInstance, +import { Observer, State, - SubscriptionContainerKeyType, - isValidObject, generateId, - ProxyWeakMapType, - ComponentIdType, extractRelevantObservers, - SelectorWeakMapType, - SelectorMethodType, - LogCodeManager, normalizeArray, defineConfig, } from '@agile-ts/core'; import type { Collection, Group } from '@agile-ts/core'; // Only import Collection and Group type for better Treeshaking -import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'; - -// TODO https://stackoverflow.com/questions/68148235/require-module-inside-a-function-doesnt-work -let proxyPackage: any = null; -try { - proxyPackage = require('@agile-ts/proxytree'); -} catch (e) { - // empty catch block -} +import { + BaseAgileHookConfigInterface, + getReturnValue, + SubscribableAgileInstancesType, + useBaseAgile, +} from './useBaseAgile'; /** * A React Hook for binding the most relevant value of multiple Agile Instances @@ -67,7 +55,6 @@ export function useAgile< ): AgileOutputHookArrayType | AgileOutputHookType { config = defineConfig(config, { key: generateId(), - proxyBased: false, agileInstance: null as any, componentId: undefined, observerType: undefined, @@ -77,180 +64,26 @@ export function useAgile< normalizeArray(deps), config.observerType ); - const proxyTreeWeakMap = new WeakMap(); - - // Builds return value, - // depending on whether the deps were provided in array shape or not - const getReturnValue = ( - depsArray: (Observer | undefined)[] - ): AgileOutputHookArrayType | AgileOutputHookType => { - const handleReturn = ( - dep: Observer | undefined - ): AgileOutputHookType => { - if (dep == null) return undefined as any; - const value = dep.value; - - // If proxyBased and the value is of the type object. - // Wrap a Proxy around the object to track the accessed properties. - if (config.proxyBased && isValidObject(value, true)) { - if (proxyPackage != null) { - const { ProxyTree } = proxyPackage; - const proxyTree = new ProxyTree(value); - proxyTreeWeakMap.set(dep, proxyTree); - return proxyTree.proxy; - } else { - console.error( - 'In order to use the Agile proxy functionality, ' + - `the installation of an additional package called '@agile-ts/proxytree' is required!` - ); - } - } - - // If specified selector function and the value is of type object. - // Return the selected value. - // (Destroys the type of the useAgile hook, - // however the type can be adjusted in the useSelector hook) - if (config.selector && isValidObject(value, true)) { - return config.selector(value); - } - return value; - }; - - // Handle single dep return value - if (depsArray.length === 1 && !Array.isArray(deps)) { - return handleReturn(depsArray[0]); - } - - // Handle deps array return value - return depsArray.map((dep) => { - return handleReturn(dep); - }) as AgileOutputHookArrayType; + const handleReturn = (dep: Observer | undefined) => { + return dep != null ? dep.value : undefined; }; - // Trigger State, used to force Component to rerender - const [, forceRender] = React.useReducer((s) => s + 1, 0); - - useIsomorphicLayoutEffect(() => { - let agileInstance = config.agileInstance; - - // https://github.com/microsoft/TypeScript/issues/20812 - const observers: Observer[] = depsArray.filter( - (dep): dep is Observer => dep !== undefined - ); - - // Try to extract Agile Instance from the specified Instance/s - if (!agileInstance) agileInstance = getAgileInstance(observers[0]); - if (!agileInstance || !agileInstance.subController) { - LogCodeManager.getLogger()?.error( - 'Failed to subscribe Component with deps because of missing valid Agile Instance.', - deps - ); - return; - } - - // TODO Proxy doesn't work as expected when 'selecting' a not yet existing property. - // For example you select the 'user.data.name' property, but the 'user' object is undefined. - // -> No correct Proxy Path could be created on the Component mount, since the to select property doesn't exist - // -> Selector was created based on the not complete Proxy Path - // -> Component re-renders to often - // - // Build Proxy Path WeakMap based on the Proxy Tree WeakMap - // by extracting the routes from the Proxy Tree. - // Building the Path WeakMap in the 'useIsomorphicLayoutEffect' - // because the 'useIsomorphicLayoutEffect' is called after the rerender. - // -> All used paths in the UI-Component were successfully tracked. - let proxyWeakMap: ProxyWeakMapType | undefined = undefined; - if (config.proxyBased && proxyPackage != null) { - proxyWeakMap = new WeakMap(); - for (const observer of observers) { - const proxyTree = proxyTreeWeakMap.get(observer); - if (proxyTree != null) { - proxyWeakMap.set(observer, { - paths: proxyTree.getUsedRoutes() as any, - }); - } - } - } - - // Build Selector WeakMap based on the specified selector method - let selectorWeakMap: SelectorWeakMapType | undefined = undefined; - if (config.selector != null) { - selectorWeakMap = new WeakMap(); - for (const observer of observers) { - selectorWeakMap.set(observer, { methods: [config.selector] }); - } - } - - // Create Callback based Subscription - const subscriptionContainer = agileInstance.subController.subscribe( - () => { - forceRender(); - }, - observers, - { - key: config.key, - proxyWeakMap, - waitForMount: false, - componentId: config.componentId, - selectorWeakMap, - } - ); - - // Unsubscribe Callback based Subscription on unmount - return () => { - agileInstance?.subController.unsubscribe(subscriptionContainer); - }; - }, config.deps); + useBaseAgile( + depsArray, + () => ({ + key: config.key, + waitForMount: false, + componentId: config.componentId, + }), + config.deps || [], + config.agileInstance + ); - return getReturnValue(depsArray); + return getReturnValue(depsArray, handleReturn, Array.isArray(deps)); } -export type SubscribableAgileInstancesType = - | State - | Collection //https://stackoverflow.com/questions/66987727/type-classa-id-number-name-string-is-not-assignable-to-type-classar - | Observer - | undefined; - -export interface AgileHookConfigInterface { - /** - * Key/Name identifier of the Subscription Container to be created. - * @default undefined - */ - key?: SubscriptionContainerKeyType; - /** - * Instance of Agile the Subscription Container belongs to. - * @default `undefined` if no Agile Instance could be extracted from the provided Instances. - */ - agileInstance?: Agile; - /** - * Whether to wrap a [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) - * around the bound Agile Instance value object, - * to automatically constrain the way the selected Agile Instance - * is compared to determine whether the Component needs to be re-rendered - * based on the object's used properties. - * - * Requires an additional package called `@agile-ts/proxytree`! - * - * @default false - */ - proxyBased?: boolean; - /** - * Equality comparison function - * that allows you to customize the way the selected Agile Instance - * is compared to determine whether the Component needs to be re-rendered. - * - * * Note that setting this property can destroy the useAgile type. - * -> should only be used internal! - * - * @default undefined - */ - selector?: SelectorMethodType; - /** - * Key/Name identifier of the UI-Component the Subscription Container is bound to. - * @default undefined - */ - componentId?: ComponentIdType; +export interface AgileHookConfigInterface extends BaseAgileHookConfigInterface { /** * What type of Observer to be bound to the UI-Component. * @@ -260,15 +93,6 @@ export interface AgileHookConfigInterface { * @default undefined */ observerType?: string; - /** - * Dependencies that determine, in addition to unmounting and remounting the React-Component, - * when the specified Agile Sub Instances should be re-subscribed to the React-Component. - * - * [Github issue](https://github.com/agile-ts/agile/issues/170) - * - * @default [] - */ - deps?: any[]; } // Array Type diff --git a/packages/react/src/hooks/useBaseAgile.ts b/packages/react/src/hooks/useBaseAgile.ts new file mode 100644 index 00000000..b566b756 --- /dev/null +++ b/packages/react/src/hooks/useBaseAgile.ts @@ -0,0 +1,125 @@ +import React from 'react'; +import Agile, { + Collection, + ComponentIdType, + getAgileInstance, + Observer, + State, + SubscriptionContainerKeyType, + RegisterSubscriptionConfigInterface, +} from '@agile-ts/core'; +import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'; +import { LogCodeManager } from '../logCodeManager'; + +/** + * An internal used React Hook + * to create a Callback based Subscription Container + * based on the specified depsArray + * and thus bind these dependencies to a Functional React Component. + * + * @internal + * @param depsArray - Observers to be bound to the Functional Component. + * @param getSubContainerConfig - Method to get the Subscription Container configuration object. + * @param deps - Dependencies that determine, in addition to unmounting and remounting the React-Component, + * when the specified Agile Sub Instances should be re-subscribed to the React-Component. + * @param agileInstance - Agile Instance the to create Subscription Container belongs to. + */ +export const useBaseAgile = ( + depsArray: (Observer | undefined)[], + getSubContainerConfig: ( + observers: Observer[] + ) => RegisterSubscriptionConfigInterface, + deps: any[], + agileInstance?: Agile +) => { + // Trigger State, used to force Component to rerender + const [, forceRender] = React.useReducer((s) => s + 1, 0); + + useIsomorphicLayoutEffect(() => { + // https://github.com/microsoft/TypeScript/issues/20812 + const observers = depsArray.filter( + (dep): dep is Observer => dep !== undefined + ); + + const subContainerConfig = getSubContainerConfig(observers); + + // Try to extract Agile Instance from the specified Instance/s + if (agileInstance == null) agileInstance = getAgileInstance(observers[0]); + if (agileInstance == null || agileInstance.subController == null) { + LogCodeManager.log('30:03:00', deps); + return; + } + + // Create Callback based Subscription + const subscriptionContainer = agileInstance.subController.subscribe( + () => { + forceRender(); + }, + observers, + subContainerConfig + ); + + // Unsubscribe Callback based Subscription on unmount + return () => { + agileInstance?.subController.unsubscribe(subscriptionContainer); + }; + }, deps); +}; + +/** + * Builds return value for Agile Instance 'binding' Hooks, + * depending on whether the dependencies were provided in array shape or not. + * + * @internal + * @param depsArray - Dependencies to extract the return value from. + * @param handleReturn - Method to handle the return value. + * @param wasProvidedAsArray - Whether the specified depsArray was provided as array in the Hook. + */ +export const getReturnValue = ( + depsArray: (Observer | undefined)[], + handleReturn: (dep: Observer | undefined) => any, + wasProvidedAsArray: boolean +): any => { + // Handle single dep return value + if (depsArray.length === 1 && !wasProvidedAsArray) { + return handleReturn(depsArray[0]); + } + + // Handle deps array return value + return depsArray.map((dep) => { + return handleReturn(dep); + }); +}; + +export type SubscribableAgileInstancesType = + | State + | Collection //https://stackoverflow.com/questions/66987727/type-classa-id-number-name-string-is-not-assignable-to-type-classar + | Observer + | undefined; + +export interface BaseAgileHookConfigInterface { + /** + * Key/Name identifier of the Subscription Container to be created. + * @default undefined + */ + key?: SubscriptionContainerKeyType; + /** + * Instance of Agile the Subscription Container belongs to. + * @default `undefined` if no Agile Instance could be extracted from the provided Instances. + */ + agileInstance?: Agile; + /** + * Key/Name identifier of the UI-Component the Subscription Container is bound to. + * @default undefined + */ + componentId?: ComponentIdType; + /** + * Dependencies that determine, in addition to unmounting and remounting the React-Component, + * when the specified Agile Sub Instances should be re-subscribed to the React-Component. + * + * [Github issue](https://github.com/agile-ts/agile/issues/170) + * + * @default [] + */ + deps?: any[]; +} diff --git a/packages/react/src/hooks/useProxy.ts b/packages/react/src/hooks/useProxy.ts index e565e3a7..02b100ef 100644 --- a/packages/react/src/hooks/useProxy.ts +++ b/packages/react/src/hooks/useProxy.ts @@ -1,17 +1,72 @@ import { - AgileHookConfigInterface, + defineConfig, + extractRelevantObservers, + Observer, + ProxyWeakMapType, +} from '@agile-ts/core'; +import { generateId, isValidObject, normalizeArray } from '@agile-ts/utils'; +import { + getReturnValue, SubscribableAgileInstancesType, - useAgile, + useBaseAgile, +} from './useBaseAgile'; +import { + AgileHookConfigInterface, AgileOutputHookArrayType, AgileOutputHookType, } from './useAgile'; -import { defineConfig } from '@agile-ts/core'; +import { LogCodeManager } from '../logCodeManager'; +// TODO https://stackoverflow.com/questions/68148235/require-module-inside-a-function-doesnt-work +let proxyPackage: any = null; +try { + proxyPackage = require('@agile-ts/proxytree'); +} catch (e) { + // empty catch block +} + +/** + * A React Hook for binding the most relevant value of multiple Agile Instances + * (like the Collection's output or the State's value) + * to a React Functional Component. + * + * This binding ensures that the Component re-renders + * whenever the most relevant Observer of an Agile Instance mutates. + * + * In addition the the default 'useAgile' Hook, + * the useProxy Hooks wraps a [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) + * around the to bind Agile Instance value objects, + * to automatically constraint the way the selected Agile Instances + * are compared to determine whether the React Component needs to be re-rendered + * based on the object's used properties. + * + * @public + * @param deps - Agile Sub Instances to be bound to the Functional Component. + * @param config - Configuration object + */ export function useProxy>( deps: X | [], config?: AgileHookConfigInterface ): AgileOutputHookArrayType; - +/** + * A React Hook for binding the most relevant Agile Instance value + * (like the Collection's output or the State's value) + * to a React Functional Component. + * + * This binding ensures that the Component re-renders + * whenever the most relevant Observer of the Agile Instance mutates. + * + * In addition the the default 'useAgile' Hook, + * the useProxy Hooks wraps a [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) + * around the to bind Agile Instance value objects, + * to automatically constraint the way the selected Agile Instances + * are compared to determine whether the React Component needs to be re-rendered + * based on the object's used properties. + * + * @public + * @param dep - Agile Sub Instance to be bound to the Functional Component. + * @param config - Configuration object + */ export function useProxy( dep: X, config?: AgileHookConfigInterface @@ -24,10 +79,72 @@ export function useProxy< deps: X | Y, config: AgileHookConfigInterface = {} ): AgileOutputHookArrayType | AgileOutputHookType { - return useAgile( - deps as any, - defineConfig(config, { - proxyBased: true, - }) + config = defineConfig(config, { + key: generateId(), + agileInstance: null as any, + componentId: undefined, + deps: [], + }); + const depsArray = extractRelevantObservers(normalizeArray(deps)); + const proxyTreeWeakMap = new WeakMap(); + + const handleReturn = (dep: Observer | undefined) => { + if (dep == null) return undefined as any; + const value = dep.value; + + // If proxyBased and the value is of the type object. + // Wrap a Proxy around the object to track the accessed properties. + if (isValidObject(value, true)) { + if (proxyPackage != null) { + const { ProxyTree } = proxyPackage; + const proxyTree = new ProxyTree(value); + proxyTreeWeakMap.set(dep, proxyTree); + return proxyTree.proxy; + } else { + LogCodeManager.log('31:03:00'); + } + } + + return value; + }; + + useBaseAgile( + depsArray, + (observers) => { + // TODO Proxy doesn't work as expected when 'selecting' a not yet existing property. + // For example you select the 'user.data.name' property, but the 'user' object is undefined. + // -> No correct Proxy Path could be created on the Component mount, since the to select property doesn't exist + // -> Selector was created based on the not complete Proxy Path + // -> Component re-renders to often + // + // Build Proxy Path WeakMap based on the Proxy Tree WeakMap + // by extracting the routes from the Proxy Tree. + // Building the Path WeakMap in the 'useIsomorphicLayoutEffect' + // because the 'useIsomorphicLayoutEffect' is called after the rerender. + // -> All used paths in the UI-Component were successfully tracked. + let proxyWeakMap: ProxyWeakMapType | undefined = undefined; + if (proxyPackage != null) { + proxyWeakMap = new WeakMap(); + for (const observer of observers) { + const proxyTree = proxyTreeWeakMap.get(observer); + if (proxyTree != null) { + proxyWeakMap.set(observer, { + paths: proxyTree.getUsedRoutes() as any, + }); + } + } + } + + return { + key: config.key, + waitForMount: false, + componentId: config.componentId, + proxyWeakMap, + }; + }, + config.deps || [], + config.agileInstance ); + + return getReturnValue(depsArray, handleReturn, Array.isArray(deps)); } diff --git a/packages/react/src/hooks/useSelector.ts b/packages/react/src/hooks/useSelector.ts index 1d694065..043df0c2 100644 --- a/packages/react/src/hooks/useSelector.ts +++ b/packages/react/src/hooks/useSelector.ts @@ -1,25 +1,62 @@ import { - AgileHookConfigInterface, + SelectorMethodType, + defineConfig, + Observer, + SelectorWeakMapType, + extractRelevantObservers, +} from '@agile-ts/core'; +import { generateId, isValidObject } from '@agile-ts/utils'; +import { + BaseAgileHookConfigInterface, + getReturnValue, SubscribableAgileInstancesType, - useAgile, -} from './useAgile'; -import { SelectorMethodType, defineConfig } from '@agile-ts/core'; + useBaseAgile, +} from './useBaseAgile'; import { AgileValueHookType } from './useValue'; +/** + * A React Hook for binding a selected value of an Agile Instance + * (like the Collection's output or the State's value) + * to a React Functional Component. + * + * This binding ensures that the Component re-renders + * whenever the selected value of an Agile Instance mutates. + * + * @public + * @param dep - Agile Sub Instance to be bound to the Functional Component. + * @param selectorMethod - Equality comparison function. + * that allows you to customize the way the selected Agile Instance + * is compared to determine whether the Component needs to be re-rendered. + * @param config - Configuration object + */ export function useSelector< ReturnType, X extends SubscribableAgileInstancesType, ValueType extends AgileValueHookType >( dep: X, - selector: SelectorMethodType, - config?: AgileHookConfigInterface + selectorMethod: SelectorMethodType, + config?: BaseAgileHookConfigInterface ): ReturnType; - +/** + * A React Hook for binding a selected value of an Agile Instance + * (like the Collection's output or the State's value) + * to a React Functional Component. + * + * This binding ensures that the Component re-renders + * whenever the selected value of an Agile Instance mutates. + * + * @public + * @param dep - Agile Sub Instance to be bound to the Functional Component. + * @param selectorMethod - Equality comparison function. + * that allows you to customize the way the selected Agile Instance + * is compared to determine whether the Component needs to be re-rendered. + * @param config - Configuration object + */ export function useSelector( dep: SubscribableAgileInstancesType, - selector: SelectorMethodType, - config?: AgileHookConfigInterface + selectorMethod: SelectorMethodType, + config?: BaseAgileHookConfigInterface ): ReturnType; export function useSelector< @@ -28,13 +65,52 @@ export function useSelector< ReturnType = any >( dep: X, - selector: SelectorMethodType, - config: AgileHookConfigInterface = {} + selectorMethod: SelectorMethodType, + config: BaseAgileHookConfigInterface = {} ): ReturnType { - return useAgile( - dep as any, - defineConfig(config, { - selector: selector, - }) - ) as any; + config = defineConfig(config, { + key: generateId(), + agileInstance: null as any, + componentId: undefined, + deps: [], + }); + const depsArray = extractRelevantObservers([dep]); + + const handleReturn = (dep: Observer | undefined): any => { + if (dep == null) return undefined as any; + const value = dep.value; + + // If specified selector function and the value is of type object. + // Return the selected value. + // (Destroys the type of the useAgile hook, + // however the type can be adjusted in the useSelector hook) + if (isValidObject(value, true)) { + return selectorMethod(value); + } + + return value; + }; + + useBaseAgile( + depsArray, + (observers) => { + // Build Selector WeakMap based on the specified selector method + let selectorWeakMap: SelectorWeakMapType | undefined = undefined; + selectorWeakMap = new WeakMap(); + for (const observer of observers) { + selectorWeakMap.set(observer, { methods: [selectorMethod] }); + } + + return { + key: config.key, + waitForMount: false, + componentId: config.componentId, + selectorWeakMap, + }; + }, + config.deps || [], + config.agileInstance + ); + + return getReturnValue(depsArray, handleReturn, false); } diff --git a/packages/react/src/hooks/useValue.ts b/packages/react/src/hooks/useValue.ts index d1e50ad6..46670532 100644 --- a/packages/react/src/hooks/useValue.ts +++ b/packages/react/src/hooks/useValue.ts @@ -5,20 +5,20 @@ import { State, defineConfig, } from '@agile-ts/core'; +import { useAgile } from './useAgile'; import { - AgileHookConfigInterface, + BaseAgileHookConfigInterface, SubscribableAgileInstancesType, - useAgile, -} from './useAgile'; +} from './useBaseAgile'; export function useValue>( deps: X | [], - config?: AgileHookConfigInterface + config?: BaseAgileHookConfigInterface ): AgileValueHookArrayType; export function useValue( dep: X, - config?: AgileHookConfigInterface + config?: BaseAgileHookConfigInterface ): AgileValueHookType; export function useValue< @@ -26,7 +26,7 @@ export function useValue< Y extends SubscribableAgileInstancesType >( deps: X | Y, - config: AgileHookConfigInterface = {} + config: BaseAgileHookConfigInterface = {} ): AgileValueHookArrayType | AgileValueHookType { return useAgile( deps as any, diff --git a/packages/react/src/hooks/useWatcher.ts b/packages/react/src/hooks/useWatcher.ts index 39fb3238..e0cb08b8 100644 --- a/packages/react/src/hooks/useWatcher.ts +++ b/packages/react/src/hooks/useWatcher.ts @@ -1,8 +1,8 @@ import React from 'react'; -import { StateWatcherCallback, State } from '@agile-ts/core'; +import { StateWatcherCallback, EnhancedState } from '@agile-ts/core'; export function useWatcher( - state: State, + state: EnhancedState, callback: StateWatcherCallback ): void { React.useEffect(() => { diff --git a/packages/react/src/logCodeManager.ts b/packages/react/src/logCodeManager.ts new file mode 100644 index 00000000..b3601ba2 --- /dev/null +++ b/packages/react/src/logCodeManager.ts @@ -0,0 +1,27 @@ +import { + LogCodeManager as CoreLogCodeManager, + assignAdditionalLogs, +} from '@agile-ts/core'; + +const additionalLogs = { + '30:03:00': + 'Failed to subscribe Component with deps because of missing valid Agile Instance.', + '31:03:00': + "In order to use the Agile proxy functionality, the installation of an additional package called '@agile-ts/proxytree' is required!", + '32:03:00': 'Failed to subscribe Component with deps', +}; + +/** + * The Log Code Manager keeps track + * and manages all important Logs for the '@agile-ts/react' package. + * + * @internal + */ +export const LogCodeManager = + typeof process === 'object' && process.env.NODE_ENV !== 'production' + ? assignAdditionalLogs< + typeof CoreLogCodeManager.logCodeMessages & typeof additionalLogs + >(additionalLogs, CoreLogCodeManager) + : assignAdditionalLogs< + typeof CoreLogCodeManager.logCodeMessages & typeof additionalLogs + >({}, CoreLogCodeManager); diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 6161059c..ffef55ed 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,20 +1,17 @@ -//========================================================================================================= -// Copy -//========================================================================================================= /** - * @internal - * Creates a fresh copy of an Array/Object + * Creates a fresh (deep) copy of the specified value. * https://www.samanthaming.com/tidbits/70-3-ways-to-clone-objects/ - * @param value - Array/Object that gets copied + * + * @public + * @param value - Value to be copied. */ export function copy(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(); + const valConstructorName = + Object.getPrototypeOf(value).constructor.name.toLowerCase(); if (valConstructorName !== 'object' && valConstructorName !== 'array') return value; @@ -27,15 +24,13 @@ export function copy(value: T): T { return newObject as T; } -//========================================================================================================= -// Is Valid Object -//========================================================================================================= /** - * @internal - * Checks if passed value is a valid Object + * Checks whether the specified value is a valid object. * https://stackoverflow.com/questions/12996871/why-does-typeof-array-with-objects-return-object-and-not-array - * @param value - Value that is tested for its correctness - * @param considerArray - Whether Arrays should be considered as object + * + * @public + * @param value - Value + * @param considerArray - Whether to considered an array as an object. */ export function isValidObject(value: any, considerArray = false): boolean { function isHTMLElement(obj: any) { @@ -59,12 +54,10 @@ export function isValidObject(value: any, considerArray = false): boolean { ); } -//========================================================================================================= -// Includes Array -//========================================================================================================= /** - * @internal - * Check if array1 contains all elements of array2 + * Checks whether 'array1' contains all elements of 'array2'. + * + * @public * @param array1 - Array 1 * @param array2 - Array 2 */ @@ -75,13 +68,11 @@ export function includesArray( return array2.every((element) => array1.includes(element)); } -//========================================================================================================= -// Normalize Array -//========================================================================================================= /** - * @internal - * Transforms Item/s to an Item Array - * @param items - Item/s that gets transformed to an Array + * Transforms Item/s into an array of Items. + * + * @public + * @param items - Item/s to be transformed into an array of Items. * @param config - Config */ export function normalizeArray( @@ -95,25 +86,21 @@ export function normalizeArray( return Array.isArray(items) ? items : [items as DataType]; } -//========================================================================================================= -// Is Function -//========================================================================================================= /** - * @internal - * Checks if value is a function - * @param value - Value that gets tested if its a function + * Checks whether the specified function is a function. + * + * @public + * @param value - Value to be checked */ export function isFunction(value: any): boolean { return typeof value === 'function'; } -//========================================================================================================= -// Is Async Function -//========================================================================================================= /** - * @internal - * Checks if value is an async function - * @param value - Value that gets tested if its an async function + * Checks whether the specified function is an async function. + * + * @public + * @param value - Value to be checked. */ export function isAsyncFunction(value: any): boolean { const valueString = value.toString(); @@ -124,13 +111,11 @@ export function isAsyncFunction(value: any): boolean { ); } -//========================================================================================================= -// Is Json String -//========================================================================================================= /** - * @internal - * Checks if value is valid JsonString - * @param value - Value that gets checked + * Checks whether the specified value is a valid JSON string + * + * @public + * @param value - Value to be checked. */ export function isJsonString(value: any): boolean { if (typeof value !== 'string') return false; @@ -142,15 +127,13 @@ export function isJsonString(value: any): boolean { return true; } -//========================================================================================================= -// Define Config -//========================================================================================================= /** - * @internal - * Merges default values/properties into config object - * @param config - Config object that receives default values - * @param defaults - Default values object that gets merged into config object - * @param overwriteUndefinedProperties - If undefined Properties in config gets overwritten by the default value + * Merges the default values object ('defaults') into the configuration object ('config'). + * + * @public + * @param config - Configuration object to merge the default values in. + * @param defaults - Default values object to be merged into the configuration object. + * @param overwriteUndefinedProperties - Whether to overwrite 'undefined' set properties with default values. */ export function defineConfig( config: ConfigInterface, @@ -174,24 +157,22 @@ export function defineConfig( return shallowCopiedConfig; } -//========================================================================================================= -// Flat Merge -//========================================================================================================= -/** - * @internal - * @param addNewProperties - Adds new properties to source Object - */ export interface FlatMergeConfigInterface { + /** + * + * Whether to add new properties (properties that doesn't exist in the source object yet) to the source object. + * @default true + */ addNewProperties?: boolean; } /** - * @internal - * Merges items into object, be aware that the merge will only happen at the top level of the object. - * Initially it adds new properties of the changes object into the source object. + * Merges the 'changes' object into the 'source' object at top level. + * + * @public * @param source - Source object - * @param changes - Changes that get merged into the source object - * @param config - Config + * @param changes - Changes object to be merged into the source object + * @param config - Configuration object */ export function flatMerge( source: DataType, @@ -219,14 +200,12 @@ export function flatMerge( return _source; } -//========================================================================================================= -// Equals -//========================================================================================================= /** - * @internal - * Check if two values are equal - * @param value1 - First Value - * @param value2 - Second Value + * Checks whether the two specified values are equivalent. + * + * @public + * @param value1 - First value. + * @param value2 - Second value. */ export function equal(value1: any, value2: any): boolean { return ( @@ -239,46 +218,44 @@ export function equal(value1: any, value2: any): boolean { ); } -//========================================================================================================= -// Not Equals -//========================================================================================================= /** - * @internal - * Checks if two values aren't equal - * @param value1 - First Value - * @param value2 - Second Value + * Checks whether the two specified values are NOT equivalent. + * + * @public + * @param value1 - First value. + * @param value2 - Second value. */ export function notEqual(value1: any, value2: any): boolean { return !equal(value1, value2); } -//========================================================================================================= -// Generate Id -//========================================================================================================= /** - * @internal - * Generates random Id - * @param length - Length of generated Id + * Generates a randomized id based on alphabetic and numeric characters. + * + * @public + * @param length - Length of the to generate id (default = 5). + * @param characters - Characters to generate the id from (default = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'). */ -export function generateId(length?: number): string { - const characters = - 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; +export function generateId( + length = 5, + characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' +): string { const charactersLength = characters.length; let result = ''; - if (!length) length = 5; for (let i = 0; i < length; i++) { result += characters.charAt(Math.floor(Math.random() * charactersLength)); } return result; } -//========================================================================================================= -// Create Array From Object -//========================================================================================================= /** - * @internal - * Transforms Object to Array - * @param object - Object that gets transformed + * Transforms the specified object into an array. + * + * Example: + * {"1": 'jeff', 2: 'frank'} -> [{key: "1", instance: 'jeff'}, {key: 2, instance: 'frank'}] + * + * @public + * @param object - Object to be transformed to an array. */ export function createArrayFromObject

(object: { [key: string]: P; @@ -293,13 +270,11 @@ export function createArrayFromObject

(object: { return array; } -//========================================================================================================= -// Clone -//========================================================================================================= /** - * @internal - * Clones a Class - * @param instance - Instance of Class you want to clone + * Clones the specified class. + * + * @public + * @param instance - Class to be cloned. */ export function clone(instance: T): T { // Clone Class @@ -312,14 +287,12 @@ export function clone(instance: T): T { return objectClone; } -//========================================================================================================= -// Remove Properties -//========================================================================================================= /** - * @internal - * Removes properties from Object - * @param object - Object from which the properties get removed - * @param properties - Properties that get removed from the object + * Removes specified properties from the defined object. + * + * @public + * @param object - Object to remove the specified properties from. + * @param properties - Property keys to be removed from the specified object. */ export function removeProperties( object: T, diff --git a/yarn.lock b/yarn.lock index f275cf02..5219edd2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,8 +2,15 @@ # yarn lockfile v1 +"@agile-ts/core@^0.1.2": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@agile-ts/core/-/core-0.1.3.tgz#d96dd4a20d65adce9aaba1c494b31e4e0dd1bb60" + integrity sha512-sHw9PMbqww0dwqLEZih9hIpZjMAmZB4yea7bkbqblNc1CRDKfCGeYGnNcg8GOqXfNfq5SywMGWo5KhhFFyx+ag== + dependencies: + "@agile-ts/utils" "^0.0.7" + "@agile-ts/core@file:packages/core": - version "0.1.2" + version "0.2.0-alpha.4" dependencies: "@agile-ts/utils" "^0.0.7" @@ -15,6 +22,11 @@ "@agile-ts/proxytree@file:packages/proxytree": version "0.0.5" +"@agile-ts/react@^0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@agile-ts/react/-/react-0.1.2.tgz#d07f6b935d9322cd60d2e9e3871da554b04460af" + integrity sha512-W4u2+X6KCeXPdkjit/NsMJG5nBsa7dNFaEzyfTsp5Cqbs99zLqY6dO8LUIYyhRt/+HBvEW9o64i/6Kqd59WM1Q== + "@akryum/winattr@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@akryum/winattr/-/winattr-3.0.0.tgz#c345d49f8415583897e345729c12b3503927dd11"