From b369998abc9d86e0f0b52ca0abd5b0111327d669 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Wed, 28 Jul 2021 07:44:20 +0200 Subject: [PATCH 01/93] adde dev option to benchmarks --- README.md | 10 +++++----- benchmark/.env | 2 +- benchmark/README.md | 10 +++++++++- benchmark/run.ts | 10 ++++++++-- 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e684906b..f677870b 100644 --- a/README.md +++ b/README.md @@ -88,19 +88,19 @@ The philosophy behind AgileTs is simple: Write minimalistic, boilerplate-free code that captures your intent. ```ts -// Create State with inital value 'frank' +// Create State with the inital value 'frank' const MY_STATE = createState('frank'); -// Update State value from 'frank' to 'jeff' +// Update the State value from 'frank' to 'jeff' MY_STATE.set('jeff'); -// Undo latest State value change +// Undo the latest State value change MY_STATE.undo(); -// Reset State value to its initial value +// Reset the State value to its initial value MY_STATE.reset(); -// Permanently store State value in an external Storage +// Permanently store the State value in an external Storage MY_STATE.persist("storage-key"); ``` diff --git a/benchmark/.env b/benchmark/.env index 21903adb..41ee6b18 100644 --- a/benchmark/.env +++ b/benchmark/.env @@ -1 +1 @@ -MANUAL_BENCHMARK=false +DEV=false diff --git a/benchmark/README.md b/benchmark/README.md index ce2e9db2..e4754a4d 100644 --- a/benchmark/README.md +++ b/benchmark/README.md @@ -121,10 +121,18 @@ Execute the benchmark located in `./benchmarks/react/counter`. ```ts yarn run test:counter ``` +If you want to test it manually, enable the `dev mode`. +```ts +yarn run test:counter --dev +``` +The difference to the 'normal' mode is that: +- the executed bundle isn't `minified` +- the test has to be started manually by opening `localhost:3003` +- the test results are printed in the `browser console` ## ⭐️ Contribute -Get a part of AgileTs and start contributing. We welcome any meaningful contribution. 😀 +Feel free to add more tests and State Managers to be tested. We welcome any meaningful contribution. 😀 To find out more about contributing, check out the [CONTRIBUTING.md](https://github.com/agile-ts/agile/blob/master/CONTRIBUTING.md). diff --git a/benchmark/run.ts b/benchmark/run.ts index 2bd03642..3ce2b57a 100644 --- a/benchmark/run.ts +++ b/benchmark/run.ts @@ -10,6 +10,7 @@ dotenv.config(); // Extract entry (at third parameter) from the executed command // yarn run ./path/to/entry -> './path/to/entry' is extracted const entry = process.argv.slice(2)[0]; +const dev = process.argv.slice(2)[1] === '--dev' || process.env.DEV === 'true'; if (entry == null) { throw new Error( "No valid entry was provided! Valid entry example: 'yarn run ./benchmarks/react/counter'" @@ -34,7 +35,7 @@ const startBenchmark = async () => { target: 'es2015', format: 'cjs', // https://esbuild.github.io/api/#format-commonjs platform: 'browser', - minify: true, // https://esbuild.github.io/api/#minify + minify: !dev, // https://esbuild.github.io/api/#minify bundle: true, // https://esbuild.github.io/api/#bundle sourcemap: 'external', // https://esbuild.github.io/api/#sourcemap// https://github.com/evanw/esbuild/issues/69 } @@ -53,7 +54,12 @@ const startBenchmark = async () => { const page = await context.newPage(); // Option to open and test the Benchmark Test Suite in the browser manually - if (process.env.MANUAL_BENCHMARK === 'true') { + if (dev) { + console.log( + `${chalk.blue('[i]')} ${chalk.gray( + `Development mode is ${chalk.green(`active`)}` + )}` + ); console.log( `${chalk.blue('[i]')} ${chalk.gray( `Benchmark is running at ${chalk.blueBright.bold(serverUrl)}` From 9db6211e40154d09bdca0a42969033c8248a0659 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Wed, 28 Jul 2021 17:00:19 +0200 Subject: [PATCH 02/93] added tree-shaking test example --- .../plainjs/develop/tree-shaking/.gitignore | 4 ++++ .../plainjs/develop/tree-shaking/package.json | 21 +++++++++++++++++++ .../plainjs/develop/tree-shaking/src/index.js | 5 +++++ .../develop/tree-shaking/webpack.config.js | 16 ++++++++++++++ packages/core/src/agile.ts | 1 - packages/tsconfig.default.json | 2 +- 6 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 examples/plainjs/develop/tree-shaking/.gitignore create mode 100644 examples/plainjs/develop/tree-shaking/package.json create mode 100644 examples/plainjs/develop/tree-shaking/src/index.js create mode 100644 examples/plainjs/develop/tree-shaking/webpack.config.js diff --git a/examples/plainjs/develop/tree-shaking/.gitignore b/examples/plainjs/develop/tree-shaking/.gitignore new file mode 100644 index 00000000..6f1daf39 --- /dev/null +++ b/examples/plainjs/develop/tree-shaking/.gitignore @@ -0,0 +1,4 @@ +node_modules +dist +.yalc +yalc.lock diff --git a/examples/plainjs/develop/tree-shaking/package.json b/examples/plainjs/develop/tree-shaking/package.json new file mode 100644 index 00000000..9f6bb167 --- /dev/null +++ b/examples/plainjs/develop/tree-shaking/package.json @@ -0,0 +1,21 @@ +{ + "name": "tree-shiking-test", + "version": "1.0.0", + "description": "", + "main": "src/index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "build": "webpack", + "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" + }, + "dependencies": { + "@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 new file mode 100644 index 00000000..f20e209d --- /dev/null +++ b/examples/plainjs/develop/tree-shaking/src/index.js @@ -0,0 +1,5 @@ +import {createState} from '@agile-ts/core'; + +const MY_STATE = createState('hi'); + +console.log(MY_STATE.value) diff --git a/examples/plainjs/develop/tree-shaking/webpack.config.js b/examples/plainjs/develop/tree-shaking/webpack.config.js new file mode 100644 index 00000000..bf6f7aa9 --- /dev/null +++ b/examples/plainjs/develop/tree-shaking/webpack.config.js @@ -0,0 +1,16 @@ +const path = require("path"); + +module.exports = { + entry: "./src/index.js", + output: { + filename: "main.js", + path: path.resolve(__dirname, "dist"), + }, + mode: "development", + optimization: { + usedExports: true, + innerGraph: true, + sideEffects: true, + }, + devtool: false, +}; diff --git a/packages/core/src/agile.ts b/packages/core/src/agile.ts index 6a8d34fa..f802b20a 100644 --- a/packages/core/src/agile.ts +++ b/packages/core/src/agile.ts @@ -14,7 +14,6 @@ import { CreateStorageConfigInterface, RegisterConfigInterface, StateConfigInterface, - flatMerge, LogCodeManager, DependableAgileInstancesType, CreateComputedConfigInterface, diff --git a/packages/tsconfig.default.json b/packages/tsconfig.default.json index fb331abd..ef97da46 100644 --- a/packages/tsconfig.default.json +++ b/packages/tsconfig.default.json @@ -5,7 +5,7 @@ /* Basic Options */ // "incremental": true, /* Enable incremental compilation */ "target": "ES2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ - "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + "module": "ES2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ // "lib": [], /* Specify library files to be included in the compilation. */ // "allowJs": true, /* Allow javascript files to be compiled. */ // "checkJs": true, /* Report errors in .js files. */ From 3a6c84b7a4505aa53eb10734b582c93fe198dc7e Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Fri, 30 Jul 2021 08:02:51 +0200 Subject: [PATCH 03/93] tried to tree shake collection --- examples/react/release/boxes/package.json | 2 +- packages/core/src/agile.ts | 173 +----------------- .../src/collection/collection.persistent.ts | 16 +- .../src/collection/group/group.observer.ts | 19 +- packages/core/src/collection/group/index.ts | 32 ++-- packages/core/src/collection/index.ts | 67 +++++-- packages/core/src/collection/item.ts | 20 +- packages/core/src/collection/selector.ts | 14 +- packages/core/src/computed/index.ts | 97 ++++++++-- packages/core/src/index.ts | 14 +- packages/core/src/integrations/index.ts | 5 +- packages/core/src/integrations/integration.ts | 2 +- packages/core/src/internal.ts | 54 ------ packages/core/src/runtime/index.ts | 17 +- packages/core/src/runtime/observer.ts | 18 +- packages/core/src/runtime/runtime.job.ts | 4 +- .../CallbackSubscriptionContainer.ts | 4 +- .../ComponentSubscriptionContainer.ts | 4 +- .../container/SubscriptionContainer.ts | 8 +- .../runtime/subscription/sub.controller.ts | 16 +- packages/core/src/shared.ts | 171 +---------------- packages/core/src/state/index.ts | 61 ++++-- packages/core/src/state/state.observer.ts | 26 +-- packages/core/src/state/state.persistent.ts | 6 +- packages/core/src/state/state.runtime.job.ts | 6 +- packages/core/src/storages/index.ts | 15 +- packages/core/src/storages/persistent.ts | 11 +- packages/core/src/storages/storage.ts | 23 ++- packages/core/src/utils.ts | 29 ++- 29 files changed, 349 insertions(+), 585 deletions(-) delete mode 100644 packages/core/src/internal.ts diff --git a/examples/react/release/boxes/package.json b/examples/react/release/boxes/package.json index ff3c03f1..ebb40840 100644 --- a/examples/react/release/boxes/package.json +++ b/examples/react/release/boxes/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { - "@agile-ts/core": "^0.1.2", + "@agile-ts/core": "file:.yalc/@agile-ts/core", "@agile-ts/logger": "^0.0.7", "@agile-ts/proxytree": "^0.0.5", "@agile-ts/react": "^0.1.2", diff --git a/packages/core/src/agile.ts b/packages/core/src/agile.ts index f802b20a..05f2df7d 100644 --- a/packages/core/src/agile.ts +++ b/packages/core/src/agile.ts @@ -1,30 +1,12 @@ -import { - Runtime, - Integration, - State, - Storage, - Collection, - CollectionConfig, - DefaultItem, - Computed, - Integrations, - SubController, - globalBind, - Storages, - CreateStorageConfigInterface, - RegisterConfigInterface, - StateConfigInterface, - LogCodeManager, - DependableAgileInstancesType, - CreateComputedConfigInterface, - ComputeFunctionType, - createStorage, - createState, - createCollection, - createComputed, - IntegrationsConfigInterface, - defineConfig, -} from './internal'; +import { Runtime } from './runtime'; +import { SubController } from './runtime/subscription/sub.controller'; +import { RegisterConfigInterface, Storages } from './storages'; +import { Storage } from './storages/storage'; +import { Integrations, IntegrationsConfigInterface } from './integrations'; +import { defineConfig } from '@agile-ts/utils'; +import { LogCodeManager } from './logCodeManager'; +import { globalBind } from './utils'; +import { Integration } from './integrations/integration'; export class Agile { public config: AgileConfigInterface; @@ -104,143 +86,6 @@ export class Agile { } } - /** - * Returns a newly created Storage. - * - * A Storage Class serves as an interface to external storages, - * such as the [Async Storage](https://github.com/react-native-async-storage/async-storage) or - * [Local Storage](https://www.w3schools.com/html/html5_webstorage.asp). - * - * It creates the foundation to easily [`persist()`](https://agile-ts.org/docs/core/state/methods#persist) [Agile Sub Instances](https://agile-ts.org/docs/introduction/#agile-sub-instance) - * (like States or Collections) in nearly any external storage. - * - * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#createstorage) - * - * @public - * @param config - Configuration object - */ - public createStorage(config: CreateStorageConfigInterface): Storage { - return createStorage(config); - } - - /** - * Returns a newly created State. - * - * A State manages 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. - * - * You can create as many global 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 - */ - public createState( - initialValue: ValueType, - config: StateConfigInterface = {} - ): State { - return createState( - initialValue, - defineConfig(config, { - agileInstance: this, - }) - ); - } - - /** - * Returns a newly created Collection. - * - * A Collection manages a reactive set of Information - * that we need to remember globally at a later point in time. - * While providing a toolkit to use and mutate this set of Information. - * - * It is designed for arrays of data objects following the same pattern. - * - * Each of these data object must have a unique `primaryKey` to be correctly identified later. - * - * You can create as many global Collections as you need. - * - * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#createcollection) - * - * @public - * @param config - Configuration object - */ - public createCollection( - config?: CollectionConfig - ): Collection { - return createCollection(config, this); - } - - /** - * Returns a newly created Computed. - * - * A Computed is an extension of the State Class - * that computes its value based on a specified compute function. - * - * The computed value will be cached to avoid unnecessary recomputes - * and is only recomputed when one of its direct dependencies changes. - * - * 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) - * - * @public - * @param computeFunction - Function to compute the computed value. - * @param config - Configuration object - */ - public createComputed( - computeFunction: ComputeFunctionType, - config?: CreateComputedConfigInterface - ): Computed; - /** - * Returns a newly created Computed. - * - * A Computed is an extension of the State Class - * that computes its value based on a specified compute function. - * - * The computed value will be cached to avoid unnecessary recomputes - * and is only recomputed when one of its direct dependencies changes. - * - * 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#createcomputed) - * - * @public - * @param computeFunction - Function to compute the computed value. - * @param deps - Hard-coded dependencies on which the Computed Class should depend. - */ - public createComputed( - computeFunction: ComputeFunctionType, - deps?: Array - ): Computed; - public createComputed( - computeFunction: ComputeFunctionType, - configOrDeps?: - | CreateComputedConfigInterface - | Array - ): Computed { - let _config: CreateComputedConfigInterface = {}; - - if (Array.isArray(configOrDeps)) { - _config = defineConfig(_config, { - computedDeps: configOrDeps, - agileInstance: this, - }); - } else { - if (configOrDeps) - _config = defineConfig(configOrDeps, { - agileInstance: this, - }); - } - - return createComputed(computeFunction, _config); - } - /** * Registers the specified Integration with AgileTs. * diff --git a/packages/core/src/collection/collection.persistent.ts b/packages/core/src/collection/collection.persistent.ts index 2f2b7136..0c7a06e9 100644 --- a/packages/core/src/collection/collection.persistent.ts +++ b/packages/core/src/collection/collection.persistent.ts @@ -1,17 +1,13 @@ +import { Collection, CollectionKey, DefaultItem, ItemKey } from './index'; import { - Collection, - CollectionKey, CreatePersistentConfigInterface, - DefaultItem, - defineConfig, - Group, - GroupKey, - ItemKey, - LogCodeManager, Persistent, PersistentKey, - StorageKey, -} from '../internal'; +} from '../storages/persistent'; +import { defineConfig } from '@agile-ts/utils'; +import { Group, GroupKey } from './group'; +import { LogCodeManager } from '../logCodeManager'; +import { StorageKey } from '../storages/storage'; export class CollectionPersistent< DataType extends Object = DefaultItem diff --git a/packages/core/src/collection/group/group.observer.ts b/packages/core/src/collection/group/group.observer.ts index 4c0e860c..305a03f7 100644 --- a/packages/core/src/collection/group/group.observer.ts +++ b/packages/core/src/collection/group/group.observer.ts @@ -1,16 +1,15 @@ import { - Observer, - Group, CreateObserverConfigInterface, - copy, - equal, - generateId, - RuntimeJob, - Item, - IngestConfigInterface, + Observer, +} from '../../runtime/observer'; +import { Group } from './index'; +import { copy, defineConfig, equal, generateId } from '@agile-ts/utils'; +import { Item } from '../item'; +import { CreateRuntimeJobConfigInterface, - defineConfig, -} from '../../internal'; + RuntimeJob, +} from '../../runtime/runtime.job'; +import { IngestConfigInterface } from '../../runtime'; export class GroupObserver extends Observer { // Group the Observer belongs to diff --git a/packages/core/src/collection/group/index.ts b/packages/core/src/collection/group/index.ts index 44aef2c5..d85e88e8 100644 --- a/packages/core/src/collection/group/index.ts +++ b/packages/core/src/collection/group/index.ts @@ -1,24 +1,26 @@ +import { Collection, DefaultItem, ItemKey } from '../index'; import { State, - Collection, - DefaultItem, - ItemKey, - normalizeArray, - Item, - copy, - CollectionPersistent, + StateObserversInterface, StatePersistentConfigInterface, - isValidObject, - PersistentKey, - ComputedTracker, +} from '../../state'; +import { GroupObserver } from './group.observer'; +import { StateIngestConfigInterface, - removeProperties, - LogCodeManager, - StateObserversInterface, - GroupObserver, StateObserver, +} from '../../state/state.observer'; +import { LogCodeManager } from '../../logCodeManager'; +import { ComputedTracker } from '../../computed/computed.tracker'; +import { + copy, defineConfig, -} from '../../internal'; + isValidObject, + normalizeArray, + removeProperties, +} from '@agile-ts/utils'; +import { Item } from '../item'; +import { PersistentKey } from '../../storages/persistent'; +import { CollectionPersistent } from '../collection.persistent'; export class Group< DataType extends Object = DefaultItem, diff --git a/packages/core/src/collection/index.ts b/packages/core/src/collection/index.ts index 83b644fa..58788b04 100644 --- a/packages/core/src/collection/index.ts +++ b/packages/core/src/collection/index.ts @@ -1,27 +1,28 @@ +import { Agile } from '../agile'; +import { Item } from './item'; +import { CollectionPersistent } from './collection.persistent'; import { - Agile, - Item, - Group, - GroupKey, - Selector, - SelectorKey, - StorageKey, - GroupConfigInterface, - isValidObject, - normalizeArray, copy, - CollectionPersistent, - GroupAddConfigInterface, - ComputedTracker, + defineConfig, generateId, - SideEffectConfigInterface, - SelectorConfigInterface, - removeProperties, isFunction, - LogCodeManager, - PatchOptionConfigInterface, - defineConfig, -} from '../internal'; + isValidObject, + normalizeArray, + removeProperties, +} from '@agile-ts/utils'; +import { + GroupAddConfigInterface, + GroupConfigInterface, + GroupKey, +} from './group'; +import { LogCodeManager } from '../logCodeManager'; +import { Selector, SelectorConfigInterface, SelectorKey } from './selector'; +import { Group } from './group'; +import { ComputedTracker } from '../computed/computed.tracker'; +import { StorageKey } from '../storages/storage'; +import { SideEffectConfigInterface } from '../runtime/runtime.job'; +import { PatchOptionConfigInterface } from '../state'; +import { shared } from '../shared'; export class Collection< DataType extends Object = DefaultItem, @@ -1484,6 +1485,32 @@ export class Collection< } } +/** + * Returns a newly created Collection. + * + * A Collection manages a reactive set of Information + * that we need to remember globally at a later point in time. + * While providing a toolkit to use and mutate this set of Information. + * + * It is designed for arrays of data objects following the same pattern. + * + * Each of these data object must have a unique `primaryKey` to be correctly identified later. + * + * You can create as many global Collections as you need. + * + * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#createcollection) + * + * @public + * @param config - Configuration object + * @param agileInstance - Instance of Agile the Collection belongs to. + */ +export function createCollection( + config?: CollectionConfig, + agileInstance: Agile = shared +): Collection { + return new Collection(agileInstance, config); +} + export type DefaultItem = Record; // same as { [key: string]: any }; export type CollectionKey = string | number; export type ItemKey = string | number; diff --git a/packages/core/src/collection/item.ts b/packages/core/src/collection/item.ts index 1be4cebf..491af2a9 100644 --- a/packages/core/src/collection/item.ts +++ b/packages/core/src/collection/item.ts @@ -1,16 +1,10 @@ -import { - State, - Collection, - StateKey, - StateRuntimeJobConfigInterface, - SelectorKey, - PersistentKey, - isValidObject, - CollectionPersistent, - StatePersistentConfigInterface, - DefaultItem, - defineConfig, -} from '../internal'; +import { Collection, DefaultItem } from './index'; +import { State, StateKey, StatePersistentConfigInterface } from '../state'; +import { SelectorKey } from './selector'; +import { StateRuntimeJobConfigInterface } from '../state/state.runtime.job'; +import { defineConfig, isValidObject } from '@agile-ts/utils'; +import { PersistentKey } from '../storages/persistent'; +import { CollectionPersistent } from './collection.persistent'; export class Item extends State< DataType diff --git a/packages/core/src/collection/selector.ts b/packages/core/src/collection/selector.ts index 937c5458..68be7df6 100644 --- a/packages/core/src/collection/selector.ts +++ b/packages/core/src/collection/selector.ts @@ -1,12 +1,8 @@ -import { - Collection, - DefaultItem, - defineConfig, - Item, - ItemKey, - State, - StateRuntimeJobConfigInterface, -} from '../internal'; +import { Collection, DefaultItem, ItemKey } from './index'; +import { State } from '../state'; +import { Item } from './item'; +import { StateRuntimeJobConfigInterface } from '../state/state.runtime.job'; +import { defineConfig } from '@agile-ts/utils'; export class Selector< DataType extends Object = DefaultItem diff --git a/packages/core/src/computed/index.ts b/packages/core/src/computed/index.ts index 4499c73d..58c53807 100644 --- a/packages/core/src/computed/index.ts +++ b/packages/core/src/computed/index.ts @@ -1,17 +1,17 @@ +import { State, StateConfigInterface } from '../state'; +import { Observer } from '../runtime/observer'; +import { Agile } from '../agile'; import { - State, - Agile, - Observer, - StateConfigInterface, - ComputedTracker, - Collection, - StateIngestConfigInterface, - removeProperties, - LogCodeManager, - isAsyncFunction, - extractRelevantObservers, defineConfig, -} from '../internal'; + isAsyncFunction, + removeProperties, +} from '@agile-ts/utils'; +import { extractRelevantObservers } from '../utils'; +import { ComputedTracker } from './computed.tracker'; +import { LogCodeManager } from '../logCodeManager'; +import { StateIngestConfigInterface } from '../state/state.observer'; +import { Collection } from '../collection'; +import { CreateAgileSubInstanceInterface, shared } from '../shared'; export class Computed extends State< ComputedValueType @@ -204,6 +204,79 @@ export class Computed extends State< } } +/** + * Returns a newly created Computed. + * + * A Computed is an extension of the State Class + * that computes its value based on a specified compute function. + * + * The computed value will be cached to avoid unnecessary recomputes + * and is only recomputed when one of its direct dependencies changes. + * + * 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) + * + * @public + * @param computeFunction - Function to compute the computed value. + * @param config - Configuration object + */ +export function createComputed( + computeFunction: ComputeFunctionType, + config?: CreateComputedConfigInterfaceWithAgile +): Computed; +/** + * Returns a newly created Computed. + * + * A Computed is an extension of the State Class + * that computes its value based on a specified compute function. + * + * The computed value will be cached to avoid unnecessary recomputes + * and is only recomputed when one of its direct dependencies changes. + * + * 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#createcomputed) + * + * @public + * @param computeFunction - Function to compute the computed value. + * @param deps - Hard-coded dependencies on which the Computed Class should depend. + */ +export function createComputed( + computeFunction: ComputeFunctionType, + deps?: Array +): Computed; +export function createComputed( + computeFunction: ComputeFunctionType, + configOrDeps?: + | CreateComputedConfigInterface + | Array +): Computed { + let _config: CreateComputedConfigInterfaceWithAgile = {}; + + if (Array.isArray(configOrDeps)) { + _config = defineConfig(_config, { + computedDeps: configOrDeps, + }); + } else { + if (configOrDeps) _config = configOrDeps; + } + + _config = defineConfig(_config, { agileInstance: shared }); + + return new Computed( + _config.agileInstance as any, + computeFunction, + removeProperties(_config, ['agileInstance']) + ); +} + +export interface CreateComputedConfigInterfaceWithAgile + extends CreateAgileSubInstanceInterface, + CreateComputedConfigInterface {} + export type ComputeFunctionType = () => | ComputedValueType | Promise; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 6469f1fa..ced21915 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,4 +1,14 @@ -import { Agile } from './internal'; +import { Agile } from './agile'; + +export * from './storages'; +export * from './storages/storage'; +export * from './state'; +export * from './integrations/integration'; +export * from './computed'; +export * from './collection'; +export * from './collection/item'; +export * from './collection/group'; +export * from './collection/selector'; +export * from './shared'; -export * from './internal'; export default Agile; diff --git a/packages/core/src/integrations/index.ts b/packages/core/src/integrations/index.ts index c13e081b..d54802d7 100644 --- a/packages/core/src/integrations/index.ts +++ b/packages/core/src/integrations/index.ts @@ -1,4 +1,7 @@ -import { Agile, Integration, LogCodeManager, defineConfig } from '../internal'; +import { Agile } from '../agile'; +import { Integration } from './integration'; +import { defineConfig } from '@agile-ts/utils'; +import { LogCodeManager } from '../logCodeManager'; const onRegisterInitialIntegrationCallbacks: (( integration: Integration diff --git a/packages/core/src/integrations/integration.ts b/packages/core/src/integrations/integration.ts index f2c409ac..2acb6eb9 100644 --- a/packages/core/src/integrations/integration.ts +++ b/packages/core/src/integrations/integration.ts @@ -1,4 +1,4 @@ -import { Agile } from '../internal'; +import { Agile } from '../agile'; export class Integration { // Key/Name identifier of the Integration diff --git a/packages/core/src/internal.ts b/packages/core/src/internal.ts deleted file mode 100644 index b9356568..00000000 --- a/packages/core/src/internal.ts +++ /dev/null @@ -1,54 +0,0 @@ -// This file exposes Agile functions and types to the outside world -// It also serves as a cyclic dependency workaround -// https://medium.com/visual-development/how-to-fix-nasty-circular-dependency-issues-once-and-for-all-in-javascript-typescript-a04c987cf0de. - -// !! All internal Agile modules must be imported from here!! - -// Utils -export * from './utils'; -export * from '@agile-ts/utils'; - -// Logger -export * from './logCodeManager'; - -// Agile -export * from './agile'; - -// Integrations -export * from './integrations'; -export * from './integrations/integration'; - -// Runtime -export * from './runtime'; -export * from './runtime/observer'; -export * from './runtime/runtime.job'; -export * from './runtime/subscription/container/SubscriptionContainer'; -export * from './runtime/subscription/container/CallbackSubscriptionContainer'; -export * from './runtime/subscription/container/ComponentSubscriptionContainer'; -export * from './runtime/subscription/sub.controller'; - -// Storage -export * from './storages'; -export * from './storages/storage'; -export * from './storages/persistent'; - -// State -export * from './state'; -export * from './state/state.observer'; -export * from './state/state.persistent'; -export * from './state/state.runtime.job'; - -// Computed -export * from './computed'; -export * from './computed/computed.tracker'; - -// Collection -export * from './collection'; -export * from './collection/group'; -export * from './collection/group/group.observer'; -export * from './collection/item'; -export * from './collection/selector'; -export * from './collection/collection.persistent'; - -// Shared -export * from './shared'; diff --git a/packages/core/src/runtime/index.ts b/packages/core/src/runtime/index.ts index 0dd3419f..52320848 100644 --- a/packages/core/src/runtime/index.ts +++ b/packages/core/src/runtime/index.ts @@ -1,13 +1,10 @@ -import { - Agile, - SubscriptionContainer, - RuntimeJob, - CallbackSubscriptionContainer, - ComponentSubscriptionContainer, - notEqual, - LogCodeManager, - defineConfig, -} from '../internal'; +import { Agile } from '../agile'; +import { RuntimeJob } from './runtime.job'; +import { LogCodeManager } from '../logCodeManager'; +import { defineConfig, notEqual } from '@agile-ts/utils'; +import { SubscriptionContainer } from './subscription/container/SubscriptionContainer'; +import { CallbackSubscriptionContainer } from './subscription/container/CallbackSubscriptionContainer'; +import { ComponentSubscriptionContainer } from './subscription/container/ComponentSubscriptionContainer'; export class Runtime { // Agile Instance the Runtime belongs to diff --git a/packages/core/src/runtime/observer.ts b/packages/core/src/runtime/observer.ts index dc49f1f6..783e37a0 100644 --- a/packages/core/src/runtime/observer.ts +++ b/packages/core/src/runtime/observer.ts @@ -1,14 +1,10 @@ -import { - Agile, - StateKey, - RuntimeJob, - SubscriptionContainer, - IngestConfigInterface, - CreateRuntimeJobConfigInterface, - LogCodeManager, - generateId, - defineConfig, -} from '../internal'; +import { Agile } from '../agile'; +import { SubscriptionContainer } from './subscription/container/SubscriptionContainer'; +import { defineConfig, generateId } from '@agile-ts/utils'; +import { StateKey } from '../state'; +import { CreateRuntimeJobConfigInterface, RuntimeJob } from './runtime.job'; +import { LogCodeManager } from '../logCodeManager'; +import { IngestConfigInterface } from './index'; export type ObserverKey = string | number; diff --git a/packages/core/src/runtime/runtime.job.ts b/packages/core/src/runtime/runtime.job.ts index 54748c87..4bbdadc9 100644 --- a/packages/core/src/runtime/runtime.job.ts +++ b/packages/core/src/runtime/runtime.job.ts @@ -1,4 +1,6 @@ -import { defineConfig, Observer, SubscriptionContainer } from '../internal'; +import { Observer } from './observer'; +import { SubscriptionContainer } from './subscription/container/SubscriptionContainer'; +import { defineConfig } from '@agile-ts/utils'; export class RuntimeJob { public config: RuntimeJobConfigInterface; diff --git a/packages/core/src/runtime/subscription/container/CallbackSubscriptionContainer.ts b/packages/core/src/runtime/subscription/container/CallbackSubscriptionContainer.ts index bf974bea..211b3b32 100644 --- a/packages/core/src/runtime/subscription/container/CallbackSubscriptionContainer.ts +++ b/packages/core/src/runtime/subscription/container/CallbackSubscriptionContainer.ts @@ -1,8 +1,8 @@ import { - Observer, SubscriptionContainer, SubscriptionContainerConfigInterface, -} from '../../../internal'; +} from './SubscriptionContainer'; +import { Observer } from '../../observer'; export class CallbackSubscriptionContainer extends SubscriptionContainer { /** diff --git a/packages/core/src/runtime/subscription/container/ComponentSubscriptionContainer.ts b/packages/core/src/runtime/subscription/container/ComponentSubscriptionContainer.ts index ff80eded..bfff1cbf 100644 --- a/packages/core/src/runtime/subscription/container/ComponentSubscriptionContainer.ts +++ b/packages/core/src/runtime/subscription/container/ComponentSubscriptionContainer.ts @@ -1,8 +1,8 @@ import { - Observer, SubscriptionContainer, SubscriptionContainerConfigInterface, -} from '../../../internal'; +} from './SubscriptionContainer'; +import { Observer } from '../../observer'; export class ComponentSubscriptionContainer< C = any diff --git a/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts b/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts index 1ef0c89d..764c47eb 100644 --- a/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts +++ b/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts @@ -1,9 +1,5 @@ -import { - defineConfig, - generateId, - isValidObject, - Observer, -} from '../../../internal'; +import { Observer } from '../../observer'; +import { defineConfig, generateId, isValidObject } from '@agile-ts/utils'; export class SubscriptionContainer { /** diff --git a/packages/core/src/runtime/subscription/sub.controller.ts b/packages/core/src/runtime/subscription/sub.controller.ts index 51a76d4b..b9c03616 100644 --- a/packages/core/src/runtime/subscription/sub.controller.ts +++ b/packages/core/src/runtime/subscription/sub.controller.ts @@ -1,15 +1,13 @@ +import { Agile } from '../../agile'; +import { ComponentSubscriptionContainer } from './container/ComponentSubscriptionContainer'; +import { CallbackSubscriptionContainer } from './container/CallbackSubscriptionContainer'; +import { Observer } from '../observer'; import { - Agile, - Observer, SubscriptionContainer, - ComponentSubscriptionContainer, - CallbackSubscriptionContainer, - isFunction, SubscriptionContainerConfigInterface, - removeProperties, - LogCodeManager, - defineConfig, -} from '../../internal'; +} from './container/SubscriptionContainer'; +import { defineConfig, isFunction, removeProperties } from '@agile-ts/utils'; +import { LogCodeManager } from '../../logCodeManager'; export class SubController { // Agile Instance the SubController belongs to diff --git a/packages/core/src/shared.ts b/packages/core/src/shared.ts index 5f000c31..be26494b 100644 --- a/packages/core/src/shared.ts +++ b/packages/core/src/shared.ts @@ -1,21 +1,15 @@ +import { Agile } from './agile'; +import { runsOnServer } from './utils'; +import { CreateStorageConfigInterface, Storage } from './storages/storage'; +import { State, StateConfigInterface } from './state'; +import { defineConfig, removeProperties } from '@agile-ts/utils'; +import { Collection, CollectionConfig, DefaultItem } from './collection'; import { - Agile, - Collection, - CollectionConfig, Computed, ComputeFunctionType, CreateComputedConfigInterface, - CreateStorageConfigInterface, - DefaultItem, - defineConfig, DependableAgileInstancesType, - flatMerge, - removeProperties, - runsOnServer, - State, - StateConfigInterface, - Storage, -} from './internal'; +} from './computed'; /** * Shared Agile Instance that is used when no Agile Instance was specified. @@ -36,149 +30,6 @@ export function assignSharedAgileInstance(agileInstance: Agile): void { sharedAgileInstance = agileInstance; } -/** - * Returns a newly created Storage. - * - * A Storage Class serves as an interface to external storages, - * such as the [Async Storage](https://github.com/react-native-async-storage/async-storage) or - * [Local Storage](https://www.w3schools.com/html/html5_webstorage.asp). - * - * It creates the foundation to easily [`persist()`](https://agile-ts.org/docs/core/state/methods#persist) [Agile Sub Instances](https://agile-ts.org/docs/introduction/#agile-sub-instance) - * (like States or Collections) in nearly any external storage. - * - * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#createstorage) - * - * @public - * @param config - Configuration object - */ -export function createStorage(config: CreateStorageConfigInterface): Storage { - return new Storage(config); -} - -/** - * Returns a newly created State. - * - * A State manages 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. - * - * You can create as many global 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 = {} -): State { - config = defineConfig(config, { - agileInstance: sharedAgileInstance, - }); - return new State( - config.agileInstance as any, - initialValue, - removeProperties(config, ['agileInstance']) - ); -} - -/** - * Returns a newly created Collection. - * - * A Collection manages a reactive set of Information - * that we need to remember globally at a later point in time. - * While providing a toolkit to use and mutate this set of Information. - * - * It is designed for arrays of data objects following the same pattern. - * - * Each of these data object must have a unique `primaryKey` to be correctly identified later. - * - * You can create as many global Collections as you need. - * - * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#createcollection) - * - * @public - * @param config - Configuration object - * @param agileInstance - Instance of Agile the Collection belongs to. - */ -export function createCollection( - config?: CollectionConfig, - agileInstance: Agile = sharedAgileInstance -): Collection { - return new Collection(agileInstance, config); -} - -/** - * Returns a newly created Computed. - * - * A Computed is an extension of the State Class - * that computes its value based on a specified compute function. - * - * The computed value will be cached to avoid unnecessary recomputes - * and is only recomputed when one of its direct dependencies changes. - * - * 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) - * - * @public - * @param computeFunction - Function to compute the computed value. - * @param config - Configuration object - */ -export function createComputed( - computeFunction: ComputeFunctionType, - config?: CreateComputedConfigInterfaceWithAgile -): Computed; -/** - * Returns a newly created Computed. - * - * A Computed is an extension of the State Class - * that computes its value based on a specified compute function. - * - * The computed value will be cached to avoid unnecessary recomputes - * and is only recomputed when one of its direct dependencies changes. - * - * 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#createcomputed) - * - * @public - * @param computeFunction - Function to compute the computed value. - * @param deps - Hard-coded dependencies on which the Computed Class should depend. - */ -export function createComputed( - computeFunction: ComputeFunctionType, - deps?: Array -): Computed; -export function createComputed( - computeFunction: ComputeFunctionType, - configOrDeps?: - | CreateComputedConfigInterface - | Array -): Computed { - let _config: CreateComputedConfigInterfaceWithAgile = {}; - - if (Array.isArray(configOrDeps)) { - _config = defineConfig(_config, { - computedDeps: configOrDeps, - }); - } else { - if (configOrDeps) _config = configOrDeps; - } - - _config = defineConfig(_config, { agileInstance: sharedAgileInstance }); - - return new Computed( - _config.agileInstance as any, - computeFunction, - removeProperties(_config, ['agileInstance']) - ); -} - export interface CreateAgileSubInstanceInterface { /** * Instance of Agile the Instance belongs to. @@ -186,11 +37,3 @@ export interface CreateAgileSubInstanceInterface { */ agileInstance?: Agile; } - -export interface CreateStateConfigInterfaceWithAgile - extends CreateAgileSubInstanceInterface, - StateConfigInterface {} - -export interface CreateComputedConfigInterfaceWithAgile - extends CreateAgileSubInstanceInterface, - CreateComputedConfigInterface {} diff --git a/packages/core/src/state/index.ts b/packages/core/src/state/index.ts index 89bded3c..375edc62 100644 --- a/packages/core/src/state/index.ts +++ b/packages/core/src/state/index.ts @@ -1,23 +1,23 @@ +import { StatePersistent } from './state.persistent'; +import { Agile } from '../agile'; import { - Agile, - StorageKey, copy, - flatMerge, - isValidObject, - StateObserver, - StatePersistent, - Observer, + defineConfig, equal, + flatMerge, + generateId, isFunction, + isValidObject, notEqual, - generateId, - PersistentKey, - ComputedTracker, - StateIngestConfigInterface, removeProperties, - LogCodeManager, - defineConfig, -} from '../internal'; +} from '@agile-ts/utils'; +import { ComputedTracker } from '../computed/computed.tracker'; +import { StateIngestConfigInterface, StateObserver } from './state.observer'; +import { LogCodeManager } from '../logCodeManager'; +import { PersistentKey } from '../storages/persistent'; +import { Observer } from '../runtime/observer'; +import { StorageKey } from '../storages/storage'; +import { CreateAgileSubInstanceInterface, shared } from '../shared'; export class State { // Agile Instance the State belongs to @@ -760,6 +760,39 @@ export class State { } } +/** + * Returns a newly created State. + * + * A State manages 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. + * + * You can create as many global 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 = {} +): State { + config = defineConfig(config, { + agileInstance: shared, + }); + return new State( + config.agileInstance as any, + initialValue, + removeProperties(config, ['agileInstance']) + ); +} + +export interface CreateStateConfigInterfaceWithAgile + extends CreateAgileSubInstanceInterface, + StateConfigInterface {} + export type StateKey = string | number; export interface StateObserversInterface { diff --git a/packages/core/src/state/state.observer.ts b/packages/core/src/state/state.observer.ts index dc1711b3..bd7f01d8 100644 --- a/packages/core/src/state/state.observer.ts +++ b/packages/core/src/state/state.observer.ts @@ -1,21 +1,21 @@ +import { Observer, ObserverKey } from '../runtime/observer'; +import { SideEffectInterface, State } from './index'; import { - Observer, - State, - Computed, copy, + createArrayFromObject, + defineConfig, equal, - notEqual, + generateId, isFunction, - IngestConfigInterface, - StateRuntimeJob, - SideEffectInterface, - createArrayFromObject, + notEqual, +} from '@agile-ts/utils'; +import { Computed } from '../computed'; +import { CreateStateRuntimeJobConfigInterface, - generateId, - SubscriptionContainer, - ObserverKey, - defineConfig, -} from '../internal'; + StateRuntimeJob, +} from './state.runtime.job'; +import { SubscriptionContainer } from '../runtime/subscription/container/SubscriptionContainer'; +import { IngestConfigInterface } from '../runtime'; export class StateObserver extends Observer { // State the Observer belongs to diff --git a/packages/core/src/state/state.persistent.ts b/packages/core/src/state/state.persistent.ts index d978196c..d151e37a 100644 --- a/packages/core/src/state/state.persistent.ts +++ b/packages/core/src/state/state.persistent.ts @@ -1,10 +1,10 @@ import { CreatePersistentConfigInterface, - defineConfig, Persistent, PersistentKey, - State, -} from '../internal'; +} from '../storages/persistent'; +import { State } from './index'; +import { defineConfig } from '@agile-ts/utils'; export class StatePersistent extends Persistent { // State the Persistent belongs to diff --git a/packages/core/src/state/state.runtime.job.ts b/packages/core/src/state/state.runtime.job.ts index 16e1628a..1b6699c7 100644 --- a/packages/core/src/state/state.runtime.job.ts +++ b/packages/core/src/state/state.runtime.job.ts @@ -1,10 +1,10 @@ import { - defineConfig, RuntimeJob, RuntimeJobConfigInterface, RuntimeJobKey, - StateObserver, -} from '../internal'; +} from '../runtime/runtime.job'; +import { StateObserver } from './state.observer'; +import { defineConfig } from '@agile-ts/utils'; export class StateRuntimeJob extends RuntimeJob { public config: StateRuntimeJobConfigInterface; diff --git a/packages/core/src/storages/index.ts b/packages/core/src/storages/index.ts index 498f9bb5..fcbf778d 100644 --- a/packages/core/src/storages/index.ts +++ b/packages/core/src/storages/index.ts @@ -1,13 +1,8 @@ -import { - Agile, - Storage, - Persistent, - StorageKey, - StorageItemKey, - notEqual, - LogCodeManager, - defineConfig, -} from '../internal'; +import { Agile } from '../agile'; +import { Persistent } from './persistent'; +import { defineConfig, notEqual } from '@agile-ts/utils'; +import { LogCodeManager } from '../logCodeManager'; +import { Storage, StorageItemKey, StorageKey } from './storage'; export class Storages { // Agile Instance the Storages belongs to diff --git a/packages/core/src/storages/persistent.ts b/packages/core/src/storages/persistent.ts index e4134fca..2efb66be 100644 --- a/packages/core/src/storages/persistent.ts +++ b/packages/core/src/storages/persistent.ts @@ -1,10 +1,7 @@ -import { - Agile, - copy, - defineConfig, - LogCodeManager, - StorageKey, -} from '../internal'; +import { Agile } from '../agile'; +import { StorageKey } from './storage'; +import { copy, defineConfig } from '@agile-ts/utils'; +import { LogCodeManager } from '../logCodeManager'; export class Persistent { // Agile Instance the Persistent belongs to diff --git a/packages/core/src/storages/storage.ts b/packages/core/src/storages/storage.ts index 0f200cd6..a1b70e28 100644 --- a/packages/core/src/storages/storage.ts +++ b/packages/core/src/storages/storage.ts @@ -2,9 +2,9 @@ import { isJsonString, isAsyncFunction, isFunction, - LogCodeManager, defineConfig, -} from '../internal'; +} from '@agile-ts/utils'; +import { LogCodeManager } from '../logCodeManager'; export class Storage { public config: StorageConfigInterface; @@ -184,6 +184,25 @@ export class Storage { } } +/** + * Returns a newly created Storage. + * + * A Storage Class serves as an interface to external storages, + * such as the [Async Storage](https://github.com/react-native-async-storage/async-storage) or + * [Local Storage](https://www.w3schools.com/html/html5_webstorage.asp). + * + * It creates the foundation to easily [`persist()`](https://agile-ts.org/docs/core/state/methods#persist) [Agile Sub Instances](https://agile-ts.org/docs/introduction/#agile-sub-instance) + * (like States or Collections) in nearly any external storage. + * + * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#createstorage) + * + * @public + * @param config - Configuration object + */ +export function createStorage(config: CreateStorageConfigInterface): Storage { + return new Storage(config); +} + export type StorageKey = string | number; export type StorageItemKey = string | number; diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index 8748f28e..a75b60d7 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -1,12 +1,8 @@ -import { - Agile, - Observer, - Collection, - normalizeArray, - isFunction, - LogCodeManager, - shared, -} from './internal'; +import { isFunction, normalizeArray } from '@agile-ts/utils'; +import { Agile } from './agile'; +import { shared } from './shared'; +import { LogCodeManager } from './logCodeManager'; +import { Observer } from './runtime/observer'; /** * Extracts an Instance of Agile from the specified Instance. @@ -95,14 +91,15 @@ export function extractObservers( continue; } + // TODO this use of the Collection avoid tree shaking it // If the Instance equals to a Collection - if (instance instanceof Collection) { - observers.push( - instance.getGroupWithReference(instance.config.defaultGroupKey) - .observers as any - ); - continue; - } + // if (instance instanceof Collection) { + // observers.push( + // instance.getGroupWithReference(instance.config.defaultGroupKey) + // .observers as any + // ); + // continue; + // } // If the Instance contains a property that is an Observer if (instance['observer'] && instance['observer'] instanceof Observer) { From e805e058ecba530d8db50b432b877380bca39c3e Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Fri, 30 Jul 2021 15:05:07 +0200 Subject: [PATCH 04/93] fixed typos --- packages/core/src/index.ts | 21 ++++++++++++++++++++- packages/core/src/utils.ts | 15 ++++++++------- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index ced21915..445fc460 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,6 +1,6 @@ import { Agile } from './agile'; -export * from './storages'; +// Required export * from './storages/storage'; export * from './state'; export * from './integrations/integration'; @@ -12,3 +12,22 @@ export * from './collection/selector'; export * from './shared'; export default Agile; + +// For unit testing +export * from './storages'; +export * from './storages/persistent'; +export * from './state/state.observer'; +export * from './state/state.persistent'; +export * from './state/state.runtime.job'; +export * from './runtime'; +export * from './runtime/observer'; +export * from './runtime/runtime.job'; +export * from './runtime/subscription/sub.controller'; +export * from './runtime/subscription/container/SubscriptionContainer'; +export * from './runtime/subscription/container/CallbackSubscriptionContainer'; +export * from './runtime/subscription/container/ComponentSubscriptionContainer'; +export * from './integrations'; +export * from './integrations/integration'; +export * from './computed/computed.tracker'; +export * from './collection/collection.persistent'; +export * from './collection/group/group.observer'; diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index a75b60d7..f73f1bdb 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -3,6 +3,7 @@ import { Agile } from './agile'; import { shared } from './shared'; import { LogCodeManager } from './logCodeManager'; import { Observer } from './runtime/observer'; +import { Collection } from './collection'; /** * Extracts an Instance of Agile from the specified Instance. @@ -93,13 +94,13 @@ export function extractObservers( // TODO this use of the Collection avoid tree shaking it // If the Instance equals to a Collection - // if (instance instanceof Collection) { - // observers.push( - // instance.getGroupWithReference(instance.config.defaultGroupKey) - // .observers as any - // ); - // continue; - // } + if (instance instanceof Collection) { + observers.push( + instance.getGroupWithReference(instance.config.defaultGroupKey) + .observers as any + ); + continue; + } // If the Instance contains a property that is an Observer if (instance['observer'] && instance['observer'] instanceof Observer) { From 98afcacd14babb9a7b5d6a52af28681bffddcf1a Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sat, 31 Jul 2021 07:54:58 +0200 Subject: [PATCH 05/93] optimized treeshaking collection --- .../plainjs/develop/tree-shaking/package.json | 8 +- .../plainjs/develop/tree-shaking/src/index.js | 7 +- .../functional-component-ts/src/App.tsx | 6 +- .../functional-component-ts/src/core/index.ts | 27 +++-- packages/core/src/collection/index.ts | 4 + packages/core/src/index.ts | 6 +- packages/core/src/utils.ts | 4 +- packages/react/src/hocs/AgileHOC.ts | 5 +- packages/react/src/hooks/useAgile.ts | 8 +- packages/react/tests/old/useAgile.spec.ts | 101 ++++++++++-------- 10 files changed, 100 insertions(+), 76 deletions(-) diff --git a/examples/plainjs/develop/tree-shaking/package.json b/examples/plainjs/develop/tree-shaking/package.json index 9f6bb167..53515c71 100644 --- a/examples/plainjs/develop/tree-shaking/package.json +++ b/examples/plainjs/develop/tree-shaking/package.json @@ -6,8 +6,8 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "webpack", - "install:dev:agile": "yalc add @agile-ts/core & yarn install", - "install:prod:agile": "yarn add @agile-ts/core & yarn install" + "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" }, "author": "", "license": "ISC", @@ -16,6 +16,8 @@ "webpack-cli": "^4.7.2" }, "dependencies": { - "@agile-ts/core": "file:.yalc/@agile-ts/core" + "@agile-ts/core": "file:.yalc/@agile-ts/core", + "@agile-ts/react": "file:.yalc/@agile-ts/react", + "react": "^17.0.2" } } diff --git a/examples/plainjs/develop/tree-shaking/src/index.js b/examples/plainjs/develop/tree-shaking/src/index.js index f20e209d..9d713ce5 100644 --- a/examples/plainjs/develop/tree-shaking/src/index.js +++ b/examples/plainjs/develop/tree-shaking/src/index.js @@ -1,5 +1,8 @@ -import {createState} from '@agile-ts/core'; +import { createState } from '@agile-ts/core'; +import { useAgile } from '@agile-ts/react'; const MY_STATE = createState('hi'); -console.log(MY_STATE.value) +console.log(MY_STATE.value); + +useAgile(MY_STATE); diff --git a/examples/react/develop/functional-component-ts/src/App.tsx b/examples/react/develop/functional-component-ts/src/App.tsx index 4c4d8f35..40ecce70 100644 --- a/examples/react/develop/functional-component-ts/src/App.tsx +++ b/examples/react/develop/functional-component-ts/src/App.tsx @@ -13,13 +13,13 @@ import { MY_STATE_3, STATE_OBJECT, } from './core'; -import { generateId, globalBind, Item } from '@agile-ts/core'; +import { generateId } from '@agile-ts/utils'; let rerenderCount = 0; let rerenderCountInCountupView = 0; const App = (props: any) => { - // Note: Rerenders twice because of React Strickt Mode (also useState does trigger a rerender twice) + // Note: re-renders twice because of React strict Mode (also useState does trigger a rerender twice) // https://stackoverflow.com/questions/54927622/usestate-do-double-render rerenderCount++; @@ -69,7 +69,7 @@ const App = (props: any) => { // Create global Instance of Core (for better debugging) useEffect(() => { - globalBind('__core__', { ...require('./core') }); + // globalBind('__core__', { ...require('./core') }); }, []); const CountupView = () => { 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 c98da0af..f2f4efe8 100644 --- a/examples/react/develop/functional-component-ts/src/core/index.ts +++ b/examples/react/develop/functional-component-ts/src/core/index.ts @@ -1,6 +1,14 @@ -import { Agile, clone, Item } from '@agile-ts/core'; +import Agile, { + assignSharedAgileInstance, + createCollection, + createComputed, + createState, + createStorage, + Item, +} from '@agile-ts/core'; import Event from '@agile-ts/event'; import { assignSharedAgileLoggerConfig, Logger } from '@agile-ts/logger'; +import { clone } from '@agile-ts/utils'; export const myStorage: any = {}; @@ -8,10 +16,11 @@ assignSharedAgileLoggerConfig({ level: Logger.level.DEBUG }); export const App = new Agile({ localStorage: true, }); +assignSharedAgileInstance(App); // Register custom second Storage App.registerStorage( - App.createStorage({ + createStorage({ key: 'myStorage', methods: { get: (key: string) => { @@ -30,7 +39,7 @@ App.registerStorage( }) ); -export const STATE_OBJECT = App.createState( +export const STATE_OBJECT = createState( { name: 'frank', age: 10, @@ -43,9 +52,9 @@ export const STATE_OBJECT = App.createState( }, { key: 'state-object' } ); -export const COUNTUP = App.createState(1); // .interval((value) => value + 1, 1000); -export const MY_STATE = App.createState('MyState', { key: 'my-state' }); //.persist(); -export const MY_STATE_2 = App.createState('MyState2', { +export const COUNTUP = createState(1); // .interval((value) => value + 1, 1000); +export const MY_STATE = createState('MyState', { key: 'my-state' }); //.persist(); +export const MY_STATE_2 = createState('MyState2', { key: 'my-state2', }).persist({ storageKeys: ['myStorage', 'localStorage'], @@ -54,13 +63,13 @@ export const MY_STATE_2 = App.createState('MyState2', { MY_STATE_2.onLoad(() => { console.log('On Load MY_STATE_2'); }); -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'); @@ -69,7 +78,7 @@ interface collectionValueInterface { name: string; } -export const MY_COLLECTION = App.createCollection( +export const MY_COLLECTION = createCollection( (collection) => ({ key: 'my-collection', primaryKey: 'key', diff --git a/packages/core/src/collection/index.ts b/packages/core/src/collection/index.ts index 58788b04..9e33e60e 100644 --- a/packages/core/src/collection/index.ts +++ b/packages/core/src/collection/index.ts @@ -53,6 +53,10 @@ export class Collection< // Whether the Collection was instantiated correctly public isInstantiated = false; + // Helper property to check whether an unknown instance is a Collection + // without importing the Collection for properly using 'instanceof' (Treeshaking support) + public isCollection = true; + /** * A Collection manages a reactive set of Information * that we need to remember globally at a later point in time. diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 445fc460..d42eb68f 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,15 +1,19 @@ import { Agile } from './agile'; // Required +export * from './agile'; +export * from './integrations/integration'; export * from './storages/storage'; export * from './state'; -export * from './integrations/integration'; export * from './computed'; export * from './collection'; export * from './collection/item'; export * from './collection/group'; export * from './collection/selector'; export * from './shared'; +export * from './utils'; // Needed by external package +export * from './logCodeManager'; // Needed by external package +export * from '@agile-ts/utils'; export default Agile; diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index f73f1bdb..351a03ed 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -3,7 +3,6 @@ import { Agile } from './agile'; import { shared } from './shared'; import { LogCodeManager } from './logCodeManager'; import { Observer } from './runtime/observer'; -import { Collection } from './collection'; /** * Extracts an Instance of Agile from the specified Instance. @@ -92,9 +91,8 @@ export function extractObservers( continue; } - // TODO this use of the Collection avoid tree shaking it // If the Instance equals to a Collection - if (instance instanceof Collection) { + if (instance.isCollection) { observers.push( instance.getGroupWithReference(instance.config.defaultGroupKey) .observers as any diff --git a/packages/react/src/hocs/AgileHOC.ts b/packages/react/src/hocs/AgileHOC.ts index 82055a7d..99e161d2 100644 --- a/packages/react/src/hocs/AgileHOC.ts +++ b/packages/react/src/hocs/AgileHOC.ts @@ -1,17 +1,16 @@ import React, { ComponentClass } from 'react'; -import { +import Agile, { State, - Agile, ComponentSubscriptionContainer, getAgileInstance, Observer, - Collection, isValidObject, flatMerge, extractRelevantObservers, normalizeArray, LogCodeManager, } from '@agile-ts/core'; +import type { Collection } from '@agile-ts/core'; // Only import Collection and Group type for better Treeshaking /** * A Higher order Component for binding the most relevant value of multiple Agile Instances diff --git a/packages/react/src/hooks/useAgile.ts b/packages/react/src/hooks/useAgile.ts index 41de3840..1cb215cc 100644 --- a/packages/react/src/hooks/useAgile.ts +++ b/packages/react/src/hooks/useAgile.ts @@ -1,7 +1,5 @@ import React from 'react'; -import { - Agile, - Collection, +import Agile, { getAgileInstance, Observer, State, @@ -16,8 +14,8 @@ import { LogCodeManager, normalizeArray, defineConfig, - Group, } 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 @@ -210,7 +208,7 @@ export function useAgile< export type SubscribableAgileInstancesType = | State - | Collection //https://stackoverflow.com/questions/66987727/type-classa-id-number-name-string-is-not-assignable-to-type-classar + | Collection //https://stackoverflow.com/questions/66987727/type-classa-id-number-name-string-is-not-assignable-to-type-classar | Observer | undefined; diff --git a/packages/react/tests/old/useAgile.spec.ts b/packages/react/tests/old/useAgile.spec.ts index 6a8c9b4e..58f2254c 100644 --- a/packages/react/tests/old/useAgile.spec.ts +++ b/packages/react/tests/old/useAgile.spec.ts @@ -2,51 +2,58 @@ // THIS ARE ONLY TYPE TESTS // NOTE: Has to be out commented because React Hooks in not React Components are not possible! -/* -import { Agile, Collection } from '@agile-ts/core'; -import { useAgile } from './hooks/useAgile'; - -const App = new Agile(); - -const MY_NUMBER_STATE = App.createState(1); -const MY_STRING_STATE = App.createState('test'); -const MY_STRING_COMPUTED = App.createComputed( - () => MY_STRING_STATE.value + ' ' + MY_NUMBER_STATE.value -); -const MY_COLLECTION = App.createCollection<{ id: string; name: 'Test' }>(); -const MY_SELECTOR = MY_COLLECTION.getSelector(1); - -const [ - myStringState, - mySelector, - myNumberState, - myStringComputed, - myCollection, - myGroup, - myNumberState2, -] = useAgile([ - MY_STRING_STATE, - MY_SELECTOR, - MY_NUMBER_STATE, - MY_STRING_COMPUTED, - MY_COLLECTION as any, - MY_COLLECTION.getGroup('test'), - MY_NUMBER_STATE, -]); - -const myStringState2 = useAgile(MY_STRING_STATE); -const [myGroup2, myStringState3, myCollection2] = useAgile([ - MY_COLLECTION.getGroup('test'), - MY_STRING_STATE, - MY_COLLECTION as any, -]); - -const myState10 = useAgile(MY_NUMBER_STATE); -const myCollection10 = useAgile(MY_COLLECTION); -const myCollection11 = useAgile( - new Collection<{ id: number; name: string }>(App) -); +// Be aware that in the test folder other ts rules count than in the src folder +// THIS ARE ONLY TYPE TESTS +// NOTE: Has to be out commented because React Hooks in not React Components are not possible! -const myGroupValue = useValue(MY_COLLECTION.getGroup('test')); -const myGroupAgile = useAgile(MY_COLLECTION.getGroup('test')); - */ +// import { +// Collection, +// createCollection, +// createComputed, +// createState, +// shared, +// } from '@agile-ts/core'; +// import { useAgile } from './useAgile'; +// import { useValue } from './useValue'; +// +// const MY_NUMBER_STATE = createState(1); +// const MY_STRING_STATE = createState('test'); +// const MY_STRING_COMPUTED = createComputed( +// () => MY_STRING_STATE.value + ' ' + MY_NUMBER_STATE.value +// ); +// const MY_COLLECTION = createCollection<{ id: string; name: 'Test' }>(); +// const MY_SELECTOR = MY_COLLECTION.getSelector(1); +// +// const [ +// myStringState, +// mySelector, +// myNumberState, +// myStringComputed, +// myCollection, +// myGroup, +// myNumberState2, +// ] = useAgile([ +// MY_STRING_STATE, +// MY_SELECTOR, +// MY_NUMBER_STATE, +// MY_STRING_COMPUTED, +// MY_COLLECTION as any, +// MY_COLLECTION.getGroup('test'), +// MY_NUMBER_STATE, +// ]); +// +// const myStringState2 = useAgile(MY_STRING_STATE); +// const [myGroup2, myStringState3, myCollection2] = useAgile([ +// MY_COLLECTION.getGroup('test'), +// MY_STRING_STATE, +// MY_COLLECTION as any, +// ]); +// +// const myState10 = useAgile(MY_NUMBER_STATE); +// const myCollection10 = useAgile(MY_COLLECTION); +// const myCollection11 = useAgile( +// new Collection<{ id: number; name: string }>(shared) +// ); +// +// const myGroupValue = useValue(MY_COLLECTION.getGroup('test')); +// const myGroupAgile = useAgile(MY_COLLECTION.getGroup('test')); From 0afa0348ea0ce4ed7193a46fab46287291f2b9bd Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sun, 1 Aug 2021 06:52:03 +0200 Subject: [PATCH 06/93] readded internal because of import order error --- packages/core/src/agile.ts | 22 +++++--- .../src/collection/collection.persistent.ts | 16 ++++-- .../src/collection/group/group.observer.ts | 19 ++++--- packages/core/src/collection/group/index.ts | 32 +++++------ packages/core/src/collection/index.ts | 42 +++++++------- packages/core/src/collection/item.ts | 20 ++++--- packages/core/src/collection/selector.ts | 14 +++-- .../core/src/computed/computed.tracker.ts | 2 +- packages/core/src/computed/index.ts | 26 +++++---- packages/core/src/index.ts | 37 +------------ packages/core/src/integrations/index.ts | 5 +- packages/core/src/integrations/integration.ts | 2 +- packages/core/src/internal.ts | 55 +++++++++++++++++++ packages/core/src/runtime/index.ts | 17 +++--- packages/core/src/runtime/observer.ts | 18 +++--- packages/core/src/runtime/runtime.job.ts | 4 +- .../CallbackSubscriptionContainer.ts | 4 +- .../ComponentSubscriptionContainer.ts | 4 +- .../container/SubscriptionContainer.ts | 8 ++- .../runtime/subscription/sub.controller.ts | 16 +++--- packages/core/src/shared.ts | 13 +---- packages/core/src/state/index.ts | 30 +++++----- packages/core/src/state/state.observer.ts | 26 ++++----- packages/core/src/state/state.persistent.ts | 6 +- packages/core/src/state/state.runtime.job.ts | 6 +- packages/core/src/storages/index.ts | 15 +++-- packages/core/src/storages/persistent.ts | 11 ++-- packages/core/src/storages/storage.ts | 4 +- packages/core/src/utils.ts | 13 +++-- 29 files changed, 269 insertions(+), 218 deletions(-) create mode 100644 packages/core/src/internal.ts diff --git a/packages/core/src/agile.ts b/packages/core/src/agile.ts index 05f2df7d..23a02432 100644 --- a/packages/core/src/agile.ts +++ b/packages/core/src/agile.ts @@ -1,12 +1,16 @@ -import { Runtime } from './runtime'; -import { SubController } from './runtime/subscription/sub.controller'; -import { RegisterConfigInterface, Storages } from './storages'; -import { Storage } from './storages/storage'; -import { Integrations, IntegrationsConfigInterface } from './integrations'; -import { defineConfig } from '@agile-ts/utils'; -import { LogCodeManager } from './logCodeManager'; -import { globalBind } from './utils'; -import { Integration } from './integrations/integration'; +import { + Runtime, + Integration, + Storage, + Integrations, + SubController, + globalBind, + Storages, + RegisterConfigInterface, + LogCodeManager, + IntegrationsConfigInterface, + defineConfig, +} from './internal'; export class Agile { public config: AgileConfigInterface; diff --git a/packages/core/src/collection/collection.persistent.ts b/packages/core/src/collection/collection.persistent.ts index 0c7a06e9..2f2b7136 100644 --- a/packages/core/src/collection/collection.persistent.ts +++ b/packages/core/src/collection/collection.persistent.ts @@ -1,13 +1,17 @@ -import { Collection, CollectionKey, DefaultItem, ItemKey } from './index'; import { + Collection, + CollectionKey, CreatePersistentConfigInterface, + DefaultItem, + defineConfig, + Group, + GroupKey, + ItemKey, + LogCodeManager, Persistent, PersistentKey, -} from '../storages/persistent'; -import { defineConfig } from '@agile-ts/utils'; -import { Group, GroupKey } from './group'; -import { LogCodeManager } from '../logCodeManager'; -import { StorageKey } from '../storages/storage'; + StorageKey, +} from '../internal'; export class CollectionPersistent< DataType extends Object = DefaultItem diff --git a/packages/core/src/collection/group/group.observer.ts b/packages/core/src/collection/group/group.observer.ts index 305a03f7..4c0e860c 100644 --- a/packages/core/src/collection/group/group.observer.ts +++ b/packages/core/src/collection/group/group.observer.ts @@ -1,15 +1,16 @@ import { - CreateObserverConfigInterface, Observer, -} from '../../runtime/observer'; -import { Group } from './index'; -import { copy, defineConfig, equal, generateId } from '@agile-ts/utils'; -import { Item } from '../item'; -import { - CreateRuntimeJobConfigInterface, + Group, + CreateObserverConfigInterface, + copy, + equal, + generateId, RuntimeJob, -} from '../../runtime/runtime.job'; -import { IngestConfigInterface } from '../../runtime'; + Item, + IngestConfigInterface, + CreateRuntimeJobConfigInterface, + defineConfig, +} from '../../internal'; export class GroupObserver extends Observer { // Group the Observer belongs to diff --git a/packages/core/src/collection/group/index.ts b/packages/core/src/collection/group/index.ts index d85e88e8..44aef2c5 100644 --- a/packages/core/src/collection/group/index.ts +++ b/packages/core/src/collection/group/index.ts @@ -1,26 +1,24 @@ -import { Collection, DefaultItem, ItemKey } from '../index'; import { State, - StateObserversInterface, + Collection, + DefaultItem, + ItemKey, + normalizeArray, + Item, + copy, + CollectionPersistent, StatePersistentConfigInterface, -} from '../../state'; -import { GroupObserver } from './group.observer'; -import { + isValidObject, + PersistentKey, + ComputedTracker, StateIngestConfigInterface, + removeProperties, + LogCodeManager, + StateObserversInterface, + GroupObserver, StateObserver, -} from '../../state/state.observer'; -import { LogCodeManager } from '../../logCodeManager'; -import { ComputedTracker } from '../../computed/computed.tracker'; -import { - copy, defineConfig, - isValidObject, - normalizeArray, - removeProperties, -} from '@agile-ts/utils'; -import { Item } from '../item'; -import { PersistentKey } from '../../storages/persistent'; -import { CollectionPersistent } from '../collection.persistent'; +} from '../../internal'; export class Group< DataType extends Object = DefaultItem, diff --git a/packages/core/src/collection/index.ts b/packages/core/src/collection/index.ts index 9e33e60e..4bab336f 100644 --- a/packages/core/src/collection/index.ts +++ b/packages/core/src/collection/index.ts @@ -1,28 +1,28 @@ -import { Agile } from '../agile'; -import { Item } from './item'; -import { CollectionPersistent } from './collection.persistent'; import { - copy, - defineConfig, - generateId, - isFunction, + Agile, + Item, + Group, + GroupKey, + Selector, + SelectorKey, + StorageKey, + GroupConfigInterface, isValidObject, normalizeArray, - removeProperties, -} from '@agile-ts/utils'; -import { + copy, + CollectionPersistent, GroupAddConfigInterface, - GroupConfigInterface, - GroupKey, -} from './group'; -import { LogCodeManager } from '../logCodeManager'; -import { Selector, SelectorConfigInterface, SelectorKey } from './selector'; -import { Group } from './group'; -import { ComputedTracker } from '../computed/computed.tracker'; -import { StorageKey } from '../storages/storage'; -import { SideEffectConfigInterface } from '../runtime/runtime.job'; -import { PatchOptionConfigInterface } from '../state'; -import { shared } from '../shared'; + ComputedTracker, + generateId, + SideEffectConfigInterface, + SelectorConfigInterface, + removeProperties, + isFunction, + LogCodeManager, + PatchOptionConfigInterface, + defineConfig, + shared, +} from '../internal'; export class Collection< DataType extends Object = DefaultItem, diff --git a/packages/core/src/collection/item.ts b/packages/core/src/collection/item.ts index 491af2a9..1be4cebf 100644 --- a/packages/core/src/collection/item.ts +++ b/packages/core/src/collection/item.ts @@ -1,10 +1,16 @@ -import { Collection, DefaultItem } from './index'; -import { State, StateKey, StatePersistentConfigInterface } from '../state'; -import { SelectorKey } from './selector'; -import { StateRuntimeJobConfigInterface } from '../state/state.runtime.job'; -import { defineConfig, isValidObject } from '@agile-ts/utils'; -import { PersistentKey } from '../storages/persistent'; -import { CollectionPersistent } from './collection.persistent'; +import { + State, + Collection, + StateKey, + StateRuntimeJobConfigInterface, + SelectorKey, + PersistentKey, + isValidObject, + CollectionPersistent, + StatePersistentConfigInterface, + DefaultItem, + defineConfig, +} from '../internal'; export class Item extends State< DataType diff --git a/packages/core/src/collection/selector.ts b/packages/core/src/collection/selector.ts index 68be7df6..937c5458 100644 --- a/packages/core/src/collection/selector.ts +++ b/packages/core/src/collection/selector.ts @@ -1,8 +1,12 @@ -import { Collection, DefaultItem, ItemKey } from './index'; -import { State } from '../state'; -import { Item } from './item'; -import { StateRuntimeJobConfigInterface } from '../state/state.runtime.job'; -import { defineConfig } from '@agile-ts/utils'; +import { + Collection, + DefaultItem, + defineConfig, + Item, + ItemKey, + State, + StateRuntimeJobConfigInterface, +} from '../internal'; export class Selector< DataType extends Object = DefaultItem diff --git a/packages/core/src/computed/computed.tracker.ts b/packages/core/src/computed/computed.tracker.ts index e3254f90..c9c7ecc6 100644 --- a/packages/core/src/computed/computed.tracker.ts +++ b/packages/core/src/computed/computed.tracker.ts @@ -1,4 +1,4 @@ -import { Observer } from '../runtime/observer'; +import { Observer } from '../internal'; export class ComputedTracker { static isTracking = false; diff --git a/packages/core/src/computed/index.ts b/packages/core/src/computed/index.ts index 58c53807..bdb39aff 100644 --- a/packages/core/src/computed/index.ts +++ b/packages/core/src/computed/index.ts @@ -1,17 +1,19 @@ -import { State, StateConfigInterface } from '../state'; -import { Observer } from '../runtime/observer'; -import { Agile } from '../agile'; import { - defineConfig, - isAsyncFunction, + State, + Agile, + Observer, + StateConfigInterface, + ComputedTracker, + Collection, + StateIngestConfigInterface, removeProperties, -} from '@agile-ts/utils'; -import { extractRelevantObservers } from '../utils'; -import { ComputedTracker } from './computed.tracker'; -import { LogCodeManager } from '../logCodeManager'; -import { StateIngestConfigInterface } from '../state/state.observer'; -import { Collection } from '../collection'; -import { CreateAgileSubInstanceInterface, shared } from '../shared'; + LogCodeManager, + isAsyncFunction, + extractRelevantObservers, + defineConfig, + CreateAgileSubInstanceInterface, + shared, +} from '../internal'; export class Computed extends State< ComputedValueType diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index d42eb68f..6469f1fa 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,37 +1,4 @@ -import { Agile } from './agile'; - -// Required -export * from './agile'; -export * from './integrations/integration'; -export * from './storages/storage'; -export * from './state'; -export * from './computed'; -export * from './collection'; -export * from './collection/item'; -export * from './collection/group'; -export * from './collection/selector'; -export * from './shared'; -export * from './utils'; // Needed by external package -export * from './logCodeManager'; // Needed by external package -export * from '@agile-ts/utils'; +import { Agile } from './internal'; +export * from './internal'; export default Agile; - -// For unit testing -export * from './storages'; -export * from './storages/persistent'; -export * from './state/state.observer'; -export * from './state/state.persistent'; -export * from './state/state.runtime.job'; -export * from './runtime'; -export * from './runtime/observer'; -export * from './runtime/runtime.job'; -export * from './runtime/subscription/sub.controller'; -export * from './runtime/subscription/container/SubscriptionContainer'; -export * from './runtime/subscription/container/CallbackSubscriptionContainer'; -export * from './runtime/subscription/container/ComponentSubscriptionContainer'; -export * from './integrations'; -export * from './integrations/integration'; -export * from './computed/computed.tracker'; -export * from './collection/collection.persistent'; -export * from './collection/group/group.observer'; diff --git a/packages/core/src/integrations/index.ts b/packages/core/src/integrations/index.ts index d54802d7..c13e081b 100644 --- a/packages/core/src/integrations/index.ts +++ b/packages/core/src/integrations/index.ts @@ -1,7 +1,4 @@ -import { Agile } from '../agile'; -import { Integration } from './integration'; -import { defineConfig } from '@agile-ts/utils'; -import { LogCodeManager } from '../logCodeManager'; +import { Agile, Integration, LogCodeManager, defineConfig } from '../internal'; const onRegisterInitialIntegrationCallbacks: (( integration: Integration diff --git a/packages/core/src/integrations/integration.ts b/packages/core/src/integrations/integration.ts index 2acb6eb9..f2c409ac 100644 --- a/packages/core/src/integrations/integration.ts +++ b/packages/core/src/integrations/integration.ts @@ -1,4 +1,4 @@ -import { Agile } from '../agile'; +import { Agile } from '../internal'; export class Integration { // Key/Name identifier of the Integration diff --git a/packages/core/src/internal.ts b/packages/core/src/internal.ts new file mode 100644 index 00000000..a1c8852c --- /dev/null +++ b/packages/core/src/internal.ts @@ -0,0 +1,55 @@ +// This file exposes Agile functions and types to the outside world. +// It also serves as a cyclic dependency workaround, +// and allows us to structure the loading order as needed (for example, './agile' need to be loaded before './state') +// https://medium.com/visual-development/how-to-fix-nasty-circular-dependency-issues-once-and-for-all-in-javascript-typescript-a04c987cf0de + +// !! All internal Agile modules must be imported from here!! + +// Utils +export * from './utils'; +export * from '@agile-ts/utils'; + +// Logger +export * from './logCodeManager'; + +// Agile +export * from './agile'; + +// Integrations +export * from './integrations'; +export * from './integrations/integration'; + +// Runtime +export * from './runtime'; +export * from './runtime/observer'; +export * from './runtime/runtime.job'; +export * from './runtime/subscription/container/SubscriptionContainer'; +export * from './runtime/subscription/container/CallbackSubscriptionContainer'; +export * from './runtime/subscription/container/ComponentSubscriptionContainer'; +export * from './runtime/subscription/sub.controller'; + +// Storage +export * from './storages'; +export * from './storages/storage'; +export * from './storages/persistent'; + +// State +export * from './state'; +export * from './state/state.observer'; +export * from './state/state.persistent'; +export * from './state/state.runtime.job'; + +// Computed +export * from './computed'; +export * from './computed/computed.tracker'; + +// Collection +export * from './collection'; +export * from './collection/group'; +export * from './collection/group/group.observer'; +export * from './collection/item'; +export * from './collection/selector'; +export * from './collection/collection.persistent'; + +// Shared +export * from './shared'; diff --git a/packages/core/src/runtime/index.ts b/packages/core/src/runtime/index.ts index 52320848..0dd3419f 100644 --- a/packages/core/src/runtime/index.ts +++ b/packages/core/src/runtime/index.ts @@ -1,10 +1,13 @@ -import { Agile } from '../agile'; -import { RuntimeJob } from './runtime.job'; -import { LogCodeManager } from '../logCodeManager'; -import { defineConfig, notEqual } from '@agile-ts/utils'; -import { SubscriptionContainer } from './subscription/container/SubscriptionContainer'; -import { CallbackSubscriptionContainer } from './subscription/container/CallbackSubscriptionContainer'; -import { ComponentSubscriptionContainer } from './subscription/container/ComponentSubscriptionContainer'; +import { + Agile, + SubscriptionContainer, + RuntimeJob, + CallbackSubscriptionContainer, + ComponentSubscriptionContainer, + notEqual, + LogCodeManager, + defineConfig, +} from '../internal'; export class Runtime { // Agile Instance the Runtime belongs to diff --git a/packages/core/src/runtime/observer.ts b/packages/core/src/runtime/observer.ts index 783e37a0..dc49f1f6 100644 --- a/packages/core/src/runtime/observer.ts +++ b/packages/core/src/runtime/observer.ts @@ -1,10 +1,14 @@ -import { Agile } from '../agile'; -import { SubscriptionContainer } from './subscription/container/SubscriptionContainer'; -import { defineConfig, generateId } from '@agile-ts/utils'; -import { StateKey } from '../state'; -import { CreateRuntimeJobConfigInterface, RuntimeJob } from './runtime.job'; -import { LogCodeManager } from '../logCodeManager'; -import { IngestConfigInterface } from './index'; +import { + Agile, + StateKey, + RuntimeJob, + SubscriptionContainer, + IngestConfigInterface, + CreateRuntimeJobConfigInterface, + LogCodeManager, + generateId, + defineConfig, +} from '../internal'; export type ObserverKey = string | number; diff --git a/packages/core/src/runtime/runtime.job.ts b/packages/core/src/runtime/runtime.job.ts index 4bbdadc9..54748c87 100644 --- a/packages/core/src/runtime/runtime.job.ts +++ b/packages/core/src/runtime/runtime.job.ts @@ -1,6 +1,4 @@ -import { Observer } from './observer'; -import { SubscriptionContainer } from './subscription/container/SubscriptionContainer'; -import { defineConfig } from '@agile-ts/utils'; +import { defineConfig, Observer, SubscriptionContainer } from '../internal'; export class RuntimeJob { public config: RuntimeJobConfigInterface; diff --git a/packages/core/src/runtime/subscription/container/CallbackSubscriptionContainer.ts b/packages/core/src/runtime/subscription/container/CallbackSubscriptionContainer.ts index 211b3b32..bf974bea 100644 --- a/packages/core/src/runtime/subscription/container/CallbackSubscriptionContainer.ts +++ b/packages/core/src/runtime/subscription/container/CallbackSubscriptionContainer.ts @@ -1,8 +1,8 @@ import { + Observer, SubscriptionContainer, SubscriptionContainerConfigInterface, -} from './SubscriptionContainer'; -import { Observer } from '../../observer'; +} from '../../../internal'; export class CallbackSubscriptionContainer extends SubscriptionContainer { /** diff --git a/packages/core/src/runtime/subscription/container/ComponentSubscriptionContainer.ts b/packages/core/src/runtime/subscription/container/ComponentSubscriptionContainer.ts index bfff1cbf..ff80eded 100644 --- a/packages/core/src/runtime/subscription/container/ComponentSubscriptionContainer.ts +++ b/packages/core/src/runtime/subscription/container/ComponentSubscriptionContainer.ts @@ -1,8 +1,8 @@ import { + Observer, SubscriptionContainer, SubscriptionContainerConfigInterface, -} from './SubscriptionContainer'; -import { Observer } from '../../observer'; +} from '../../../internal'; export class ComponentSubscriptionContainer< C = any diff --git a/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts b/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts index 764c47eb..1ef0c89d 100644 --- a/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts +++ b/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts @@ -1,5 +1,9 @@ -import { Observer } from '../../observer'; -import { defineConfig, generateId, isValidObject } from '@agile-ts/utils'; +import { + defineConfig, + generateId, + isValidObject, + Observer, +} from '../../../internal'; export class SubscriptionContainer { /** diff --git a/packages/core/src/runtime/subscription/sub.controller.ts b/packages/core/src/runtime/subscription/sub.controller.ts index b9c03616..51a76d4b 100644 --- a/packages/core/src/runtime/subscription/sub.controller.ts +++ b/packages/core/src/runtime/subscription/sub.controller.ts @@ -1,13 +1,15 @@ -import { Agile } from '../../agile'; -import { ComponentSubscriptionContainer } from './container/ComponentSubscriptionContainer'; -import { CallbackSubscriptionContainer } from './container/CallbackSubscriptionContainer'; -import { Observer } from '../observer'; import { + Agile, + Observer, SubscriptionContainer, + ComponentSubscriptionContainer, + CallbackSubscriptionContainer, + isFunction, SubscriptionContainerConfigInterface, -} from './container/SubscriptionContainer'; -import { defineConfig, isFunction, removeProperties } from '@agile-ts/utils'; -import { LogCodeManager } from '../../logCodeManager'; + removeProperties, + LogCodeManager, + defineConfig, +} from '../../internal'; export class SubController { // Agile Instance the SubController belongs to diff --git a/packages/core/src/shared.ts b/packages/core/src/shared.ts index be26494b..604d1c89 100644 --- a/packages/core/src/shared.ts +++ b/packages/core/src/shared.ts @@ -1,15 +1,4 @@ -import { Agile } from './agile'; -import { runsOnServer } from './utils'; -import { CreateStorageConfigInterface, Storage } from './storages/storage'; -import { State, StateConfigInterface } from './state'; -import { defineConfig, removeProperties } from '@agile-ts/utils'; -import { Collection, CollectionConfig, DefaultItem } from './collection'; -import { - Computed, - ComputeFunctionType, - CreateComputedConfigInterface, - DependableAgileInstancesType, -} from './computed'; +import { Agile, runsOnServer } from './internal'; /** * Shared Agile Instance that is used when no Agile Instance was specified. diff --git a/packages/core/src/state/index.ts b/packages/core/src/state/index.ts index 375edc62..7eaf518f 100644 --- a/packages/core/src/state/index.ts +++ b/packages/core/src/state/index.ts @@ -1,23 +1,25 @@ -import { StatePersistent } from './state.persistent'; -import { Agile } from '../agile'; import { + Agile, + StorageKey, copy, - defineConfig, - equal, flatMerge, - generateId, - isFunction, isValidObject, + StateObserver, + StatePersistent, + Observer, + equal, + isFunction, notEqual, + generateId, + PersistentKey, + ComputedTracker, + StateIngestConfigInterface, removeProperties, -} from '@agile-ts/utils'; -import { ComputedTracker } from '../computed/computed.tracker'; -import { StateIngestConfigInterface, StateObserver } from './state.observer'; -import { LogCodeManager } from '../logCodeManager'; -import { PersistentKey } from '../storages/persistent'; -import { Observer } from '../runtime/observer'; -import { StorageKey } from '../storages/storage'; -import { CreateAgileSubInstanceInterface, shared } from '../shared'; + LogCodeManager, + defineConfig, + shared, + CreateAgileSubInstanceInterface, +} from '../internal'; export class State { // Agile Instance the State belongs to diff --git a/packages/core/src/state/state.observer.ts b/packages/core/src/state/state.observer.ts index bd7f01d8..dc1711b3 100644 --- a/packages/core/src/state/state.observer.ts +++ b/packages/core/src/state/state.observer.ts @@ -1,21 +1,21 @@ -import { Observer, ObserverKey } from '../runtime/observer'; -import { SideEffectInterface, State } from './index'; import { + Observer, + State, + Computed, copy, - createArrayFromObject, - defineConfig, equal, - generateId, - isFunction, notEqual, -} from '@agile-ts/utils'; -import { Computed } from '../computed'; -import { - CreateStateRuntimeJobConfigInterface, + isFunction, + IngestConfigInterface, StateRuntimeJob, -} from './state.runtime.job'; -import { SubscriptionContainer } from '../runtime/subscription/container/SubscriptionContainer'; -import { IngestConfigInterface } from '../runtime'; + SideEffectInterface, + createArrayFromObject, + CreateStateRuntimeJobConfigInterface, + generateId, + SubscriptionContainer, + ObserverKey, + defineConfig, +} from '../internal'; export class StateObserver extends Observer { // State the Observer belongs to diff --git a/packages/core/src/state/state.persistent.ts b/packages/core/src/state/state.persistent.ts index d151e37a..d978196c 100644 --- a/packages/core/src/state/state.persistent.ts +++ b/packages/core/src/state/state.persistent.ts @@ -1,10 +1,10 @@ import { CreatePersistentConfigInterface, + defineConfig, Persistent, PersistentKey, -} from '../storages/persistent'; -import { State } from './index'; -import { defineConfig } from '@agile-ts/utils'; + State, +} from '../internal'; export class StatePersistent extends Persistent { // State the Persistent belongs to diff --git a/packages/core/src/state/state.runtime.job.ts b/packages/core/src/state/state.runtime.job.ts index 1b6699c7..16e1628a 100644 --- a/packages/core/src/state/state.runtime.job.ts +++ b/packages/core/src/state/state.runtime.job.ts @@ -1,10 +1,10 @@ import { + defineConfig, RuntimeJob, RuntimeJobConfigInterface, RuntimeJobKey, -} from '../runtime/runtime.job'; -import { StateObserver } from './state.observer'; -import { defineConfig } from '@agile-ts/utils'; + StateObserver, +} from '../internal'; export class StateRuntimeJob extends RuntimeJob { public config: StateRuntimeJobConfigInterface; diff --git a/packages/core/src/storages/index.ts b/packages/core/src/storages/index.ts index fcbf778d..498f9bb5 100644 --- a/packages/core/src/storages/index.ts +++ b/packages/core/src/storages/index.ts @@ -1,8 +1,13 @@ -import { Agile } from '../agile'; -import { Persistent } from './persistent'; -import { defineConfig, notEqual } from '@agile-ts/utils'; -import { LogCodeManager } from '../logCodeManager'; -import { Storage, StorageItemKey, StorageKey } from './storage'; +import { + Agile, + Storage, + Persistent, + StorageKey, + StorageItemKey, + notEqual, + LogCodeManager, + defineConfig, +} from '../internal'; export class Storages { // Agile Instance the Storages belongs to diff --git a/packages/core/src/storages/persistent.ts b/packages/core/src/storages/persistent.ts index 2efb66be..e4134fca 100644 --- a/packages/core/src/storages/persistent.ts +++ b/packages/core/src/storages/persistent.ts @@ -1,7 +1,10 @@ -import { Agile } from '../agile'; -import { StorageKey } from './storage'; -import { copy, defineConfig } from '@agile-ts/utils'; -import { LogCodeManager } from '../logCodeManager'; +import { + Agile, + copy, + defineConfig, + LogCodeManager, + StorageKey, +} from '../internal'; export class Persistent { // Agile Instance the Persistent belongs to diff --git a/packages/core/src/storages/storage.ts b/packages/core/src/storages/storage.ts index a1b70e28..a03b53d7 100644 --- a/packages/core/src/storages/storage.ts +++ b/packages/core/src/storages/storage.ts @@ -2,9 +2,9 @@ import { isJsonString, isAsyncFunction, isFunction, + LogCodeManager, defineConfig, -} from '@agile-ts/utils'; -import { LogCodeManager } from '../logCodeManager'; +} from '../internal'; export class Storage { public config: StorageConfigInterface; diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index 351a03ed..3272d416 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -1,8 +1,11 @@ -import { isFunction, normalizeArray } from '@agile-ts/utils'; -import { Agile } from './agile'; -import { shared } from './shared'; -import { LogCodeManager } from './logCodeManager'; -import { Observer } from './runtime/observer'; +import { + Agile, + Observer, + normalizeArray, + isFunction, + LogCodeManager, + shared, +} from './internal'; /** * Extracts an Instance of Agile from the specified Instance. From a1da58f771f639dcfe91bfeb2d2090bc843d1f62 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sun, 1 Aug 2021 07:32:39 +0200 Subject: [PATCH 07/93] added release manual command --- packages/core/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index 64e19bd3..646fab42 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -37,7 +37,8 @@ "test": "jest", "test:coverage": "jest --coverage", "lint": "eslint src/**/*", - "size": "yarn run build && size-limit" + "size": "yarn run build && size-limit", + "release:manual": "yarn run prepare && yarn run release && npm publish && git checkout README.md" }, "devDependencies": { "@agile-ts/logger": "file:../logger", From 35d2b88e5ab0f4bcb641ecd1f86bfd9a193d29f7 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sun, 1 Aug 2021 08:04:20 +0200 Subject: [PATCH 08/93] fixed tests --- packages/core/src/collection/index.ts | 26 --- packages/core/src/computed/index.ts | 73 -------- packages/core/src/shared.ts | 169 +++++++++++++++++- packages/core/src/state/index.ts | 33 ---- packages/core/src/storages/storage.ts | 19 -- .../collection.persistent.integration.test.ts | 10 +- packages/core/tests/unit/agile.test.ts | 109 ----------- .../tests/unit/storages/persistent.test.ts | 4 +- 8 files changed, 175 insertions(+), 268 deletions(-) diff --git a/packages/core/src/collection/index.ts b/packages/core/src/collection/index.ts index 4bab336f..c51cba3f 100644 --- a/packages/core/src/collection/index.ts +++ b/packages/core/src/collection/index.ts @@ -1489,32 +1489,6 @@ export class Collection< } } -/** - * Returns a newly created Collection. - * - * A Collection manages a reactive set of Information - * that we need to remember globally at a later point in time. - * While providing a toolkit to use and mutate this set of Information. - * - * It is designed for arrays of data objects following the same pattern. - * - * Each of these data object must have a unique `primaryKey` to be correctly identified later. - * - * You can create as many global Collections as you need. - * - * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#createcollection) - * - * @public - * @param config - Configuration object - * @param agileInstance - Instance of Agile the Collection belongs to. - */ -export function createCollection( - config?: CollectionConfig, - agileInstance: Agile = shared -): Collection { - return new Collection(agileInstance, config); -} - export type DefaultItem = Record; // same as { [key: string]: any }; export type CollectionKey = string | number; export type ItemKey = string | number; diff --git a/packages/core/src/computed/index.ts b/packages/core/src/computed/index.ts index bdb39aff..b59d21d9 100644 --- a/packages/core/src/computed/index.ts +++ b/packages/core/src/computed/index.ts @@ -206,79 +206,6 @@ export class Computed extends State< } } -/** - * Returns a newly created Computed. - * - * A Computed is an extension of the State Class - * that computes its value based on a specified compute function. - * - * The computed value will be cached to avoid unnecessary recomputes - * and is only recomputed when one of its direct dependencies changes. - * - * 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) - * - * @public - * @param computeFunction - Function to compute the computed value. - * @param config - Configuration object - */ -export function createComputed( - computeFunction: ComputeFunctionType, - config?: CreateComputedConfigInterfaceWithAgile -): Computed; -/** - * Returns a newly created Computed. - * - * A Computed is an extension of the State Class - * that computes its value based on a specified compute function. - * - * The computed value will be cached to avoid unnecessary recomputes - * and is only recomputed when one of its direct dependencies changes. - * - * 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#createcomputed) - * - * @public - * @param computeFunction - Function to compute the computed value. - * @param deps - Hard-coded dependencies on which the Computed Class should depend. - */ -export function createComputed( - computeFunction: ComputeFunctionType, - deps?: Array -): Computed; -export function createComputed( - computeFunction: ComputeFunctionType, - configOrDeps?: - | CreateComputedConfigInterface - | Array -): Computed { - let _config: CreateComputedConfigInterfaceWithAgile = {}; - - if (Array.isArray(configOrDeps)) { - _config = defineConfig(_config, { - computedDeps: configOrDeps, - }); - } else { - if (configOrDeps) _config = configOrDeps; - } - - _config = defineConfig(_config, { agileInstance: shared }); - - return new Computed( - _config.agileInstance as any, - computeFunction, - removeProperties(_config, ['agileInstance']) - ); -} - -export interface CreateComputedConfigInterfaceWithAgile - extends CreateAgileSubInstanceInterface, - CreateComputedConfigInterface {} - export type ComputeFunctionType = () => | ComputedValueType | Promise; diff --git a/packages/core/src/shared.ts b/packages/core/src/shared.ts index 604d1c89..d1aa7967 100644 --- a/packages/core/src/shared.ts +++ b/packages/core/src/shared.ts @@ -1,4 +1,20 @@ -import { Agile, runsOnServer } from './internal'; +import { + Agile, + Collection, + CollectionConfig, + Computed, + ComputeFunctionType, + CreateComputedConfigInterface, + CreateStorageConfigInterface, + DefaultItem, + defineConfig, + DependableAgileInstancesType, + removeProperties, + runsOnServer, + State, + StateConfigInterface, + Storage, +} from './internal'; /** * Shared Agile Instance that is used when no Agile Instance was specified. @@ -19,6 +35,157 @@ export function assignSharedAgileInstance(agileInstance: Agile): void { sharedAgileInstance = agileInstance; } +/** + * Returns a newly created Storage. + * + * A Storage Class serves as an interface to external storages, + * such as the [Async Storage](https://github.com/react-native-async-storage/async-storage) or + * [Local Storage](https://www.w3schools.com/html/html5_webstorage.asp). + * + * It creates the foundation to easily [`persist()`](https://agile-ts.org/docs/core/state/methods#persist) [Agile Sub Instances](https://agile-ts.org/docs/introduction/#agile-sub-instance) + * (like States or Collections) in nearly any external storage. + * + * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#createstorage) + * + * @public + * @param config - Configuration object + */ +export function createStorage(config: CreateStorageConfigInterface): Storage { + return new Storage(config); +} + +/** + * Returns a newly created State. + * + * A State manages 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. + * + * You can create as many global 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 = {} +): State { + config = defineConfig(config, { + agileInstance: sharedAgileInstance, + }); + return new State( + config.agileInstance as any, + initialValue, + removeProperties(config, ['agileInstance']) + ); +} + +/** + * Returns a newly created Computed. + * + * A Computed is an extension of the State Class + * that computes its value based on a specified compute function. + * + * The computed value will be cached to avoid unnecessary recomputes + * and is only recomputed when one of its direct dependencies changes. + * + * 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) + * + * @public + * @param computeFunction - Function to compute the computed value. + * @param config - Configuration object + */ +export function createComputed( + computeFunction: ComputeFunctionType, + config?: CreateComputedConfigInterfaceWithAgile +): Computed; +/** + * Returns a newly created Computed. + * + * A Computed is an extension of the State Class + * that computes its value based on a specified compute function. + * + * The computed value will be cached to avoid unnecessary recomputes + * and is only recomputed when one of its direct dependencies changes. + * + * 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#createcomputed) + * + * @public + * @param computeFunction - Function to compute the computed value. + * @param deps - Hard-coded dependencies on which the Computed Class should depend. + */ +export function createComputed( + computeFunction: ComputeFunctionType, + deps?: Array +): Computed; +export function createComputed( + computeFunction: ComputeFunctionType, + configOrDeps?: + | CreateComputedConfigInterface + | Array +): Computed { + let _config: CreateComputedConfigInterfaceWithAgile = {}; + + if (Array.isArray(configOrDeps)) { + _config = defineConfig(_config, { + computedDeps: configOrDeps, + }); + } else { + if (configOrDeps) _config = configOrDeps; + } + + _config = defineConfig(_config, { agileInstance: sharedAgileInstance }); + + return new Computed( + _config.agileInstance as any, + computeFunction, + removeProperties(_config, ['agileInstance']) + ); +} + +/** + * Returns a newly created Collection. + * + * A Collection manages a reactive set of Information + * that we need to remember globally at a later point in time. + * While providing a toolkit to use and mutate this set of Information. + * + * It is designed for arrays of data objects following the same pattern. + * + * Each of these data object must have a unique `primaryKey` to be correctly identified later. + * + * You can create as many global Collections as you need. + * + * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#createcollection) + * + * @public + * @param config - Configuration object + * @param agileInstance - Instance of Agile the Collection belongs to. + */ +export function createCollection( + config?: CollectionConfig, + agileInstance: Agile = sharedAgileInstance +): Collection { + return new Collection(agileInstance, config); +} + +export interface CreateComputedConfigInterfaceWithAgile + extends CreateAgileSubInstanceInterface, + CreateComputedConfigInterface {} + +export interface CreateStateConfigInterfaceWithAgile + extends CreateAgileSubInstanceInterface, + StateConfigInterface {} + export interface CreateAgileSubInstanceInterface { /** * Instance of Agile the Instance belongs to. diff --git a/packages/core/src/state/index.ts b/packages/core/src/state/index.ts index 7eaf518f..3c150b7a 100644 --- a/packages/core/src/state/index.ts +++ b/packages/core/src/state/index.ts @@ -762,39 +762,6 @@ export class State { } } -/** - * Returns a newly created State. - * - * A State manages 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. - * - * You can create as many global 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 = {} -): State { - config = defineConfig(config, { - agileInstance: shared, - }); - return new State( - config.agileInstance as any, - initialValue, - removeProperties(config, ['agileInstance']) - ); -} - -export interface CreateStateConfigInterfaceWithAgile - extends CreateAgileSubInstanceInterface, - StateConfigInterface {} - export type StateKey = string | number; export interface StateObserversInterface { diff --git a/packages/core/src/storages/storage.ts b/packages/core/src/storages/storage.ts index a03b53d7..0f200cd6 100644 --- a/packages/core/src/storages/storage.ts +++ b/packages/core/src/storages/storage.ts @@ -184,25 +184,6 @@ export class Storage { } } -/** - * Returns a newly created Storage. - * - * A Storage Class serves as an interface to external storages, - * such as the [Async Storage](https://github.com/react-native-async-storage/async-storage) or - * [Local Storage](https://www.w3schools.com/html/html5_webstorage.asp). - * - * It creates the foundation to easily [`persist()`](https://agile-ts.org/docs/core/state/methods#persist) [Agile Sub Instances](https://agile-ts.org/docs/introduction/#agile-sub-instance) - * (like States or Collections) in nearly any external storage. - * - * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#createstorage) - * - * @public - * @param config - Configuration object - */ -export function createStorage(config: CreateStorageConfigInterface): Storage { - return new Storage(config); -} - export type StorageKey = string | number; export type StorageItemKey = string | number; diff --git a/packages/core/tests/integration/collection.persistent.integration.test.ts b/packages/core/tests/integration/collection.persistent.integration.test.ts index e1d151ee..bac84d63 100644 --- a/packages/core/tests/integration/collection.persistent.integration.test.ts +++ b/packages/core/tests/integration/collection.persistent.integration.test.ts @@ -1,4 +1,4 @@ -import { Agile, Item } from '../../src'; +import { Agile, Item, createStorage, createCollection } from '../../src'; import { LogMock } from '../helper/logMock'; describe('Collection Persist Function Tests', () => { @@ -30,7 +30,7 @@ describe('Collection Persist Function Tests', () => { App = new Agile({ localStorage: false }); App.registerStorage( - App.createStorage({ + createStorage({ key: 'testStorage', prefix: 'test', methods: storageMethods, @@ -41,7 +41,7 @@ describe('Collection Persist Function Tests', () => { describe('Collection', () => { it('Can persist Collection', async () => { // Create Collection - const MY_COLLECTION = App.createCollection(); + const MY_COLLECTION = createCollection({}, App); // Test Collecting Item before Persisting MY_COLLECTION.collect({ id: 2, name: 'hans' }); @@ -147,7 +147,7 @@ describe('Collection Persist Function Tests', () => { it('Can load persisted Collection', async () => { // Create Collection - const MY_COLLECTION = App.createCollection(); + const MY_COLLECTION = createCollection({}, App); // Load persisted Value MY_COLLECTION.persist('myCollection'); @@ -201,7 +201,7 @@ describe('Collection Persist Function Tests', () => { it('Can remove persisted Collection', async () => { // Create Collection - const MY_COLLECTION = App.createCollection(); + const MY_COLLECTION = createCollection({}, App); // Load persisted Value MY_COLLECTION.persist('myCollection'); diff --git a/packages/core/tests/unit/agile.test.ts b/packages/core/tests/unit/agile.test.ts index 2d0dc29e..c9e3ee21 100644 --- a/packages/core/tests/unit/agile.test.ts +++ b/packages/core/tests/unit/agile.test.ts @@ -167,115 +167,6 @@ describe('Agile Tests', () => { jest.clearAllMocks(); // Because creating the Agile Instance calls some mocks }); - describe('createStorage function tests', () => { - beforeEach(() => { - jest.spyOn(Shared, 'createStorage'); - }); - - it('should call createStorage', () => { - const storageConfig = { - prefix: 'test', - methods: { - get: () => { - /* empty function */ - }, - set: () => { - /* empty function */ - }, - remove: () => { - /* empty function */ - }, - }, - key: 'myTestStorage', - }; - - const response = agile.createStorage(storageConfig); - - expect(response).toBeInstanceOf(Storage); - expect(Shared.createStorage).toHaveBeenCalledWith(storageConfig); - }); - }); - - describe('createState function tests', () => { - beforeEach(() => { - jest.spyOn(Shared, 'createState'); - }); - - it('should call createState with the Agile Instance it was called on', () => { - const response = agile.createState('jeff', { key: 'jeffState' }); - - expect(response).toBeInstanceOf(State); - expect(Shared.createState).toHaveBeenCalledWith('jeff', { - key: 'jeffState', - agileInstance: agile, - }); - }); - }); - - describe('createCollection function tests', () => { - beforeEach(() => { - jest.spyOn(Shared, 'createCollection'); - }); - - it('should call createCollection with the Agile Instance it was called on', () => { - const collectionConfig = { - selectors: ['test', 'test1'], - groups: ['test2', 'test10'], - defaultGroupKey: 'frank', - key: 'myCoolCollection', - }; - - const response = agile.createCollection(collectionConfig); - - expect(response).toBeInstanceOf(Collection); - expect(Shared.createCollection).toHaveBeenCalledWith( - collectionConfig, - agile - ); - }); - }); - - describe('createComputed function tests', () => { - const computedFunction = () => { - // empty - }; - - beforeEach(() => { - jest.spyOn(Shared, 'createComputed'); - }); - - it('should call createComputed with the Agile Instance it was called on (default config)', () => { - const response = agile.createComputed(computedFunction, [ - 'dummyDep' as any, - ]); - - expect(response).toBeInstanceOf(Computed); - expect(Shared.createComputed).toHaveBeenCalledWith(computedFunction, { - computedDeps: ['dummyDep' as any], - agileInstance: agile, - }); - }); - - it('should call createComputed with the Agile Instance it was called on (specific config)', () => { - const computedConfig = { - key: 'jeff', - isPlaceholder: false, - computedDeps: ['dummyDep' as any], - autodetect: true, - }; - - const response = agile.createComputed(computedFunction, computedConfig); - - expect(response).toBeInstanceOf(Computed); - expect(Shared.createComputed).toHaveBeenCalledWith(computedFunction, { - ...computedConfig, - ...{ - agileInstance: agile, - }, - }); - }); - }); - describe('integrate function tests', () => { it('should integrate provided Framework', () => { const returnedAgile = agile.integrate(testIntegration); diff --git a/packages/core/tests/unit/storages/persistent.test.ts b/packages/core/tests/unit/storages/persistent.test.ts index 1291b631..a0dd3407 100644 --- a/packages/core/tests/unit/storages/persistent.test.ts +++ b/packages/core/tests/unit/storages/persistent.test.ts @@ -1,4 +1,4 @@ -import { Agile, Persistent, Storage } from '../../../src'; +import { Agile, Persistent, Storage, createStorage } from '../../../src'; import { LogMock } from '../../helper/logMock'; describe('Persistent Tests', () => { @@ -263,7 +263,7 @@ describe('Persistent Tests', () => { it('should return true if set key and set StorageKeys', () => { dummyAgile.storages.register( - dummyAgile.createStorage({ + createStorage({ key: 'test', methods: { get: () => { From 7b728a20987c47326964ebc324dfccc676aa8b42 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sun, 1 Aug 2021 08:17:37 +0200 Subject: [PATCH 09/93] made computed state tree shakable --- packages/core/src/collection/index.ts | 5 ++--- packages/core/src/computed/index.ts | 6 ++++-- packages/core/src/state/index.ts | 2 -- packages/core/src/state/state.observer.ts | 5 ++--- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/core/src/collection/index.ts b/packages/core/src/collection/index.ts index c51cba3f..31b8a107 100644 --- a/packages/core/src/collection/index.ts +++ b/packages/core/src/collection/index.ts @@ -21,7 +21,6 @@ import { LogCodeManager, PatchOptionConfigInterface, defineConfig, - shared, } from '../internal'; export class Collection< @@ -53,8 +52,8 @@ export class Collection< // Whether the Collection was instantiated correctly public isInstantiated = false; - // Helper property to check whether an unknown instance is a Collection - // without importing the Collection for properly using 'instanceof' (Treeshaking support) + // Helper property to check whether an unknown instance is a Collection, + // without importing the Collection itself for using 'instanceof' (Treeshaking support) public isCollection = true; /** diff --git a/packages/core/src/computed/index.ts b/packages/core/src/computed/index.ts index b59d21d9..416cb4db 100644 --- a/packages/core/src/computed/index.ts +++ b/packages/core/src/computed/index.ts @@ -11,8 +11,6 @@ import { isAsyncFunction, extractRelevantObservers, defineConfig, - CreateAgileSubInstanceInterface, - shared, } from '../internal'; export class Computed extends State< @@ -27,6 +25,10 @@ export class Computed extends State< // Only hardCoded dependencies the Computed Class depends on public hardCodedDeps: Array = []; + // Helper property to check whether an unknown instance is a Computed, + // without importing the Computed itself for using 'instanceof' (Treeshaking support) + public isComputed = true; + /** * A Computed is an extension of the State Class * that computes its value based on a specified compute function. diff --git a/packages/core/src/state/index.ts b/packages/core/src/state/index.ts index 3c150b7a..89bded3c 100644 --- a/packages/core/src/state/index.ts +++ b/packages/core/src/state/index.ts @@ -17,8 +17,6 @@ import { removeProperties, LogCodeManager, defineConfig, - shared, - CreateAgileSubInstanceInterface, } from '../internal'; export class State { diff --git a/packages/core/src/state/state.observer.ts b/packages/core/src/state/state.observer.ts index dc1711b3..adbb6b14 100644 --- a/packages/core/src/state/state.observer.ts +++ b/packages/core/src/state/state.observer.ts @@ -1,7 +1,6 @@ import { Observer, State, - Computed, copy, equal, notEqual, @@ -59,9 +58,9 @@ export class StateObserver extends Observer { * @param config - Configuration object */ public ingest(config: StateIngestConfigInterface = {}): void { - const state = this.state(); + const state = this.state() as any; - if (state instanceof Computed) { + if (state.isComputed) { state.compute().then((result) => { this.ingestValue(result, config); }); From 90bce945c5b5a5232233369709f520e19e788f05 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sun, 1 Aug 2021 10:46:26 +0200 Subject: [PATCH 10/93] removed 'type()' method --- packages/core/src/logCodeManager.ts | 2 - packages/core/src/state/index.ts | 73 ++------- packages/core/src/state/state.observer.ts | 5 - .../tests/unit/collection/group/group.test.ts | 6 - .../core/tests/unit/collection/item.test.ts | 6 - .../tests/unit/collection/selector.test.ts | 8 - .../core/tests/unit/computed/computed.test.ts | 6 - .../tests/unit/state/state.observer.test.ts | 13 -- packages/core/tests/unit/state/state.test.ts | 144 ++++++------------ 9 files changed, 53 insertions(+), 210 deletions(-) diff --git a/packages/core/src/logCodeManager.ts b/packages/core/src/logCodeManager.ts index 413b46b6..eedd445f 100644 --- a/packages/core/src/logCodeManager.ts +++ b/packages/core/src/logCodeManager.ts @@ -75,8 +75,6 @@ const logCodeMessages = { '13:03:00': "Invalid Storage '${0}()' method provided!", // State - '14:02:00': "Incorrect type '${0}' was provided! Requires type of ${1}.", - '14:03:00': "Incorrect type '${0}' was provided! Requires type of ${1}.", '14:03:01': "'${1}' is a not supported type! Supported types are: String, Boolean, Array, Object, Number.", '14:03:02': "The 'patch()' method works only in object based States!", diff --git a/packages/core/src/state/index.ts b/packages/core/src/state/index.ts index 89bded3c..592a4e36 100644 --- a/packages/core/src/state/index.ts +++ b/packages/core/src/state/index.ts @@ -25,8 +25,6 @@ export class State { // Key/Name identifier of the State public _key?: StateKey; - // Primitive type which constrains the State value (for basic typesafety in Javascript) - public valueType?: string; // Whether the current value differs from the initial value public isSet = false; // Whether the State is a placeholder and only exist in the background @@ -59,9 +57,6 @@ export class State { // Manages the permanent persistent in external Storages public persistent: StatePersistent | undefined; - // Registered callbacks that are fired on each State value change - public watchers: { [key: string]: StateWatcherCallback } = {}; - // When an interval is active, the 'intervalId' to clear the interval is temporary stored here public currentInterval?: NodeJS.Timer | number; @@ -203,15 +198,6 @@ export class State { ? (value as any)(copy(this._value)) : value; - // Check if value has correct type (Javascript) - if (!this.hasCorrectType(_value)) { - LogCodeManager.log(config.force ? '14:02:00' : '14:03:00', [ - typeof _value, - this.valueType, - ]); - if (!config.force) return this; - } - // Ingest the State with the new value into the runtime this.observers['value'].ingestValue(_value, config); @@ -235,26 +221,6 @@ export class State { return this; } - /** - * Assigns a primitive type to the State - * which constrains the State value on the specified type - * to ensure basic typesafety in Javascript. - * - * [Learn more..](https://agile-ts.org/docs/core/state/methods/#type) - * - * @public - * @param type - Primitive type the State value must follow (`String`, `Boolean`, `Array`, `Object`, `Number`). - */ - public type(type: any): this { - const supportedTypes = ['String', 'Boolean', 'Array', 'Object', 'Number']; - if (!supportedTypes.includes(type.name)) { - LogCodeManager.log('14:03:01', [type]); - return this; - } - this.valueType = type.name.toLowerCase(); - return this; - } - /** * Undoes the latest State value change. * @@ -377,7 +343,14 @@ export class State { LogCodeManager.log('00:03:01', ['Watcher Callback', 'function']); return this; } - this.watchers[key] = _callback; + + this.addSideEffect( + key, + (instance) => { + _callback(instance.value, key); + }, + { weight: 0 } + ); return generateKey ? key : this; } @@ -390,23 +363,10 @@ export class State { * @param key - Key/Name identifier of the watcher callback to be removed. */ public removeWatcher(key: string): this { - delete this.watchers[key]; + this.removeSideEffect(key); return this; } - /** - * Returns a boolean indicating whether a watcher callback with the specified `key` - * exists in the State or not. - * - * [Learn more..](https://agile-ts.org/docs/core/state/methods/#haswatcher) - * - * @public - * @param key - Key/Name identifier of the watcher callback to be checked for existence. - */ - public hasWatcher(key: string): boolean { - return !!this.watchers[key]; - } - /** * Fires on the initial State value assignment and then destroys itself. * @@ -419,7 +379,7 @@ export class State { const watcherKey = 'InauguratedWatcherKey'; this.watch(watcherKey, (value, key) => { callback(value, key); - this.removeWatcher(watcherKey); + this.removeSideEffect(watcherKey); }); return this; } @@ -737,19 +697,6 @@ export class State { return !!this.sideEffects[key]; } - /** - * Returns a boolean indicating whether the passed value - * is of the before defined State `valueType` or not. - * - * @internal - * @param value - Value to be checked for the correct type. - */ - public hasCorrectType(value: any): boolean { - if (!this.valueType) return true; - const type = typeof value; - return type === this.valueType; - } - /** * Returns the persistable value of the State. * diff --git a/packages/core/src/state/state.observer.ts b/packages/core/src/state/state.observer.ts index adbb6b14..ab02dc7c 100644 --- a/packages/core/src/state/state.observer.ts +++ b/packages/core/src/state/state.observer.ts @@ -183,11 +183,6 @@ export class StateObserver extends Observer { public sideEffects(job: StateRuntimeJob) { const state = job.observer.state(); - // Call watcher functions - for (const watcherKey in state.watchers) - if (isFunction(state.watchers[watcherKey])) - state.watchers[watcherKey](state._value, watcherKey); - // Call side effect functions if (job.config?.sideEffects?.enabled) { const sideEffectArray = createArrayFromObject< diff --git a/packages/core/tests/unit/collection/group/group.test.ts b/packages/core/tests/unit/collection/group/group.test.ts index f9d48a74..2a7525f1 100644 --- a/packages/core/tests/unit/collection/group/group.test.ts +++ b/packages/core/tests/unit/collection/group/group.test.ts @@ -51,7 +51,6 @@ describe('Group Tests', () => { // Check if State was called with correct parameters expect(group._key).toBeUndefined(); - expect(group.valueType).toBeUndefined(); expect(group.isSet).toBeFalsy(); expect(group.isPlaceholder).toBeFalsy(); expect(group.initialStateValue).toStrictEqual([]); @@ -69,7 +68,6 @@ describe('Group Tests', () => { expect(group.computeExistsMethod).toBeInstanceOf(Function); expect(group.isPersisted).toBeFalsy(); expect(group.persistent).toBeUndefined(); - expect(group.watchers).toStrictEqual({}); }); it('should create Group with no initialItems (specific config)', () => { @@ -92,7 +90,6 @@ describe('Group Tests', () => { // Check if State was called with correct parameters expect(group._key).toBe('dummyKey'); - expect(group.valueType).toBeUndefined(); expect(group.isSet).toBeFalsy(); expect(group.isPlaceholder).toBeTruthy(); expect(group.initialStateValue).toStrictEqual([]); @@ -110,7 +107,6 @@ describe('Group Tests', () => { expect(group.computeExistsMethod).toBeInstanceOf(Function); expect(group.isPersisted).toBeFalsy(); expect(group.persistent).toBeUndefined(); - expect(group.watchers).toStrictEqual({}); }); it('should create Group with initialItems (default config)', () => { @@ -130,7 +126,6 @@ describe('Group Tests', () => { // Check if State was called with correct parameters expect(group._key).toBeUndefined(); - expect(group.valueType).toBeUndefined(); expect(group.isSet).toBeFalsy(); expect(group.isPlaceholder).toBeFalsy(); expect(group.initialStateValue).toStrictEqual(['test1', 'test2', 'test3']); @@ -146,7 +141,6 @@ describe('Group Tests', () => { expect(group.computeExistsMethod).toBeInstanceOf(Function); expect(group.isPersisted).toBeFalsy(); expect(group.persistent).toBeUndefined(); - expect(group.watchers).toStrictEqual({}); }); describe('Group Function Tests', () => { diff --git a/packages/core/tests/unit/collection/item.test.ts b/packages/core/tests/unit/collection/item.test.ts index 374352b8..38538da9 100644 --- a/packages/core/tests/unit/collection/item.test.ts +++ b/packages/core/tests/unit/collection/item.test.ts @@ -43,7 +43,6 @@ describe('Item Tests', () => { ).toHaveBeenCalledWith('dummyId'); expect(item._key).toBe(dummyData[dummyCollection.config.primaryKey]); - expect(item.valueType).toBeUndefined(); expect(item.isSet).toBeFalsy(); expect(item.isPlaceholder).toBeFalsy(); expect(item.initialStateValue).toStrictEqual(dummyData); @@ -60,7 +59,6 @@ describe('Item Tests', () => { expect(item.computeExistsMethod).toBeInstanceOf(Function); expect(item.isPersisted).toBeFalsy(); expect(item.persistent).toBeUndefined(); - expect(item.watchers).toStrictEqual({}); expect(item.selectedBy.size).toBe(0); }); @@ -82,7 +80,6 @@ describe('Item Tests', () => { // Check if State was called with correct parameters expect(item._key).toBe(dummyData[dummyCollection.config.primaryKey]); - expect(item.valueType).toBeUndefined(); expect(item.isSet).toBeFalsy(); expect(item.isPlaceholder).toBeTruthy(); expect(item.initialStateValue).toStrictEqual(dummyData); @@ -99,7 +96,6 @@ describe('Item Tests', () => { expect(item.computeExistsMethod).toBeInstanceOf(Function); expect(item.isPersisted).toBeFalsy(); expect(item.persistent).toBeUndefined(); - expect(item.watchers).toStrictEqual({}); expect(item.selectedBy.size).toBe(0); }); @@ -119,7 +115,6 @@ describe('Item Tests', () => { // Check if State was called with correct parameters expect(item._key).toBeUndefined(); - expect(item.valueType).toBeUndefined(); expect(item.isSet).toBeFalsy(); expect(item.isPlaceholder).toBeFalsy(); expect(item.initialStateValue).toStrictEqual(dummyData); @@ -136,7 +131,6 @@ describe('Item Tests', () => { expect(item.computeExistsMethod).toBeInstanceOf(Function); expect(item.isPersisted).toBeFalsy(); expect(item.persistent).toBeUndefined(); - expect(item.watchers).toStrictEqual({}); expect(item.selectedBy.size).toBe(0); }); diff --git a/packages/core/tests/unit/collection/selector.test.ts b/packages/core/tests/unit/collection/selector.test.ts index 11080f9f..68378e32 100644 --- a/packages/core/tests/unit/collection/selector.test.ts +++ b/packages/core/tests/unit/collection/selector.test.ts @@ -38,7 +38,6 @@ describe('Selector Tests', () => { // Check if State was called with correct parameters expect(selector._key).toBeUndefined(); - expect(selector.valueType).toBeUndefined(); expect(selector.isSet).toBeFalsy(); expect(selector.isPlaceholder).toBeTruthy(); expect(selector.initialStateValue).toBeNull(); @@ -55,7 +54,6 @@ describe('Selector Tests', () => { expect(selector.computeExistsMethod).toBeInstanceOf(Function); expect(selector.isPersisted).toBeFalsy(); expect(selector.persistent).toBeUndefined(); - expect(selector.watchers).toStrictEqual({}); }); it('should create Selector and call initial select (specific config)', () => { @@ -77,7 +75,6 @@ describe('Selector Tests', () => { // Check if State was called with correct parameters expect(selector._key).toBe('dummyKey'); - expect(selector.valueType).toBeUndefined(); expect(selector.isSet).toBeFalsy(); expect(selector.isPlaceholder).toBeTruthy(); expect(selector.initialStateValue).toBeNull(); @@ -94,7 +91,6 @@ describe('Selector Tests', () => { expect(selector.computeExistsMethod).toBeInstanceOf(Function); expect(selector.isPersisted).toBeFalsy(); expect(selector.persistent).toBeUndefined(); - expect(selector.watchers).toStrictEqual({}); }); it("should create Selector and shouldn't call initial select (config.isPlaceholder = true)", () => { @@ -114,7 +110,6 @@ describe('Selector Tests', () => { // Check if State was called with correct parameters expect(selector._key).toBeUndefined(); - expect(selector.valueType).toBeUndefined(); expect(selector.isSet).toBeFalsy(); expect(selector.isPlaceholder).toBeTruthy(); expect(selector.initialStateValue).toBeNull(); @@ -131,7 +126,6 @@ describe('Selector Tests', () => { expect(selector.computeExistsMethod).toBeInstanceOf(Function); expect(selector.isPersisted).toBeFalsy(); expect(selector.persistent).toBeUndefined(); - expect(selector.watchers).toStrictEqual({}); }); it("should create Selector and shouldn't call initial select if specified selector key is null (default config)", () => { @@ -149,7 +143,6 @@ describe('Selector Tests', () => { // Check if State was called with correct parameters expect(selector._key).toBeUndefined(); - expect(selector.valueType).toBeUndefined(); expect(selector.isSet).toBeFalsy(); expect(selector.isPlaceholder).toBeTruthy(); expect(selector.initialStateValue).toBeNull(); @@ -166,7 +159,6 @@ describe('Selector Tests', () => { expect(selector.computeExistsMethod).toBeInstanceOf(Function); expect(selector.isPersisted).toBeFalsy(); expect(selector.persistent).toBeUndefined(); - expect(selector.watchers).toStrictEqual({}); }); describe('Selector Function Tests', () => { diff --git a/packages/core/tests/unit/computed/computed.test.ts b/packages/core/tests/unit/computed/computed.test.ts index ef88decc..47ec4b26 100644 --- a/packages/core/tests/unit/computed/computed.test.ts +++ b/packages/core/tests/unit/computed/computed.test.ts @@ -42,7 +42,6 @@ describe('Computed Tests', () => { // Check if State was called with correct parameters expect(computed._key).toBeUndefined(); - expect(computed.valueType).toBeUndefined(); expect(computed.isSet).toBeFalsy(); expect(computed.isPlaceholder).toBeFalsy(); expect(computed.initialStateValue).toBe(null); @@ -59,7 +58,6 @@ describe('Computed Tests', () => { expect(computed.computeExistsMethod).toBeInstanceOf(Function); expect(computed.isPersisted).toBeFalsy(); expect(computed.persistent).toBeUndefined(); - expect(computed.watchers).toStrictEqual({}); }); it('should create Computed with a not async compute method (specific config)', () => { @@ -111,7 +109,6 @@ describe('Computed Tests', () => { // Check if State was called with correct parameters expect(computed._key).toBe('coolComputed'); - expect(computed.valueType).toBeUndefined(); expect(computed.isSet).toBeFalsy(); expect(computed.isPlaceholder).toBeFalsy(); expect(computed.initialStateValue).toBe(null); @@ -128,7 +125,6 @@ describe('Computed Tests', () => { expect(computed.computeExistsMethod).toBeInstanceOf(Function); expect(computed.isPersisted).toBeFalsy(); expect(computed.persistent).toBeUndefined(); - expect(computed.watchers).toStrictEqual({}); }); it('should create Computed with an async compute method (default config)', () => { @@ -149,7 +145,6 @@ describe('Computed Tests', () => { // Check if State was called with correct parameters expect(computed._key).toBeUndefined(); - expect(computed.valueType).toBeUndefined(); expect(computed.isSet).toBeFalsy(); expect(computed.isPlaceholder).toBeFalsy(); expect(computed.initialStateValue).toBe(null); @@ -166,7 +161,6 @@ describe('Computed Tests', () => { expect(computed.computeExistsMethod).toBeInstanceOf(Function); expect(computed.isPersisted).toBeFalsy(); expect(computed.persistent).toBeUndefined(); - expect(computed.watchers).toStrictEqual({}); }); describe('Computed Function Tests', () => { diff --git a/packages/core/tests/unit/state/state.observer.test.ts b/packages/core/tests/unit/state/state.observer.test.ts index 035ec4a5..53ebd1dd 100644 --- a/packages/core/tests/unit/state/state.observer.test.ts +++ b/packages/core/tests/unit/state/state.observer.test.ts @@ -422,7 +422,6 @@ describe('StateObserver Tests', () => { key: 'dummyJob', }); - dummyState.watchers['dummyWatcher'] = jest.fn(); dummyState.sideEffects['dummySideEffect3'] = { weight: 100, callback: jest.fn(() => { @@ -448,10 +447,6 @@ describe('StateObserver Tests', () => { stateObserver.sideEffects(dummyJob); - expect(dummyState.watchers['dummyWatcher']).toHaveBeenCalledWith( - 'dummyValue', - 'dummyWatcher' - ); expect( dummyState.sideEffects['dummySideEffect'].callback ).toHaveBeenCalledWith(dummyState, dummyJob.config); @@ -479,10 +474,6 @@ describe('StateObserver Tests', () => { stateObserver.sideEffects(dummyJob); - expect(dummyState.watchers['dummyWatcher']).toHaveBeenCalledWith( - 'dummyValue', - 'dummyWatcher' - ); expect( dummyState.sideEffects['dummySideEffect'].callback ).not.toHaveBeenCalled(); @@ -507,10 +498,6 @@ describe('StateObserver Tests', () => { stateObserver.sideEffects(dummyJob); - expect(dummyState.watchers['dummyWatcher']).toHaveBeenCalledWith( - 'dummyValue', - 'dummyWatcher' - ); expect( dummyState.sideEffects['dummySideEffect'].callback ).toHaveBeenCalledWith(dummyState, dummyJob.config); diff --git a/packages/core/tests/unit/state/state.test.ts b/packages/core/tests/unit/state/state.test.ts index 78b3afe9..4155c813 100644 --- a/packages/core/tests/unit/state/state.test.ts +++ b/packages/core/tests/unit/state/state.test.ts @@ -32,7 +32,6 @@ describe('State Tests', () => { expect(state.set).toHaveBeenCalledWith('coolValue', { overwrite: true }); expect(state._key).toBeUndefined(); - expect(state.valueType).toBeUndefined(); expect(state.isSet).toBeFalsy(); expect(state.isPlaceholder).toBeTruthy(); expect(state.initialStateValue).toBe('coolValue'); @@ -47,7 +46,6 @@ describe('State Tests', () => { expect(state.computeExistsMethod).toBeInstanceOf(Function); expect(state.isPersisted).toBeFalsy(); expect(state.persistent).toBeUndefined(); - expect(state.watchers).toStrictEqual({}); }); it('should create State and should call initial set (specific config)', () => { @@ -63,7 +61,6 @@ describe('State Tests', () => { expect(state.set).toHaveBeenCalledWith('coolValue', { overwrite: true }); expect(state._key).toBe('coolState'); - expect(state.valueType).toBeUndefined(); expect(state.isSet).toBeFalsy(); expect(state.isPlaceholder).toBeTruthy(); expect(state.initialStateValue).toBe('coolValue'); @@ -80,7 +77,6 @@ describe('State Tests', () => { expect(state.computeExistsMethod).toBeInstanceOf(Function); expect(state.isPersisted).toBeFalsy(); expect(state.persistent).toBeUndefined(); - expect(state.watchers).toStrictEqual({}); }); it("should create State and shouldn't call initial set (config.isPlaceholder = true)", () => { @@ -91,7 +87,6 @@ describe('State Tests', () => { expect(state.set).not.toHaveBeenCalled(); expect(state._key).toBeUndefined(); - expect(state.valueType).toBeUndefined(); expect(state.isSet).toBeFalsy(); expect(state.isPlaceholder).toBeTruthy(); expect(state.initialStateValue).toBe('coolValue'); @@ -106,7 +101,6 @@ describe('State Tests', () => { expect(state.computeExistsMethod).toBeInstanceOf(Function); expect(state.isPersisted).toBeFalsy(); expect(state.persistent).toBeUndefined(); - expect(state.watchers).toStrictEqual({}); }); describe('State Function Tests', () => { @@ -278,30 +272,6 @@ describe('State Tests', () => { ); }); - it("shouldn't ingestValue if value hasn't correct type (default config)", () => { - numberState.type(Number); - - numberState.set('coolValue' as any); - - LogMock.hasNotLogged('warn'); - LogMock.hasLoggedCode('14:03:00', ['string', 'number']); - expect( - numberState.observers['value'].ingestValue - ).not.toHaveBeenCalled(); - }); - - it("should ingestValue if value hasn't correct type (config.force = true)", () => { - numberState.type(Number); - - numberState.set('coolValue' as any, { force: true }); - - LogMock.hasNotLogged('error'); - LogMock.hasLoggedCode('14:02:00', ['string', 'number']); - expect( - numberState.observers['value'].ingestValue - ).toHaveBeenCalledWith('coolValue', { force: true }); - }); - it("should ingestValue if value hasn't correct type but the type isn't explicit defined (default config)", () => { numberState.set('coolValue' as any); @@ -337,21 +307,6 @@ describe('State Tests', () => { }); }); - describe('type function tests', () => { - it('should assign valid Type to State', () => { - numberState.type(Number); - - expect(numberState.valueType).toBe('number'); - }); - - it("shouldn't assign invalid Type to State", () => { - numberState.type('fuckingType'); - - expect(numberState.valueType).toBeUndefined(); - LogMock.hasLoggedCode('14:03:01', ['fuckingType']); - }); - }); - describe('undo function tests', () => { beforeEach(() => { numberState.set = jest.fn(); @@ -516,30 +471,50 @@ describe('State Tests', () => { }); describe('watch function tests', () => { - const dummyCallbackFunction1 = () => { - /* empty function */ - }; - const dummyCallbackFunction2 = () => { - /* empty function */ - }; + 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', dummyCallbackFunction1); + const response = numberState.watch('dummyKey', dummyCallbackFunction); expect(response).toBe(numberState); - expect(numberState.watchers).toHaveProperty('dummyKey'); - expect(numberState.watchers['dummyKey']).toBe(dummyCallbackFunction1); + 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(dummyCallbackFunction1); + const response = numberState.watch(dummyCallbackFunction); expect(response).toBe('randomKey'); - expect(numberState.watchers).toHaveProperty('randomKey'); - expect(numberState.watchers['randomKey']).toBe(dummyCallbackFunction1); + 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", () => { @@ -549,22 +524,20 @@ describe('State Tests', () => { ); expect(response).toBe(numberState); - expect(numberState.watchers).not.toHaveProperty('dummyKey'); + expect(numberState.addSideEffect).not.toHaveBeenCalled(); LogMock.hasLoggedCode('00:03:01', ['Watcher Callback', 'function']); }); }); describe('removeWatcher function tests', () => { beforeEach(() => { - numberState.watchers['dummyKey'] = () => { - /* empty function */ - }; + jest.spyOn(numberState, 'removeSideEffect'); }); it('should remove watcher at key from State', () => { numberState.removeWatcher('dummyKey'); - expect(numberState.watchers).not.toHaveProperty('dummyKey'); + expect(numberState.removeSideEffect).toHaveBeenCalledWith('dummyKey'); }); }); @@ -573,6 +546,7 @@ describe('State Tests', () => { beforeEach(() => { jest.spyOn(numberState, 'watch'); + jest.spyOn(numberState, 'removeSideEffect'); dummyCallbackFunction = jest.fn(); }); @@ -583,35 +557,21 @@ describe('State Tests', () => { 'InauguratedWatcherKey', expect.any(Function) ); - expect(numberState.watchers).toHaveProperty('InauguratedWatcherKey'); }); - it('should remove itself after getting called', () => { + it('should remove itself after invoking', () => { numberState.onInaugurated(dummyCallbackFunction); // Call Inaugurated Watcher - numberState.watchers['InauguratedWatcherKey'](10, 'testKey'); + numberState.sideEffects['InauguratedWatcherKey'].callback(numberState); - expect(dummyCallbackFunction).toHaveBeenCalledWith(10, 'testKey'); - expect(numberState.watchers).not.toHaveProperty( + expect(dummyCallbackFunction).toHaveBeenCalledWith( + numberState.value, + 'InauguratedWatcherKey' + ); + expect(numberState.removeSideEffect).toHaveBeenCalledWith( 'InauguratedWatcherKey' ); - }); - }); - - describe('hasWatcher function tests', () => { - beforeEach(() => { - numberState.watchers['dummyKey'] = () => { - /* empty function */ - }; - }); - - it('should return true if Watcher at given Key exists', () => { - expect(numberState.hasWatcher('dummyKey')).toBeTruthy(); - }); - - it("should return false if Watcher at given Key doesn't exists", () => { - expect(numberState.hasWatcher('notExistingDummyKey')).toBeFalsy(); }); }); @@ -1077,23 +1037,5 @@ describe('State Tests', () => { expect(numberState.hasSideEffect('notExistingDummyKey')).toBeFalsy(); }); }); - - describe('hasCorrectType function tests', () => { - it('should return true if State Type matches passed type', () => { - numberState.type(Number); - - expect(numberState.hasCorrectType(10)).toBeTruthy(); - }); - - it("should return false if State Type doesn't matches passed type", () => { - numberState.type(Number); - - expect(numberState.hasCorrectType('stringValue')).toBeFalsy(); - }); - - it('should return true if State has no defined Type', () => { - expect(numberState.hasCorrectType('stringValue')).toBeTruthy(); - }); - }); }); }); From 29294c114b28efdb93003be67180e8cc0438746b Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sun, 1 Aug 2021 18:39:42 +0200 Subject: [PATCH 11/93] readded cjs support (https://blog.logrocket.com/publishing-node-modules-typescript-es-modules/) --- packages/core/package.json | 9 ++++++--- packages/core/tsconfig.esm.json | 9 +++++++++ packages/core/tsconfig.json | 3 ++- packages/core/tsconfig.production.json | 3 ++- packages/tsconfig.default.json | 2 +- 5 files changed, 20 insertions(+), 6 deletions(-) create mode 100644 packages/core/tsconfig.esm.json diff --git a/packages/core/package.json b/packages/core/package.json index 646fab42..e3e99f6b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -24,16 +24,19 @@ "agile-ts" ], "main": "dist/index.js", + "module": "dist/esm/index.js", "types": "dist/index.d.ts", "scripts": { - "build": "tsc", - "prepare": "tsc && tsc -p ./tsconfig.production.json", + "build": "yarn run build:esm && yarn run build:cjs", + "prepare": "yarn run build", + "build:esm": "tsc -p ./tsconfig.esm.json", + "build:cjs": "tsc -p ./tsconfig.json && tsc -p ./tsconfig.production.json", "dev:publish": "yalc publish", "dev:push": "yalc push", "watch:push": "tsc-watch --onSuccess \"yarn run dev:push\"", "watch": "tsc -w", "release": "node ./scripts/prepublish.js && yarn run prepare", - "preview": "npm pack", + "pack": "npm pack", "test": "jest", "test:coverage": "jest --coverage", "lint": "eslint src/**/*", diff --git a/packages/core/tsconfig.esm.json b/packages/core/tsconfig.esm.json new file mode 100644 index 00000000..4913df29 --- /dev/null +++ b/packages/core/tsconfig.esm.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "ES2015", + "outDir": "dist/esm", + "declaration": false, // already generated via 'tsconfig.json' + "removeComments": true + } +} diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 600cd205..791f587c 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -1,10 +1,11 @@ { "extends": "../tsconfig.default.json", "compilerOptions": { + "module": "commonjs", "rootDir": "src", "outDir": "dist" }, "include": [ "./src/**/*" // Only include what is in src (-> dist, tests, .. will be excluded) ] -} \ No newline at end of file +} diff --git a/packages/core/tsconfig.production.json b/packages/core/tsconfig.production.json index 7f9c2701..6695d544 100644 --- a/packages/core/tsconfig.production.json +++ b/packages/core/tsconfig.production.json @@ -1,8 +1,9 @@ // Use File: Overwrites already generated js files with new js files that have no comments +// Not doing in main 'tsconfig.json' because then the typescript declarations would have no comments { "extends": "./tsconfig.json", "compilerOptions": { "declaration": false, // '.d.ts' files should have been generated before "removeComments": true } -} \ No newline at end of file +} diff --git a/packages/tsconfig.default.json b/packages/tsconfig.default.json index ef97da46..7efcfb92 100644 --- a/packages/tsconfig.default.json +++ b/packages/tsconfig.default.json @@ -10,7 +10,7 @@ // "allowJs": true, /* Allow javascript files to be compiled. */ // "checkJs": true, /* Report errors in .js files. */ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ - "declaration": true, /* Generates corresponding '.d.ts' file. */ + "declaration": true, /* Generates corresponding '.d.ts' file. */ // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ // "sourceMap": true, /* Generates corresponding '.map' file. */ // "outFile": "./core/dist/index.js", /* Concatenate and emit output to single file. */ From 70e80e94ad0e6a931a20cf7c1d88f1913180d609 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Mon, 2 Aug 2021 21:03:31 +0200 Subject: [PATCH 12/93] fixed typo --- packages/core/package.json | 2 +- packages/core/src/shared.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index e3e99f6b..6e92418c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@agile-ts/core", - "version": "0.1.2", + "version": "0.2.0-alpha.2", "author": "BennoDev", "license": "MIT", "homepage": "https://agile-ts.org/", diff --git a/packages/core/src/shared.ts b/packages/core/src/shared.ts index d1aa7967..4d31abdd 100644 --- a/packages/core/src/shared.ts +++ b/packages/core/src/shared.ts @@ -130,7 +130,7 @@ export function createComputed( export function createComputed( computeFunction: ComputeFunctionType, configOrDeps?: - | CreateComputedConfigInterface + | CreateComputedConfigInterfaceWithAgile | Array ): Computed { let _config: CreateComputedConfigInterfaceWithAgile = {}; From ff26d373ca307d079506184b21c4da574da35de9 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Tue, 3 Aug 2021 06:57:48 +0200 Subject: [PATCH 13/93] updated ts configs --- package.json | 12 ++++++------ packages/api/package.json | 12 ++++++++---- packages/api/tsconfig.esm.json | 9 +++++++++ packages/api/tsconfig.json | 3 ++- packages/api/tsconfig.production.json | 6 ++++-- packages/core/package.json | 9 +++++---- packages/core/tsconfig.esm.json | 2 +- packages/core/tsconfig.production.json | 2 +- packages/cra-template-agile-typescript/package.json | 2 +- packages/cra-template-agile/package.json | 2 +- packages/event/package.json | 12 ++++++++---- packages/event/tsconfig.esm.json | 9 +++++++++ packages/event/tsconfig.json | 3 ++- packages/event/tsconfig.production.json | 6 ++++-- packages/logger/package.json | 12 ++++++++---- packages/logger/tsconfig.esm.json | 9 +++++++++ packages/logger/tsconfig.json | 3 ++- packages/logger/tsconfig.production.json | 6 ++++-- packages/multieditor/package.json | 12 ++++++++---- packages/multieditor/tsconfig.esm.json | 9 +++++++++ packages/multieditor/tsconfig.json | 3 ++- packages/multieditor/tsconfig.production.json | 6 ++++-- packages/proxytree/package.json | 12 ++++++++---- packages/proxytree/tsconfig.esm.json | 9 +++++++++ packages/proxytree/tsconfig.json | 3 ++- packages/proxytree/tsconfig.production.json | 6 ++++-- packages/react/package.json | 12 ++++++++---- packages/react/tsconfig.esm.json | 9 +++++++++ packages/react/tsconfig.json | 3 ++- packages/react/tsconfig.production.json | 6 ++++-- packages/tsconfig.default.json | 2 +- packages/utils/package.json | 12 ++++++++---- packages/utils/tsconfig.esm.json | 9 +++++++++ packages/utils/tsconfig.json | 3 ++- packages/utils/tsconfig.production.json | 6 ++++-- packages/vue/package.json | 12 ++++++++---- packages/vue/tsconfig.esm.json | 9 +++++++++ packages/vue/tsconfig.json | 3 ++- packages/vue/tsconfig.production.json | 6 ++++-- 39 files changed, 200 insertions(+), 71 deletions(-) create mode 100644 packages/api/tsconfig.esm.json create mode 100644 packages/event/tsconfig.esm.json create mode 100644 packages/logger/tsconfig.esm.json create mode 100644 packages/multieditor/tsconfig.esm.json create mode 100644 packages/proxytree/tsconfig.esm.json create mode 100644 packages/react/tsconfig.esm.json create mode 100644 packages/utils/tsconfig.esm.json create mode 100644 packages/vue/tsconfig.esm.json diff --git a/package.json b/package.json index 7511c703..7a77e31f 100644 --- a/package.json +++ b/package.json @@ -35,12 +35,12 @@ "release": "lerna run release && changeset publish", "prettier": "prettier --config .prettierrc --write \"**/*.{js,ts}\"", "lint": "eslint --cache \"**/*.{js,jsx,ts,tsx}\"", - "pack": "lerna run prepare && lerna run preview", - "pack:core": "cd packages/core && yarn run prepare && yarn run preview", - "pack:react": "cd packages/react && yarn run prepare && yarn run preview", - "pack:multieditor": "cd packages/multieditor && yarn run prepare && yarn run preview", - "pack:api": "cd packages/api && yarn run prepare && yarn run preview", - "pack:event": "cd packages/event && yarn run prepare && yarn run preview" + "pack": "lerna run prepare && lerna run pack", + "pack:core": "cd packages/core && yarn run prepare && yarn run pack", + "pack:react": "cd packages/react && yarn run prepare && yarn run pack", + "pack:multieditor": "cd packages/multieditor && yarn run prepare && yarn run pack", + "pack:api": "cd packages/api && yarn run prepare && yarn run pack", + "pack:event": "cd packages/event && yarn run prepare && yarn run pack" }, "repository": { "type": "git", diff --git a/packages/api/package.json b/packages/api/package.json index 8fcfd3eb..1d2812bf 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -15,14 +15,17 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { - "build": "tsc", - "prepare": "tsc && tsc -p ./tsconfig.production.json", + "build": "yarn run build:esm && yarn run build:cjs", + "prepare": "yarn run build", + "build:esm": "tsc -p ./tsconfig.esm.json", + "build:cjs": "tsc -p ./tsconfig.json && tsc -p ./tsconfig.production.json", "dev:publish": "yalc publish", "dev:push": "yalc push", "watch:push": "tsc-watch --onSuccess \"yarn run dev:push\"", "watch": "tsc -w", "release": "yarn run prepare", - "preview": "npm pack", + "release:manual": "yarn run prepare && yarn run release && npm publish", + "pack": "npm pack", "test": "jest", "test:coverage": "jest --coverage", "lint": "eslint src/**/*" @@ -48,5 +51,6 @@ "LICENSE", "README.md", "CHANGELOG.md" - ] + ], + "sideEffects": false } diff --git a/packages/api/tsconfig.esm.json b/packages/api/tsconfig.esm.json new file mode 100644 index 00000000..9512f0ad --- /dev/null +++ b/packages/api/tsconfig.esm.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "ES2015", + "outDir": "dist/esm", + "declaration": false, // already generated via 'tsconfig.json' in root + "removeComments": true + } +} diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json index 600cd205..791f587c 100644 --- a/packages/api/tsconfig.json +++ b/packages/api/tsconfig.json @@ -1,10 +1,11 @@ { "extends": "../tsconfig.default.json", "compilerOptions": { + "module": "commonjs", "rootDir": "src", "outDir": "dist" }, "include": [ "./src/**/*" // Only include what is in src (-> dist, tests, .. will be excluded) ] -} \ No newline at end of file +} diff --git a/packages/api/tsconfig.production.json b/packages/api/tsconfig.production.json index 4b5c4d12..b8ec8c38 100644 --- a/packages/api/tsconfig.production.json +++ b/packages/api/tsconfig.production.json @@ -1,7 +1,9 @@ +// Use File: Overwrites already generated js files with new js files that have no comments +// Not doing in main 'tsconfig.json' because then the typescript declarations would have no comments too { "extends": "./tsconfig.json", "compilerOptions": { - "declaration": false, + "declaration": false, // '.d.ts' files should have been generated before "removeComments": true } -} \ No newline at end of file +} diff --git a/packages/core/package.json b/packages/core/package.json index 6e92418c..a0b5c23e 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@agile-ts/core", - "version": "0.2.0-alpha.2", + "version": "0.2.0-alpha.3", "author": "BennoDev", "license": "MIT", "homepage": "https://agile-ts.org/", @@ -36,12 +36,12 @@ "watch:push": "tsc-watch --onSuccess \"yarn run dev:push\"", "watch": "tsc -w", "release": "node ./scripts/prepublish.js && yarn run prepare", + "release:manual": "yarn run prepare && yarn run release && npm publish && git checkout README.md", "pack": "npm pack", "test": "jest", "test:coverage": "jest --coverage", "lint": "eslint src/**/*", - "size": "yarn run build && size-limit", - "release:manual": "yarn run prepare && yarn run release && npm publish && git checkout README.md" + "size": "yarn run build && size-limit" }, "devDependencies": { "@agile-ts/logger": "file:../logger", @@ -73,5 +73,6 @@ "LICENSE", "README.md", "CHANGELOG.md" - ] + ], + "sideEffects": false } diff --git a/packages/core/tsconfig.esm.json b/packages/core/tsconfig.esm.json index 4913df29..9512f0ad 100644 --- a/packages/core/tsconfig.esm.json +++ b/packages/core/tsconfig.esm.json @@ -3,7 +3,7 @@ "compilerOptions": { "module": "ES2015", "outDir": "dist/esm", - "declaration": false, // already generated via 'tsconfig.json' + "declaration": false, // already generated via 'tsconfig.json' in root "removeComments": true } } diff --git a/packages/core/tsconfig.production.json b/packages/core/tsconfig.production.json index 6695d544..b8ec8c38 100644 --- a/packages/core/tsconfig.production.json +++ b/packages/core/tsconfig.production.json @@ -1,5 +1,5 @@ // Use File: Overwrites already generated js files with new js files that have no comments -// Not doing in main 'tsconfig.json' because then the typescript declarations would have no comments +// Not doing in main 'tsconfig.json' because then the typescript declarations would have no comments too { "extends": "./tsconfig.json", "compilerOptions": { diff --git a/packages/cra-template-agile-typescript/package.json b/packages/cra-template-agile-typescript/package.json index 1e13ec10..e3d0ecec 100644 --- a/packages/cra-template-agile-typescript/package.json +++ b/packages/cra-template-agile-typescript/package.json @@ -14,7 +14,7 @@ "agile" ], "scripts": { - "preview": "npm pack" + "pack": "npm pack" }, "devDependencies": { "@agile-ts/core": "^0.1.2", diff --git a/packages/cra-template-agile/package.json b/packages/cra-template-agile/package.json index 838c51d7..5b7d80d2 100644 --- a/packages/cra-template-agile/package.json +++ b/packages/cra-template-agile/package.json @@ -14,7 +14,7 @@ "agile" ], "scripts": { - "preview": "npm pack" + "pack": "npm pack" }, "devDependencies": { "@agile-ts/core": "^0.1.2", diff --git a/packages/event/package.json b/packages/event/package.json index 4c65f748..27cd758e 100644 --- a/packages/event/package.json +++ b/packages/event/package.json @@ -14,14 +14,17 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { - "build": "tsc", - "prepare": "tsc && tsc -p ./tsconfig.production.json", + "build": "yarn run build:esm && yarn run build:cjs", + "prepare": "yarn run build", + "build:esm": "tsc -p ./tsconfig.esm.json", + "build:cjs": "tsc -p ./tsconfig.json && tsc -p ./tsconfig.production.json", "dev:publish": "yalc publish", "dev:push": "yalc push", "watch:push": "tsc-watch --onSuccess \"yarn run dev:push\"", "watch": "tsc -w", "release": "yarn run prepare", - "preview": "npm pack", + "release:manual": "yarn run prepare && yarn run release && npm publish", + "pack": "npm pack", "test": "jest", "test:coverage": "jest --coverage", "lint": "eslint src/**/*" @@ -50,5 +53,6 @@ "LICENSE", "README.md", "CHANGELOG.md" - ] + ], + "sideEffects": false } diff --git a/packages/event/tsconfig.esm.json b/packages/event/tsconfig.esm.json new file mode 100644 index 00000000..9512f0ad --- /dev/null +++ b/packages/event/tsconfig.esm.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "ES2015", + "outDir": "dist/esm", + "declaration": false, // already generated via 'tsconfig.json' in root + "removeComments": true + } +} diff --git a/packages/event/tsconfig.json b/packages/event/tsconfig.json index 600cd205..791f587c 100644 --- a/packages/event/tsconfig.json +++ b/packages/event/tsconfig.json @@ -1,10 +1,11 @@ { "extends": "../tsconfig.default.json", "compilerOptions": { + "module": "commonjs", "rootDir": "src", "outDir": "dist" }, "include": [ "./src/**/*" // Only include what is in src (-> dist, tests, .. will be excluded) ] -} \ No newline at end of file +} diff --git a/packages/event/tsconfig.production.json b/packages/event/tsconfig.production.json index 4b5c4d12..b8ec8c38 100644 --- a/packages/event/tsconfig.production.json +++ b/packages/event/tsconfig.production.json @@ -1,7 +1,9 @@ +// Use File: Overwrites already generated js files with new js files that have no comments +// Not doing in main 'tsconfig.json' because then the typescript declarations would have no comments too { "extends": "./tsconfig.json", "compilerOptions": { - "declaration": false, + "declaration": false, // '.d.ts' files should have been generated before "removeComments": true } -} \ No newline at end of file +} diff --git a/packages/logger/package.json b/packages/logger/package.json index 6c60caa0..ae318b4d 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -14,14 +14,17 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { - "build": "tsc", - "prepare": "tsc && tsc -p ./tsconfig.production.json", + "build": "yarn run build:esm && yarn run build:cjs", + "prepare": "yarn run build", + "build:esm": "tsc -p ./tsconfig.esm.json", + "build:cjs": "tsc -p ./tsconfig.json && tsc -p ./tsconfig.production.json", "dev:publish": "yalc publish", "dev:push": "yalc push", "watch:push": "tsc-watch --onSuccess \"yarn run dev:push\"", "watch": "tsc -w", "release": "yarn run prepare", - "preview": "npm pack", + "release:manual": "yarn run prepare && yarn run release && npm publish", + "pack": "npm pack", "test": "jest", "test:coverage": "jest --coverage", "lint": "eslint src/**/*" @@ -47,5 +50,6 @@ "LICENSE", "README.md", "CHANGELOG.md" - ] + ], + "sideEffects": false } diff --git a/packages/logger/tsconfig.esm.json b/packages/logger/tsconfig.esm.json new file mode 100644 index 00000000..9512f0ad --- /dev/null +++ b/packages/logger/tsconfig.esm.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "ES2015", + "outDir": "dist/esm", + "declaration": false, // already generated via 'tsconfig.json' in root + "removeComments": true + } +} diff --git a/packages/logger/tsconfig.json b/packages/logger/tsconfig.json index 600cd205..791f587c 100644 --- a/packages/logger/tsconfig.json +++ b/packages/logger/tsconfig.json @@ -1,10 +1,11 @@ { "extends": "../tsconfig.default.json", "compilerOptions": { + "module": "commonjs", "rootDir": "src", "outDir": "dist" }, "include": [ "./src/**/*" // Only include what is in src (-> dist, tests, .. will be excluded) ] -} \ No newline at end of file +} diff --git a/packages/logger/tsconfig.production.json b/packages/logger/tsconfig.production.json index 4b5c4d12..b8ec8c38 100644 --- a/packages/logger/tsconfig.production.json +++ b/packages/logger/tsconfig.production.json @@ -1,7 +1,9 @@ +// Use File: Overwrites already generated js files with new js files that have no comments +// Not doing in main 'tsconfig.json' because then the typescript declarations would have no comments too { "extends": "./tsconfig.json", "compilerOptions": { - "declaration": false, + "declaration": false, // '.d.ts' files should have been generated before "removeComments": true } -} \ No newline at end of file +} diff --git a/packages/multieditor/package.json b/packages/multieditor/package.json index b958efe7..5fa30bc7 100644 --- a/packages/multieditor/package.json +++ b/packages/multieditor/package.json @@ -18,14 +18,17 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { - "build": "tsc", - "prepare": "tsc && tsc -p ./tsconfig.production.json", + "build": "yarn run build:esm && yarn run build:cjs", + "prepare": "yarn run build", + "build:esm": "tsc -p ./tsconfig.esm.json", + "build:cjs": "tsc -p ./tsconfig.json && tsc -p ./tsconfig.production.json", "dev:publish": "yalc publish", "dev:push": "yalc push", "watch:push": "tsc-watch --onSuccess \"yarn run dev:push\"", "watch": "tsc -w", "release": "yarn run prepare", - "preview": "npm pack", + "release:manual": "yarn run prepare && yarn run release && npm publish", + "pack": "npm pack", "test": "jest", "test:coverage": "jest --coverage", "lint": "eslint src/**/*" @@ -51,5 +54,6 @@ "LICENSE", "README.md", "CHANGELOG.md" - ] + ], + "sideEffects": false } diff --git a/packages/multieditor/tsconfig.esm.json b/packages/multieditor/tsconfig.esm.json new file mode 100644 index 00000000..9512f0ad --- /dev/null +++ b/packages/multieditor/tsconfig.esm.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "ES2015", + "outDir": "dist/esm", + "declaration": false, // already generated via 'tsconfig.json' in root + "removeComments": true + } +} diff --git a/packages/multieditor/tsconfig.json b/packages/multieditor/tsconfig.json index 600cd205..791f587c 100644 --- a/packages/multieditor/tsconfig.json +++ b/packages/multieditor/tsconfig.json @@ -1,10 +1,11 @@ { "extends": "../tsconfig.default.json", "compilerOptions": { + "module": "commonjs", "rootDir": "src", "outDir": "dist" }, "include": [ "./src/**/*" // Only include what is in src (-> dist, tests, .. will be excluded) ] -} \ No newline at end of file +} diff --git a/packages/multieditor/tsconfig.production.json b/packages/multieditor/tsconfig.production.json index 4b5c4d12..b8ec8c38 100644 --- a/packages/multieditor/tsconfig.production.json +++ b/packages/multieditor/tsconfig.production.json @@ -1,7 +1,9 @@ +// Use File: Overwrites already generated js files with new js files that have no comments +// Not doing in main 'tsconfig.json' because then the typescript declarations would have no comments too { "extends": "./tsconfig.json", "compilerOptions": { - "declaration": false, + "declaration": false, // '.d.ts' files should have been generated before "removeComments": true } -} \ No newline at end of file +} diff --git a/packages/proxytree/package.json b/packages/proxytree/package.json index 8550a5b7..66a423f9 100644 --- a/packages/proxytree/package.json +++ b/packages/proxytree/package.json @@ -14,14 +14,17 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { - "build": "tsc", - "prepare": "tsc && tsc -p ./tsconfig.production.json", + "build": "yarn run build:esm && yarn run build:cjs", + "prepare": "yarn run build", + "build:esm": "tsc -p ./tsconfig.esm.json", + "build:cjs": "tsc -p ./tsconfig.json && tsc -p ./tsconfig.production.json", "dev:publish": "yalc publish", "dev:push": "yalc push", "watch:push": "tsc-watch --onSuccess \"yarn run dev:push\"", "watch": "tsc -w", "release": "yarn run prepare", - "preview": "npm pack", + "release:manual": "yarn run prepare && yarn run release && npm publish", + "pack": "npm pack", "test": "jest", "test:coverage": "jest --coverage", "lint": "eslint src/**/*" @@ -41,5 +44,6 @@ "LICENSE", "README.md", "CHANGELOG.md" - ] + ], + "sideEffects": false } diff --git a/packages/proxytree/tsconfig.esm.json b/packages/proxytree/tsconfig.esm.json new file mode 100644 index 00000000..9512f0ad --- /dev/null +++ b/packages/proxytree/tsconfig.esm.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "ES2015", + "outDir": "dist/esm", + "declaration": false, // already generated via 'tsconfig.json' in root + "removeComments": true + } +} diff --git a/packages/proxytree/tsconfig.json b/packages/proxytree/tsconfig.json index 600cd205..791f587c 100644 --- a/packages/proxytree/tsconfig.json +++ b/packages/proxytree/tsconfig.json @@ -1,10 +1,11 @@ { "extends": "../tsconfig.default.json", "compilerOptions": { + "module": "commonjs", "rootDir": "src", "outDir": "dist" }, "include": [ "./src/**/*" // Only include what is in src (-> dist, tests, .. will be excluded) ] -} \ No newline at end of file +} diff --git a/packages/proxytree/tsconfig.production.json b/packages/proxytree/tsconfig.production.json index 4b5c4d12..b8ec8c38 100644 --- a/packages/proxytree/tsconfig.production.json +++ b/packages/proxytree/tsconfig.production.json @@ -1,7 +1,9 @@ +// Use File: Overwrites already generated js files with new js files that have no comments +// Not doing in main 'tsconfig.json' because then the typescript declarations would have no comments too { "extends": "./tsconfig.json", "compilerOptions": { - "declaration": false, + "declaration": false, // '.d.ts' files should have been generated before "removeComments": true } -} \ No newline at end of file +} diff --git a/packages/react/package.json b/packages/react/package.json index c7101863..732fee9c 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -25,14 +25,17 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { - "build": "tsc", - "prepare": "tsc && tsc -p ./tsconfig.production.json", + "build": "yarn run build:esm && yarn run build:cjs", + "prepare": "yarn run build", + "build:esm": "tsc -p ./tsconfig.esm.json", + "build:cjs": "tsc -p ./tsconfig.json && tsc -p ./tsconfig.production.json", "dev:publish": "yalc publish", "dev:push": "yalc push", "watch:push": "tsc-watch --onSuccess \"yarn run dev:push\"", "watch": "tsc -w", "release": "yarn run prepare", - "preview": "npm pack", + "release:manual": "yarn run prepare && yarn run release && npm publish", + "pack": "npm pack", "test": "jest", "test:coverage": "jest --coverage", "lint": "eslint src/**/*", @@ -75,5 +78,6 @@ "LICENSE", "README.md", "CHANGELOG.md" - ] + ], + "sideEffects": false } diff --git a/packages/react/tsconfig.esm.json b/packages/react/tsconfig.esm.json new file mode 100644 index 00000000..9512f0ad --- /dev/null +++ b/packages/react/tsconfig.esm.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "ES2015", + "outDir": "dist/esm", + "declaration": false, // already generated via 'tsconfig.json' in root + "removeComments": true + } +} diff --git a/packages/react/tsconfig.json b/packages/react/tsconfig.json index 600cd205..791f587c 100644 --- a/packages/react/tsconfig.json +++ b/packages/react/tsconfig.json @@ -1,10 +1,11 @@ { "extends": "../tsconfig.default.json", "compilerOptions": { + "module": "commonjs", "rootDir": "src", "outDir": "dist" }, "include": [ "./src/**/*" // Only include what is in src (-> dist, tests, .. will be excluded) ] -} \ No newline at end of file +} diff --git a/packages/react/tsconfig.production.json b/packages/react/tsconfig.production.json index 4b5c4d12..b8ec8c38 100644 --- a/packages/react/tsconfig.production.json +++ b/packages/react/tsconfig.production.json @@ -1,7 +1,9 @@ +// Use File: Overwrites already generated js files with new js files that have no comments +// Not doing in main 'tsconfig.json' because then the typescript declarations would have no comments too { "extends": "./tsconfig.json", "compilerOptions": { - "declaration": false, + "declaration": false, // '.d.ts' files should have been generated before "removeComments": true } -} \ No newline at end of file +} diff --git a/packages/tsconfig.default.json b/packages/tsconfig.default.json index 7efcfb92..d6f8f4a3 100644 --- a/packages/tsconfig.default.json +++ b/packages/tsconfig.default.json @@ -5,7 +5,7 @@ /* Basic Options */ // "incremental": true, /* Enable incremental compilation */ "target": "ES2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ - "module": "ES2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ // "lib": [], /* Specify library files to be included in the compilation. */ // "allowJs": true, /* Allow javascript files to be compiled. */ // "checkJs": true, /* Report errors in .js files. */ diff --git a/packages/utils/package.json b/packages/utils/package.json index d97926ce..c8a1196c 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -13,14 +13,17 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { - "build": "tsc", - "prepare": "tsc && tsc -p ./tsconfig.production.json", + "build": "yarn run build:esm && yarn run build:cjs", + "prepare": "yarn run build", + "build:esm": "tsc -p ./tsconfig.esm.json", + "build:cjs": "tsc -p ./tsconfig.json && tsc -p ./tsconfig.production.json", "dev:publish": "yalc publish", "dev:push": "yalc push", "watch:push": "tsc-watch --onSuccess \"yarn run dev:push\"", "watch": "tsc -w", "release": "yarn run prepare", - "preview": "npm pack", + "release:manual": "yarn run prepare && yarn run release && npm publish", + "pack": "npm pack", "test": "jest", "test:coverage": "jest --coverage", "lint": "eslint src/**/*" @@ -40,5 +43,6 @@ "LICENSE", "README.md", "CHANGELOG.md" - ] + ], + "sideEffects": false } diff --git a/packages/utils/tsconfig.esm.json b/packages/utils/tsconfig.esm.json new file mode 100644 index 00000000..9512f0ad --- /dev/null +++ b/packages/utils/tsconfig.esm.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "ES2015", + "outDir": "dist/esm", + "declaration": false, // already generated via 'tsconfig.json' in root + "removeComments": true + } +} diff --git a/packages/utils/tsconfig.json b/packages/utils/tsconfig.json index 600cd205..791f587c 100644 --- a/packages/utils/tsconfig.json +++ b/packages/utils/tsconfig.json @@ -1,10 +1,11 @@ { "extends": "../tsconfig.default.json", "compilerOptions": { + "module": "commonjs", "rootDir": "src", "outDir": "dist" }, "include": [ "./src/**/*" // Only include what is in src (-> dist, tests, .. will be excluded) ] -} \ No newline at end of file +} diff --git a/packages/utils/tsconfig.production.json b/packages/utils/tsconfig.production.json index 4b5c4d12..b8ec8c38 100644 --- a/packages/utils/tsconfig.production.json +++ b/packages/utils/tsconfig.production.json @@ -1,7 +1,9 @@ +// Use File: Overwrites already generated js files with new js files that have no comments +// Not doing in main 'tsconfig.json' because then the typescript declarations would have no comments too { "extends": "./tsconfig.json", "compilerOptions": { - "declaration": false, + "declaration": false, // '.d.ts' files should have been generated before "removeComments": true } -} \ No newline at end of file +} diff --git a/packages/vue/package.json b/packages/vue/package.json index f7075842..ba8a43b6 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -16,14 +16,17 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { - "build": "tsc", - "prepare": "tsc && tsc -p ./tsconfig.production.json", + "build": "yarn run build:esm && yarn run build:cjs", + "prepare": "yarn run build", + "build:esm": "tsc -p ./tsconfig.esm.json", + "build:cjs": "tsc -p ./tsconfig.json && tsc -p ./tsconfig.production.json", "dev:publish": "yalc publish", "dev:push": "yalc push", "watch:push": "tsc-watch --onSuccess \"yarn run dev:push\"", "watch": "tsc -w", "release": "yarn run prepare", - "preview": "npm pack", + "release:manual": "yarn run prepare && yarn run release && npm publish", + "pack": "npm pack", "test": "jest", "test:coverage": "jest --coverage", "lint": "eslint src/**/*" @@ -60,5 +63,6 @@ "LICENSE", "README.md", "CHANGELOG.md" - ] + ], + "sideEffects": false } diff --git a/packages/vue/tsconfig.esm.json b/packages/vue/tsconfig.esm.json new file mode 100644 index 00000000..9512f0ad --- /dev/null +++ b/packages/vue/tsconfig.esm.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "ES2015", + "outDir": "dist/esm", + "declaration": false, // already generated via 'tsconfig.json' in root + "removeComments": true + } +} diff --git a/packages/vue/tsconfig.json b/packages/vue/tsconfig.json index 600cd205..791f587c 100644 --- a/packages/vue/tsconfig.json +++ b/packages/vue/tsconfig.json @@ -1,10 +1,11 @@ { "extends": "../tsconfig.default.json", "compilerOptions": { + "module": "commonjs", "rootDir": "src", "outDir": "dist" }, "include": [ "./src/**/*" // Only include what is in src (-> dist, tests, .. will be excluded) ] -} \ No newline at end of file +} diff --git a/packages/vue/tsconfig.production.json b/packages/vue/tsconfig.production.json index 4b5c4d12..b8ec8c38 100644 --- a/packages/vue/tsconfig.production.json +++ b/packages/vue/tsconfig.production.json @@ -1,7 +1,9 @@ +// Use File: Overwrites already generated js files with new js files that have no comments +// Not doing in main 'tsconfig.json' because then the typescript declarations would have no comments too { "extends": "./tsconfig.json", "compilerOptions": { - "declaration": false, + "declaration": false, // '.d.ts' files should have been generated before "removeComments": true } -} \ No newline at end of file +} From 32f4bdbdffb4f7f7e1ad34181cbf2b9d30cfe4db Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Tue, 3 Aug 2021 07:55:03 +0200 Subject: [PATCH 14/93] fixed typo --- README.md | 8 ++++++ .../develop/tree-shaking/webpack.config.js | 26 +++++++++---------- packages/api/package.json | 1 + packages/event/package.json | 1 + packages/logger/package.json | 1 + packages/multieditor/package.json | 1 + packages/proxytree/package.json | 1 + packages/react/package.json | 1 + packages/utils/package.json | 1 + packages/vue/package.json | 1 + 10 files changed, 29 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index f677870b..f7257c6f 100644 --- a/README.md +++ b/README.md @@ -206,6 +206,14 @@ To find out more about contributing, check out the [CONTRIBUTING.md](https://git Maintainability +### ♥️ Contributors + + + + + +[Become a contributor](https://github.com/agile-ts/agile/blob/master/CONTRIBUTING.md) +
diff --git a/examples/plainjs/develop/tree-shaking/webpack.config.js b/examples/plainjs/develop/tree-shaking/webpack.config.js index bf6f7aa9..64c455e3 100644 --- a/examples/plainjs/develop/tree-shaking/webpack.config.js +++ b/examples/plainjs/develop/tree-shaking/webpack.config.js @@ -1,16 +1,16 @@ -const path = require("path"); +const path = require('path'); module.exports = { - entry: "./src/index.js", - output: { - filename: "main.js", - path: path.resolve(__dirname, "dist"), - }, - mode: "development", - optimization: { - usedExports: true, - innerGraph: true, - sideEffects: true, - }, - devtool: false, + entry: './src/index.js', + output: { + filename: 'main.js', + path: path.resolve(__dirname, 'dist'), + }, + mode: 'development', + optimization: { + usedExports: true, + innerGraph: true, + sideEffects: true, + }, + devtool: false, }; diff --git a/packages/api/package.json b/packages/api/package.json index 1d2812bf..c223a02a 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -13,6 +13,7 @@ "agile-ts" ], "main": "dist/index.js", + "module": "dist/esm/index.js", "types": "dist/index.d.ts", "scripts": { "build": "yarn run build:esm && yarn run build:cjs", diff --git a/packages/event/package.json b/packages/event/package.json index 27cd758e..d6984708 100644 --- a/packages/event/package.json +++ b/packages/event/package.json @@ -12,6 +12,7 @@ "agile-ts" ], "main": "dist/index.js", + "module": "dist/esm/index.js", "types": "dist/index.d.ts", "scripts": { "build": "yarn run build:esm && yarn run build:cjs", diff --git a/packages/logger/package.json b/packages/logger/package.json index ae318b4d..a4ae61f9 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -12,6 +12,7 @@ "agile-ts" ], "main": "dist/index.js", + "module": "dist/esm/index.js", "types": "dist/index.d.ts", "scripts": { "build": "yarn run build:esm && yarn run build:cjs", diff --git a/packages/multieditor/package.json b/packages/multieditor/package.json index 5fa30bc7..7d4ea2df 100644 --- a/packages/multieditor/package.json +++ b/packages/multieditor/package.json @@ -16,6 +16,7 @@ "agile-ts" ], "main": "dist/index.js", + "module": "dist/esm/index.js", "types": "dist/index.d.ts", "scripts": { "build": "yarn run build:esm && yarn run build:cjs", diff --git a/packages/proxytree/package.json b/packages/proxytree/package.json index 66a423f9..557a6367 100644 --- a/packages/proxytree/package.json +++ b/packages/proxytree/package.json @@ -12,6 +12,7 @@ "agile-ts" ], "main": "dist/index.js", + "module": "dist/esm/index.js", "types": "dist/index.d.ts", "scripts": { "build": "yarn run build:esm && yarn run build:cjs", diff --git a/packages/react/package.json b/packages/react/package.json index 732fee9c..20eb51d1 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -23,6 +23,7 @@ "agile-ts" ], "main": "dist/index.js", + "module": "dist/esm/index.js", "types": "dist/index.d.ts", "scripts": { "build": "yarn run build:esm && yarn run build:cjs", diff --git a/packages/utils/package.json b/packages/utils/package.json index c8a1196c..28bb15bc 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -11,6 +11,7 @@ "agile-ts" ], "main": "dist/index.js", + "module": "dist/esm/index.js", "types": "dist/index.d.ts", "scripts": { "build": "yarn run build:esm && yarn run build:cjs", diff --git a/packages/vue/package.json b/packages/vue/package.json index ba8a43b6..0e4e5fdf 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -14,6 +14,7 @@ "reactive" ], "main": "dist/index.js", + "module": "dist/esm/index.js", "types": "dist/index.d.ts", "scripts": { "build": "yarn run build:esm && yarn run build:cjs", From 44f8415e50753b5b251500019e09d89df33a71cf Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Tue, 3 Aug 2021 20:03:28 +0200 Subject: [PATCH 15/93] tested tree shakability at a react project --- .../react/develop/simple-counter/package.json | 10 +- .../react/develop/simple-counter/src/App.js | 14 +- .../react/develop/simple-counter/yarn.lock | 37 +- packages/core/src/collection/collection.ts | 1697 ++++++++++++++++ packages/core/src/collection/index.ts | 1727 +---------------- packages/core/src/computed/computed.ts | 266 +++ packages/core/src/computed/index.ts | 334 +--- packages/core/src/integrations/index.ts | 153 +- .../core/src/integrations/integrations.ts | 151 ++ packages/core/src/runtime/index.ts | 365 +--- packages/core/src/runtime/runtime.ts | 358 ++++ packages/core/src/shared.ts | 169 +- packages/core/src/state/index.ts | 843 +------- packages/core/src/state/state.ts | 807 ++++++++ packages/core/src/storages/index.ts | 355 +--- packages/core/src/storages/storages.ts | 333 ++++ packages/core/tests/unit/agile.test.ts | 4 - packages/core/tests/unit/shared.test.ts | 6 +- 18 files changed, 3821 insertions(+), 3808 deletions(-) create mode 100644 packages/core/src/collection/collection.ts create mode 100644 packages/core/src/computed/computed.ts create mode 100644 packages/core/src/integrations/integrations.ts create mode 100644 packages/core/src/runtime/runtime.ts create mode 100644 packages/core/src/state/state.ts create mode 100644 packages/core/src/storages/storages.ts diff --git a/examples/react/develop/simple-counter/package.json b/examples/react/develop/simple-counter/package.json index 8113e04e..ecaceccb 100644 --- a/examples/react/develop/simple-counter/package.json +++ b/examples/react/develop/simple-counter/package.json @@ -3,8 +3,8 @@ "version": "0.1.0", "private": true, "dependencies": { - "@agile-ts/core": "^0.0.17", - "@agile-ts/react": "^0.1.0", + "@agile-ts/core": "file:.yalc/@agile-ts/core", + "@agile-ts/react": "file:.yalc/@agile-ts/react", "react": "17.0.2", "react-dom": "17.0.2", "react-scripts": "4.0.0" @@ -17,9 +17,9 @@ "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject", - "analyze": "source-map-explorer 'build/static/js/*.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" + "analyze": "yarn run build && source-map-explorer 'build/static/js/*.js'", + "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" }, "browserslist": { "production": [ diff --git a/examples/react/develop/simple-counter/src/App.js b/examples/react/develop/simple-counter/src/App.js index 36c4a037..21bdfe55 100644 --- a/examples/react/develop/simple-counter/src/App.js +++ b/examples/react/develop/simple-counter/src/App.js @@ -1,11 +1,9 @@ -import { Agile } from '@agile-ts/core'; -import { useAgile } from '@agile-ts/react'; +import { createState } from '@agile-ts/core'; +import { useAgile, useValue } from '@agile-ts/react'; -const AgileApp = new Agile(); - -const COUNTER_A = AgileApp.createState(1); -const COUNTER_B = AgileApp.createState(2); -const COUNTER_C = AgileApp.createState(3); +const COUNTER_A = createState(1); +const COUNTER_B = createState(2); +const COUNTER_C = createState(3); const CounterA = () => { const count = useAgile(COUNTER_A); @@ -17,7 +15,7 @@ const CounterA = () => { }; const CounterB = () => { - const count = useAgile(COUNTER_B); + const count = useValue(COUNTER_B); return (
B: {count} diff --git a/examples/react/develop/simple-counter/yarn.lock b/examples/react/develop/simple-counter/yarn.lock index e0c5ba2c..bb661d94 100644 --- a/examples/react/develop/simple-counter/yarn.lock +++ b/examples/react/develop/simple-counter/yarn.lock @@ -2,37 +2,18 @@ # yarn lockfile v1 -"@agile-ts/core@^0.0.17": - version "0.0.17" - resolved "https://registry.yarnpkg.com/@agile-ts/core/-/core-0.0.17.tgz#410ab31ff6279567ff0266a5a55818744598f2c2" - integrity sha512-EVWqf6PkBwDS/gdTTVwo9juyGPrnnKlq8dna3diXGZdDpwEzMc09nGCmLThYM5sEkDQGzir6enn3Oo2l+7Zp2Q== +"@agile-ts/core@file:.yalc/@agile-ts/core": + version "0.2.0-alpha.3" dependencies: - "@agile-ts/logger" "^0.0.4" - "@agile-ts/utils" "^0.0.4" + "@agile-ts/utils" "^0.0.7" -"@agile-ts/logger@^0.0.4": - version "0.0.4" - resolved "https://registry.yarnpkg.com/@agile-ts/logger/-/logger-0.0.4.tgz#7f4d82ef8f03b13089af0878c360575c43f0962d" - integrity sha512-qm0obAKqJMaPKM+c76gktRXyw3OL1v39AnhMZ0FBGwJqHWU+fLRkCzlQwjaROCr3F1XP01Lc/Ls3efF0WzyEPw== - dependencies: - "@agile-ts/utils" "^0.0.4" - -"@agile-ts/proxytree@^0.0.3": - version "0.0.3" - resolved "https://registry.yarnpkg.com/@agile-ts/proxytree/-/proxytree-0.0.3.tgz#e3dacab123a311f2f0d4a0369793fe90fdab7569" - integrity sha512-auO6trCo7ivLJYuLjxrnK4xuUTangVPTq8UuOMTlGbJFjmb8PLEkaXuRoVGSzv9jsT2FeS7KsP7Fs+yvv0WPdg== - -"@agile-ts/react@^0.0.18": - version "0.0.18" - resolved "https://registry.yarnpkg.com/@agile-ts/react/-/react-0.0.18.tgz#db1a617ad535f7a70254d62980d97350d4a85718" - integrity sha512-K2FO3Odqaw/XkU3DO/mWSLkxLn45W7pXk/UlZl5E/CQPFFWlWsjuxtH/C/kfK+E6rnaNoToTjGscmcoeN/bLjQ== - dependencies: - "@agile-ts/proxytree" "^0.0.3" +"@agile-ts/react@file:.yalc/@agile-ts/react": + version "0.1.2" -"@agile-ts/utils@^0.0.4": - version "0.0.4" - resolved "https://registry.yarnpkg.com/@agile-ts/utils/-/utils-0.0.4.tgz#66e9536e561796489a37155da6b74ce2dc482697" - integrity sha512-GiZyTYmCm4j2N57oDjeMuPpfQdgn9clb0Cxpfuwi2Bq5T/KPXlaROLsVGwHLjwwT+NX7xxr5qNJH8pZTnHnYRQ== +"@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.10.4": version "7.10.4" diff --git a/packages/core/src/collection/collection.ts b/packages/core/src/collection/collection.ts new file mode 100644 index 00000000..31b8a107 --- /dev/null +++ b/packages/core/src/collection/collection.ts @@ -0,0 +1,1697 @@ +import { + Agile, + Item, + Group, + GroupKey, + Selector, + SelectorKey, + StorageKey, + GroupConfigInterface, + isValidObject, + normalizeArray, + copy, + CollectionPersistent, + GroupAddConfigInterface, + ComputedTracker, + generateId, + SideEffectConfigInterface, + SelectorConfigInterface, + removeProperties, + isFunction, + LogCodeManager, + PatchOptionConfigInterface, + defineConfig, +} from '../internal'; + +export class Collection< + DataType extends Object = DefaultItem, + GroupValueType = Array // To extract the Group Type Value in Integration methods like 'useAgile()' +> { + // Agile Instance the Collection belongs to + public agileInstance: () => Agile; + + public config: CollectionConfigInterface; + private initialConfig: CreateCollectionConfigInterface; + + // Key/Name identifier of the Collection + public _key?: CollectionKey; + // Amount of the Items stored in the Collection + public size = 0; + // Items stored in the Collection + public data: { [key: string]: Item } = {}; + // Whether the Collection is persisted in an external Storage + public isPersisted = false; + // Manages the permanent persistent in external Storages + public persistent: CollectionPersistent | undefined; + + // Registered Groups of Collection + public groups: { [key: string]: Group } = {}; + // Registered Selectors of Collection + public selectors: { [key: string]: Selector } = {}; + + // Whether the Collection was instantiated correctly + public isInstantiated = false; + + // Helper property to check whether an unknown instance is a Collection, + // without importing the Collection itself for using 'instanceof' (Treeshaking support) + public isCollection = true; + + /** + * A Collection manages a reactive set of Information + * that we need to remember globally at a later point in time. + * While providing a toolkit to use and mutate this set of Information. + * + * It is designed for arrays of data objects following the same pattern. + * + * Each of these data object must have a unique `primaryKey` to be correctly identified later. + * + * You can create as many global Collections as you need. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/) + * + * @public + * @param agileInstance - Instance of Agile the Collection belongs to. + * @param config - Configuration object + */ + constructor(agileInstance: Agile, config: CollectionConfig = {}) { + this.agileInstance = () => agileInstance; + let _config = typeof config === 'function' ? config(this) : config; + _config = defineConfig(_config, { + primaryKey: 'id', + groups: {}, + selectors: {}, + defaultGroupKey: 'default', + }); + this._key = _config.key; + this.config = { + defaultGroupKey: _config.defaultGroupKey as any, + primaryKey: _config.primaryKey as any, + }; + this.initialConfig = _config; + + this.initGroups(_config.groups as any); + this.initSelectors(_config.selectors as any); + + this.isInstantiated = true; + + // Add 'initialData' to Collection + // (after 'isInstantiated' to add them properly to the Collection) + if (_config.initialData) this.collect(_config.initialData); + + // Reselect Selector Items + // Necessary because the selection of an Item + // hasn't worked with a not correctly 'instantiated' Collection before + for (const key in this.selectors) this.selectors[key].reselect(); + + // Rebuild of Groups + // Not necessary because if Items are added to the Collection, + // (after 'isInstantiated = true') + // the Groups which contain these added Items are rebuilt. + // for (const key in this.groups) this.groups[key].rebuild(); + } + + /** + * Updates the key/name identifier of the Collection. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/properties#key) + * + * @public + * @param value - New key/name identifier. + */ + public set key(value: CollectionKey | undefined) { + this.setKey(value); + } + + /** + * Returns the key/name identifier of the Collection. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/properties#key) + * + * @public + */ + public get key(): CollectionKey | undefined { + return this._key; + } + + /** + * Updates the key/name identifier of the Collection. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#setkey) + * + * @public + * @param value - New key/name identifier. + */ + public setKey(value: CollectionKey | undefined) { + const oldKey = this._key; + + // Update Collection key + this._key = value; + + // Update key in Persistent (only if oldKey is equal to persistentKey + // because otherwise the persistentKey is detached from the Collection key + // -> not managed by Collection anymore) + if (value != null && this.persistent?._key === oldKey) + this.persistent?.setKey(value); + + return this; + } + + /** + * Creates a new Group without associating it to the Collection. + * + * This way of creating a Group is intended for use in the Collection configuration object, + * where the `constructor()` takes care of the binding. + * + * After a successful initiation of the Collection we recommend using `createGroup()`, + * because it automatically connects the Group to the Collection. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#group) + * + * @public + * @param initialItems - Key/Name identifiers of the Items to be clustered by the Group. + * @param config - Configuration object + */ + public Group( + initialItems?: Array, + config: GroupConfigInterface = {} + ): Group { + if (this.isInstantiated) { + const key = config.key ?? generateId(); + LogCodeManager.log('1B:02:00'); + return this.createGroup(key, initialItems); + } + + return new Group(this, initialItems, config); + } + + /** + * Creates a new Selector without associating it to the Collection. + * + * This way of creating a Selector is intended for use in the Collection configuration object, + * where the `constructor()` takes care of the binding. + * + * After a successful initiation of the Collection we recommend using `createSelector()`, + * because it automatically connects the Group to the Collection. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#selector) + * + * @public + * @param initialKey - Key/Name identifier of the Item to be represented by the Selector. + * @param config - Configuration object + */ + public Selector( + initialKey: ItemKey | null, + config: SelectorConfigInterface = {} + ): Selector { + if (this.isInstantiated) { + const key = config.key ?? generateId(); + LogCodeManager.log('1B:02:01'); + return this.createSelector(key, initialKey); + } + + return new Selector(this, initialKey, config); + } + + /** + * Sets up the specified Groups or Group keys + * and assigns them to the Collection if they are valid. + * + * It also instantiates and assigns the default Group to the Collection. + * The default Group reflects the default pattern of the Collection. + * + * @internal + * @param groups - Entire Groups or Group keys to be set up. + */ + public initGroups(groups: { [key: string]: Group } | string[]): void { + if (!groups) return; + let groupsObject: { [key: string]: Group } = {}; + + // If groups is Array of Group keys/names, create the Groups based on these keys + if (Array.isArray(groups)) { + groups.forEach((groupKey) => { + groupsObject[groupKey] = new Group(this, [], { + key: groupKey, + }); + }); + } else groupsObject = groups; + + // Add default Group + groupsObject[this.config.defaultGroupKey] = new Group(this, [], { + key: this.config.defaultGroupKey, + }); + + // Assign missing key/name to Group based on the property key + for (const key in groupsObject) + if (groupsObject[key]._key == null) groupsObject[key].setKey(key); + + this.groups = groupsObject; + } + + /** + * Sets up the specified Selectors or Selector keys + * and assigns them to the Collection if they are valid. + * + * @internal + * @param selectors - Entire Selectors or Selector keys to be set up. + */ + public initSelectors(selectors: { [key: string]: Selector } | string[]) { + if (!selectors) return; + let selectorsObject: { [key: string]: Selector } = {}; + + // If selectors is Array of Selector keys/names, create the Selectors based on these keys + if (Array.isArray(selectors)) { + selectors.forEach((selectorKey) => { + selectorsObject[selectorKey] = new Selector( + this, + selectorKey, + { + key: selectorKey, + } + ); + }); + } else selectorsObject = selectors; + + // Assign missing key/name to Selector based on the property key + for (const key in selectorsObject) + if (selectorsObject[key]._key == null) selectorsObject[key].setKey(key); + + this.selectors = selectorsObject; + } + + /** + * Appends new data objects following the same pattern to the end of the Collection. + * + * Each collected `data object` requires a unique identifier at the primaryKey property (by default 'id') + * to be correctly identified later. + * + * For example, if we collect some kind of user object, + * it must contain such unique identifier at 'id' + * to be added to the Collection. + * ``` + * MY_COLLECTION.collect({id: '1', name: 'jeff'}); // valid + * MY_COLLECTION.collect({name: 'frank'}); // invalid + * ``` + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#collect) + * + * @public + * @param data - Data objects or entire Items to be added. + * @param groupKeys - Group/s to which the specified data objects or Items are to be added. + * @param config - Configuration object + */ + public collect( + data: DataType | Item | Array>, + groupKeys?: GroupKey | Array, + config: CollectConfigInterface = {} + ): this { + const _data = normalizeArray>(data); + const _groupKeys = normalizeArray(groupKeys); + const defaultGroupKey = this.config.defaultGroupKey; + const primaryKey = this.config.primaryKey; + config = defineConfig(config, { + method: 'push', + background: false, + patch: false, + select: false, + }); + + // Add default groupKey, since all Items are added to the default Group + if (!_groupKeys.includes(defaultGroupKey)) _groupKeys.push(defaultGroupKey); + + // Create not existing Groups + _groupKeys.forEach( + (key) => this.groups[key] == null && this.createGroup(key) + ); + + _data.forEach((data, index) => { + let itemKey; + let success = false; + + // Assign Data or Item to Collection + if (data instanceof Item) { + success = this.assignItem(data, { + background: config.background, + }); + itemKey = data._key; + } else { + success = this.assignData(data, { + patch: config.patch, + background: config.background, + }); + itemKey = data[primaryKey]; + } + + // Add itemKey to provided Groups and create corresponding Selector + if (success) { + _groupKeys.forEach((groupKey) => { + this.getGroup(groupKey)?.add(itemKey, { + method: config.method, + background: config.background, + }); + }); + + if (config.select) this.createSelector(itemKey, itemKey); + } + + if (config.forEachItem) config.forEachItem(data, itemKey, success, index); + }); + + return this; + } + + /** + * Updates the Item `data object` with the specified `object with changes`, if the Item exists. + * By default the `object with changes` is merged into the Item `data object` at top level. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#update) + * + * @public + * @param itemKey - Key/Name identifier of the Item to be updated. + * @param changes - Object with changes to be merged into the Item data object. + * @param config - Configuration object + */ + public update( + itemKey: ItemKey, + changes: DefaultItem | DataType, + config: UpdateConfigInterface = {} + ): Item | undefined { + const item = this.getItem(itemKey, { notExisting: true }); + const primaryKey = this.config.primaryKey; + config = defineConfig(config, { + patch: true, + background: false, + }); + + // Check if the given conditions are suitable for a update action + if (item == null) { + LogCodeManager.log('1B:03:00', [itemKey, this._key]); + return undefined; + } + if (!isValidObject(changes)) { + LogCodeManager.log('1B:03:01', [itemKey, this._key]); + return undefined; + } + + const oldItemKey = item._value[primaryKey]; + const newItemKey = changes[primaryKey] || oldItemKey; + + // Update itemKey if the new itemKey differs from the old one + if (oldItemKey !== newItemKey) + this.updateItemKey(oldItemKey, newItemKey, { + background: config.background, + }); + + // Patch changes into Item data object + if (config.patch) { + // Delete primaryKey property from 'changes object' because if it has changed, + // it is correctly updated in the above called 'updateItemKey()' method + if (changes[primaryKey]) delete changes[primaryKey]; + + let patchConfig: { addNewProperties?: boolean } = + typeof config.patch === 'object' ? config.patch : {}; + patchConfig = { + addNewProperties: true, + ...patchConfig, + }; + + item.patch(changes as any, { + background: config.background, + addNewProperties: patchConfig.addNewProperties, + }); + } + // Apply changes to Item data object + else { + // Ensure that the current Item identifier isn't different from the 'changes object' itemKey + if (changes[this.config.primaryKey] !== itemKey) { + changes[this.config.primaryKey] = itemKey; + LogCodeManager.log('1B:02:02', [], changes); + } + + item.set(changes as any, { + background: config.background, + }); + } + + return item; + } + + /** + * Creates a new Group and associates it to the Collection. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#createGroup) + * + * @public + * @param groupKey - Unique identifier of the Group to be created. + * @param initialItems - Key/Name identifiers of the Items to be clustered by the Group. + */ + public createGroup( + groupKey: GroupKey, + initialItems: Array = [] + ): Group { + let group = this.getGroup(groupKey, { notExisting: true }); + if (!this.isInstantiated) LogCodeManager.log('1B:02:03'); + + // Check if Group already exists + if (group != null) { + if (!group.isPlaceholder) { + LogCodeManager.log('1B:03:02', [groupKey]); + return group; + } + group.set(initialItems, { overwrite: true }); + return group; + } + + // Create new Group + group = new Group(this, initialItems, { key: groupKey }); + this.groups[groupKey] = group; + + return group; + } + + /** + * Returns a boolean indicating whether a Group with the specified `groupKey` + * exists in the Collection or not. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#hasgroup) + * + * @public + * @param groupKey - Key/Name identifier of the Group to be checked for existence. + * @param config - Configuration object + */ + public hasGroup( + groupKey: GroupKey | undefined, + config: HasConfigInterface = {} + ): boolean { + return !!this.getGroup(groupKey, config); + } + + /** + * Retrieves a single Group with the specified key/name identifier from the Collection. + * + * If the to retrieve Group doesn't exist, `undefined` is returned. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getgroup) + * + * @public + * @param groupKey - Key/Name identifier of the Group. + * @param config - Configuration object + */ + public getGroup( + groupKey: GroupKey | undefined | null, + config: HasConfigInterface = {} + ): Group | undefined { + config = defineConfig(config, { + notExisting: false, + }); + + // Retrieve Group + const group = groupKey ? this.groups[groupKey] : undefined; + + // Check if retrieved Group exists + if (group == null || (!config.notExisting && !group.exists)) + return undefined; + + ComputedTracker.tracked(group.observers['value']); + return group; + } + + /** + * Retrieves the default Group from the Collection. + * + * Every Collection should have a default Group, + * which represents the default pattern of the Collection. + * + * If the default Group, for what ever reason, doesn't exist, `undefined` is returned. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getdefaultgroup) + * + * @public + */ + public getDefaultGroup(): Group | undefined { + return this.getGroup(this.config.defaultGroupKey); + } + + /** + * Retrieves a single Group with the specified key/name identifier from the Collection. + * + * If the to retrieve Group doesn't exist, a reference Group is returned. + * This has the advantage that Components that have the reference Group bound to themselves + * are rerenderd when the original Group is created. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getgroupwithreference) + * + * @public + * @param groupKey - Key/Name identifier of the Group. + */ + public getGroupWithReference(groupKey: GroupKey): Group { + let group = this.getGroup(groupKey, { notExisting: true }); + + // Create dummy Group to hold reference + if (group == null) { + group = new Group(this, [], { + key: groupKey, + isPlaceholder: true, + }); + this.groups[groupKey] = group; + } + + ComputedTracker.tracked(group.observers['value']); + return group; + } + + /** + * Removes a Group with the specified key/name identifier from the Collection, + * if it exists in the Collection. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#removegroup) + * + * @public + * @param groupKey - Key/Name identifier of the Group to be removed. + */ + public removeGroup(groupKey: GroupKey): this { + if (this.groups[groupKey] != null) delete this.groups[groupKey]; + return this; + } + + /** + * Returns the count of registered Groups in the Collection. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getgroupcount) + * + * @public + */ + public getGroupCount(): number { + let size = 0; + Object.keys(this.groups).map(() => size++); + return size; + } + + /** + * Creates a new Selector and associates it to the Collection. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#createSelector) + * + * @public + * @param selectorKey - Unique identifier of the Selector to be created. + * @param itemKey - Key/Name identifier of the Item to be represented by the Selector. + */ + public createSelector( + selectorKey: SelectorKey, + itemKey: ItemKey | null + ): Selector { + let selector = this.getSelector(selectorKey, { notExisting: true }); + if (!this.isInstantiated) LogCodeManager.log('1B:02:04'); + + // Check if Selector already exists + if (selector != null) { + if (!selector.isPlaceholder) { + LogCodeManager.log('1B:03:03', [selectorKey]); + return selector; + } + selector.select(itemKey, { overwrite: true }); + return selector; + } + + // Create new Selector + selector = new Selector(this, itemKey, { + key: selectorKey, + }); + this.selectors[selectorKey] = selector; + + return selector; + } + + /** + * Creates a new Selector and associates it to the Collection. + * + * The specified `itemKey` is used as the unique identifier key of the new Selector. + * ``` + * MY_COLLECTION.select('1'); + * // is equivalent to + * MY_COLLECTION.createSelector('1', '1'); + * ``` + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#select) + * + * @public + * @param itemKey - Key/Name identifier of the Item to be represented by the Selector + * and used as unique identifier of the Selector. + */ + public select(itemKey: ItemKey): Selector { + return this.createSelector(itemKey, itemKey); + } + + /** + * Returns a boolean indicating whether a Selector with the specified `selectorKey` + * exists in the Collection or not. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#hasselector) + * + * @public + * @param selectorKey - Key/Name identifier of the Selector to be checked for existence. + * @param config - Configuration object + */ + public hasSelector( + selectorKey: SelectorKey | undefined, + config: HasConfigInterface = {} + ): boolean { + return !!this.getSelector(selectorKey, config); + } + + /** + * Retrieves a single Selector with the specified key/name identifier from the Collection. + * + * If the to retrieve Selector doesn't exist, `undefined` is returned. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getselector) + * + * @public + * @param selectorKey - Key/Name identifier of the Selector. + * @param config - Configuration object + */ + public getSelector( + selectorKey: SelectorKey | undefined | null, + config: HasConfigInterface = {} + ): Selector | undefined { + config = defineConfig(config, { + notExisting: false, + }); + + // Get Selector + const selector = selectorKey ? this.selectors[selectorKey] : undefined; + + // Check if Selector exists + if (selector == null || (!config.notExisting && !selector.exists)) + return undefined; + + ComputedTracker.tracked(selector.observers['value']); + return selector; + } + + /** + * Retrieves a single Selector with the specified key/name identifier from the Collection. + * + * If the to retrieve Selector doesn't exist, a reference Selector is returned. + * This has the advantage that Components that have the reference Selector bound to themselves + * are rerenderd when the original Selector is created. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getselectorwithreference) + * + * @public + * @param selectorKey - Key/Name identifier of the Selector. + */ + public getSelectorWithReference( + selectorKey: SelectorKey + ): Selector { + let selector = this.getSelector(selectorKey, { notExisting: true }); + + // Create dummy Selector to hold reference + if (selector == null) { + selector = new Selector(this, null, { + key: selectorKey, + isPlaceholder: true, + }); + this.selectors[selectorKey] = selector; + } + + ComputedTracker.tracked(selector.observers['value']); + return selector; + } + + /** + * Removes a Selector with the specified key/name identifier from the Collection, + * if it exists in the Collection. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#removeselector) + * + * @public + * @param selectorKey - Key/Name identifier of the Selector to be removed. + */ + public removeSelector(selectorKey: SelectorKey): this { + if (this.selectors[selectorKey] != null) { + this.selectors[selectorKey].unselect(); + delete this.selectors[selectorKey]; + } + return this; + } + + /** + * Returns the count of registered Selectors in the Collection. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getselectorcount) + * + * @public + */ + public getSelectorCount(): number { + let size = 0; + Object.keys(this.selectors).map(() => size++); + return size; + } + + /** + * Returns a boolean indicating whether a Item with the specified `itemKey` + * exists in the Collection or not. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#hasitem) + * + * @public + * @param itemKey - Key/Name identifier of the Item. + * @param config - Configuration object + */ + public hasItem( + itemKey: ItemKey | undefined, + config: HasConfigInterface = {} + ): boolean { + return !!this.getItem(itemKey, config); + } + + /** + * Retrieves a single Item with the specified key/name identifier from the Collection. + * + * If the to retrieve Item doesn't exist, `undefined` is returned. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getitem) + * + * @public + * @param itemKey - Key/Name identifier of the Item. + * @param config - Configuration object + */ + public getItem( + itemKey: ItemKey | undefined | null, + config: HasConfigInterface = {} + ): Item | undefined { + config = defineConfig(config, { + notExisting: false, + }); + + // Get Item + const item = itemKey != null ? this.data[itemKey] : undefined; + + // Check if Item exists + if (item == null || (!config.notExisting && !item.exists)) return undefined; + + ComputedTracker.tracked(item.observers['value']); + return item; + } + + /** + * Retrieves a single Item with the specified key/name identifier from the Collection. + * + * If the to retrieve Item doesn't exist, a reference Item is returned. + * This has the advantage that Components that have the reference Item bound to themselves + * are rerenderd when the original Item is created. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getitemwithreference) + * + * @public + * @param itemKey - Key/Name identifier of the Item. + */ + public getItemWithReference(itemKey: ItemKey): Item { + let item = this.getItem(itemKey, { notExisting: true }); + + // Create dummy Item to hold reference + if (item == null) item = this.createPlaceholderItem(itemKey, true); + + ComputedTracker.tracked(item.observers['value']); + return item; + } + + /** + * Creates a placeholder Item + * that can be used to hold a reference to a not existing Item. + * + * @internal + * @param itemKey - Unique identifier of the to create placeholder Item. + * @param addToCollection - Whether to add the Item to be created to the Collection. + */ + public createPlaceholderItem( + itemKey: ItemKey, + addToCollection = false + ): Item { + // Create placeholder Item + const item = new Item( + this, + { + [this.config.primaryKey]: itemKey, // Setting primaryKey of the Item to passed itemKey + dummy: 'item', + } as any, + { isPlaceholder: true } + ); + + // Add placeholder Item to Collection + if ( + addToCollection && + !Object.prototype.hasOwnProperty.call(this.data, itemKey) + ) + this.data[itemKey] = item; + + ComputedTracker.tracked(item.observers['value']); + return item; + } + + /** + * Retrieves the value (data object) of a single Item + * with the specified key/name identifier from the Collection. + * + * If the to retrieve Item containing the value doesn't exist, `undefined` is returned. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getitemvalue) + * + * @public + * @param itemKey - Key/Name identifier of the Item. + * @param config - Configuration object + */ + public getItemValue( + itemKey: ItemKey | undefined, + config: HasConfigInterface = {} + ): DataType | undefined { + const item = this.getItem(itemKey, config); + if (item == null) return undefined; + return item.value; + } + + /** + * Retrieves all Items from the Collection. + * ``` + * MY_COLLECTION.getAllItems(); + * // is equivalent to + * MY_COLLECTION.getDefaultGroup().items; + * ``` + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getallitems) + * + * @public + * @param config - Configuration object + */ + public getAllItems(config: HasConfigInterface = {}): Array> { + config = defineConfig(config, { + notExisting: false, + }); + + const defaultGroup = this.getDefaultGroup(); + let items: Array> = []; + + // If config.notExisting transform the data object into array since it contains all Items, + // otherwise return the default Group Items + if (config.notExisting) { + for (const key in this.data) items.push(this.data[key]); + } else { + // Why default Group Items and not all '.exists === true' Items? + // Because the default Group keeps track of all existing Items. + // It also does control the Collection output in binding methods like 'useAgile()' + // and therefore should do it here too. + items = defaultGroup?.getItems() || []; + } + + return items; + } + + /** + * Retrieves the values (data objects) of all Items from the Collection. + * ``` + * MY_COLLECTION.getAllItemValues(); + * // is equivalent to + * MY_COLLECTION.getDefaultGroup().output; + * ``` + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getallitemvalues) + * + * @public + * @param config - Configuration object + */ + public getAllItemValues(config: HasConfigInterface = {}): Array { + const items = this.getAllItems(config); + return items.map((item) => item.value); + } + + /** + * Preserves the Collection `value` in the corresponding external Storage. + * + * The Collection key/name is used as the unique identifier for the Persistent. + * If that is not desired or the Collection has no unique identifier, + * please specify a separate unique identifier for the Persistent. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#persist) + * + * @public + * @param config - Configuration object + */ + public persist(config?: CollectionPersistentConfigInterface): this; + /** + * Preserves the Collection `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/collection/methods/#persist) + * + * @public + * @param key - Key/Name identifier of Persistent. + * @param config - Configuration object + */ + public persist( + key?: StorageKey, + config?: CollectionPersistentConfigInterface + ): this; + public persist( + keyOrConfig: StorageKey | CollectionPersistentConfigInterface = {}, + config: CollectionPersistentConfigInterface = {} + ): this { + let _config: CollectionPersistentConfigInterface; + let key: StorageKey | undefined; + + if (isValidObject(keyOrConfig)) { + _config = keyOrConfig as CollectionPersistentConfigInterface; + key = this._key; + } else { + _config = config || {}; + key = keyOrConfig as StorageKey; + } + + _config = defineConfig(_config, { + loadValue: true, + storageKeys: [], + defaultStorageKey: null as any, + }); + + // Check if Collection is already persisted + if (this.persistent != null && this.isPersisted) return this; + + // Create Persistent (-> persist value) + this.persistent = new CollectionPersistent(this, { + instantiate: _config.loadValue, + storageKeys: _config.storageKeys, + key: key, + defaultStorageKey: _config.defaultStorageKey, + }); + + return this; + } + + /** + * Fires immediately after the persisted `value` + * is loaded into the Collection from a corresponding external Storage. + * + * Registering such callback function makes only sense + * when the Collection is [persisted](https://agile-ts.org/docs/core/collection/methods/#persist) in an external Storage. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#onload) + * + * @public + * @param callback - A function to be executed after the externally persisted `value` was loaded into the Collection. + */ + 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 Collection is already persisted ('isPersisted') fire specified callback immediately + if (this.isPersisted) callback(true); + + return this; + } + + /** + * Removes all Items from the Collection + * and resets all Groups and Selectors of the Collection. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#reset) + * + * @public + */ + public reset(): this { + // Reset data + this.data = {}; + this.size = 0; + + // Reset Groups + for (const key in this.groups) this.getGroup(key)?.reset(); + + // Reset Selectors + for (const key in this.selectors) this.getSelector(key)?.reset(); + + return this; + } + + /** + * Puts `itemKeys/s` into Group/s. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#put) + * + * @public + * @param itemKeys - `itemKey/s` to be put into the specified Group/s. + * @param groupKeys - Key/Name Identifier/s of the Group/s the specified `itemKey/s` are to put in. + * @param config - Configuration object + */ + public put( + itemKeys: ItemKey | Array, + groupKeys: GroupKey | Array, + config: GroupAddConfigInterface = {} + ): this { + const _itemKeys = normalizeArray(itemKeys); + const _groupKeys = normalizeArray(groupKeys); + + // Assign itemKeys to Groups + _groupKeys.forEach((groupKey) => { + this.getGroup(groupKey)?.add(_itemKeys, config); + }); + + return this; + } + + /** + * Moves specified `itemKey/s` from one Group to another Group. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#move) + * + * @public + * @param itemKeys - `itemKey/s` to be moved. + * @param oldGroupKey - Key/Name Identifier of the Group the `itemKey/s` are moved from. + * @param newGroupKey - Key/Name Identifier of the Group the `itemKey/s` are moved in. + * @param config - Configuration object + */ + public move( + itemKeys: ItemKey | Array, + oldGroupKey: GroupKey, + newGroupKey: GroupKey, + config: GroupAddConfigInterface = {} + ): this { + const _itemKeys = normalizeArray(itemKeys); + + // Remove itemKeys from old Group + this.getGroup(oldGroupKey)?.remove( + _itemKeys, + removeProperties(config, ['method', 'overwrite']) + ); + + // Assign itemKeys to new Group + this.getGroup(newGroupKey)?.add(_itemKeys, config); + + return this; + } + + /** + * Updates the key/name identifier of the Item + * and returns a boolean indicating + * whether the Item identifier was updated successfully. + * + * @internal + * @param oldItemKey - Old key/name Item identifier. + * @param newItemKey - New key/name Item identifier. + * @param config - Configuration object + */ + public updateItemKey( + oldItemKey: ItemKey, + newItemKey: ItemKey, + config: UpdateItemKeyConfigInterface = {} + ): boolean { + const item = this.getItem(oldItemKey, { notExisting: true }); + config = defineConfig(config, { + background: false, + }); + + if (item == null || oldItemKey === newItemKey) return false; + + // Check if Item with newItemKey already exists + if (this.hasItem(newItemKey)) { + LogCodeManager.log('1B:03:04', [oldItemKey, newItemKey, this._key]); + return false; + } + + // Update itemKey in data object + delete this.data[oldItemKey]; + this.data[newItemKey] = item; + + // Update key/name of the Item + item.setKey(newItemKey, { + background: config.background, + }); + + // Update Persistent key of the Item if it follows the Item Storage Key pattern + // and therefore differs from the actual Item key + // (-> isn't automatically updated when the Item key is updated) + if ( + item.persistent != null && + item.persistent._key === + CollectionPersistent.getItemStorageKey(oldItemKey, this._key) + ) + item.persistent?.setKey( + CollectionPersistent.getItemStorageKey(newItemKey, this._key) + ); + + // Update itemKey in Groups + for (const groupKey in this.groups) { + const group = this.getGroup(groupKey, { notExisting: true }); + if (group == null || !group.has(oldItemKey)) continue; + group.replace(oldItemKey, newItemKey, { background: config.background }); + } + + // Update itemKey in Selectors + for (const selectorKey in this.selectors) { + const selector = this.getSelector(selectorKey, { notExisting: true }); + if (selector == null) continue; + + // Reselect Item in Selector that has selected the newItemKey. + // Necessary because potential reference placeholder Item got overwritten + // with the new (renamed) Item + // -> has to find the new Item at selected itemKey + // since the placeholder Item got overwritten + if (selector.hasSelected(newItemKey, false)) { + selector.reselect({ + force: true, // Because itemKeys are the same (but not the Items at this itemKey anymore) + background: config.background, + }); + } + + // Select newItemKey in Selector that has selected the oldItemKey + if (selector.hasSelected(oldItemKey, false)) + selector.select(newItemKey, { + background: config.background, + }); + } + + return true; + } + + /** + * Returns all key/name identifiers of the Group/s containing the specified `itemKey`. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getgroupkeysthathaveitemkey) + * + * @public + * @param itemKey - `itemKey` to be contained in Group/s. + */ + public getGroupKeysThatHaveItemKey(itemKey: ItemKey): Array { + const groupKeys: Array = []; + for (const groupKey in this.groups) { + const group = this.groups[groupKey]; + if (group?.has(itemKey)) groupKeys.push(groupKey); + } + return groupKeys; + } + + /** + * Removes Item/s from: + * + * - `.everywhere()`: + * Removes Item/s from the entire Collection and all its Groups and Selectors (i.e. from everywhere) + * ``` + * MY_COLLECTION.remove('1').everywhere(); + * // is equivalent to + * MY_COLLECTION.removeItems('1'); + * ``` + * - `.fromGroups()`: + * Removes Item/s only from specified Groups. + * ``` + * MY_COLLECTION.remove('1').fromGroups(['1', '2']); + * // is equivalent to + * MY_COLLECTION.removeFromGroups('1', ['1', '2']); + * ``` + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#remove) + * + * @public + * @param itemKeys - Item/s with identifier/s to be removed. + */ + public remove( + itemKeys: ItemKey | Array + ): { + fromGroups: (groups: Array | ItemKey) => Collection; + everywhere: (config?: RemoveItemsConfigInterface) => Collection; + } { + return { + fromGroups: (groups: Array | ItemKey) => + this.removeFromGroups(itemKeys, groups), + everywhere: (config) => this.removeItems(itemKeys, config || {}), + }; + } + + /** + * Remove Item/s from specified Group/s. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#removefromgroups) + * + * @public + * @param itemKeys - Key/Name Identifier/s of the Item/s to be removed from the Group/s. + * @param groupKeys - Key/Name Identifier/s of the Group/s the Item/s are to remove from. + */ + public removeFromGroups( + itemKeys: ItemKey | Array, + groupKeys: GroupKey | Array + ): this { + const _itemKeys = normalizeArray(itemKeys); + const _groupKeys = normalizeArray(groupKeys); + + _itemKeys.forEach((itemKey) => { + let removedFromGroupsCount = 0; + + // Remove itemKey from the Groups + _groupKeys.forEach((groupKey) => { + const group = this.getGroup(groupKey, { notExisting: true }); + if (!group?.has(itemKey)) return; + group.remove(itemKey); + removedFromGroupsCount++; + }); + + // If the Item was removed from each Group representing the Item, + // remove it completely + if ( + removedFromGroupsCount >= + this.getGroupKeysThatHaveItemKey(itemKey).length + ) + this.removeItems(itemKey); + }); + + return this; + } + + /** + * Removes Item/s from the entire Collection and all the Collection's Groups and Selectors. + * + * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#removeitems) + * + * @public + * @param itemKeys - Key/Name identifier/s of the Item/s to be removed from the entire Collection. + * @param config - Configuration object + */ + public removeItems( + itemKeys: ItemKey | Array, + config: RemoveItemsConfigInterface = {} + ): this { + config = defineConfig(config, { + notExisting: false, + removeSelector: false, + }); + const _itemKeys = normalizeArray(itemKeys); + + _itemKeys.forEach((itemKey) => { + const item = this.getItem(itemKey, { notExisting: config.notExisting }); + if (item == null) return; + const wasPlaceholder = item.isPlaceholder; + + // Remove Item from the Groups + for (const groupKey in this.groups) { + const group = this.getGroup(groupKey, { notExisting: true }); + if (group?.has(itemKey)) group?.remove(itemKey); + } + + // Remove Item from Storage + item.persistent?.removePersistedValue(); + + // Remove Item from Collection + delete this.data[itemKey]; + + // Reselect or remove Selectors which have represented the removed Item + for (const selectorKey in this.selectors) { + const selector = this.getSelector(selectorKey, { notExisting: true }); + if (selector != null && selector.hasSelected(itemKey, false)) { + if (config.removeSelector) { + // Remove Selector + this.removeSelector(selector._key ?? 'unknown'); + } else { + // Reselect Item in Selector + // in order to create a new dummyItem + // to hold a reference to the now not existing Item + selector.reselect({ force: true }); + } + } + } + + if (!wasPlaceholder) this.size--; + }); + + return this; + } + + /** + * Assigns the provided `data` object to an already existing Item + * with specified key/name identifier found in the `data` object. + * If the Item doesn't exist yet, a new Item with the `data` object as value + * is created and assigned to the Collection. + * + * Returns a boolean indicating + * whether the `data` object was assigned/updated successfully. + * + * @internal + * @param data - Data object + * @param config - Configuration object + */ + public assignData( + data: DataType, + config: AssignDataConfigInterface = {} + ): boolean { + config = defineConfig(config, { + patch: false, + background: false, + }); + const _data = copy(data); // Copy data object to get rid of reference + const primaryKey = this.config.primaryKey; + + if (!isValidObject(_data)) { + LogCodeManager.log('1B:03:05', [this._key]); + return false; + } + + // Check if data object contains valid itemKey, + // otherwise add random itemKey to Item + if (!Object.prototype.hasOwnProperty.call(_data, primaryKey)) { + LogCodeManager.log('1B:02:05', [this._key, primaryKey]); + _data[primaryKey] = generateId(); + } + + const itemKey = _data[primaryKey]; + const item = this.getItem(itemKey, { notExisting: true }); + const wasPlaceholder = item?.isPlaceholder || false; + + // Create new Item or update existing Item + if (item != null) { + if (config.patch) { + item.patch(_data, { background: config.background }); + } else { + item.set(_data, { background: config.background }); + } + } else { + this.assignItem(new Item(this, _data), { + background: config.background, + }); + } + + // Increase size of Collection if Item was previously a placeholder + // (-> hasn't officially existed in Collection before) + if (wasPlaceholder) this.size++; + + return true; + } + + /** + * Assigns the specified Item to the Collection + * at the key/name identifier of the Item. + * + * And returns a boolean indicating + * whether the Item was assigned successfully. + * + * @internal + * @param item - Item to be added. + * @param config - Configuration object + */ + public assignItem( + item: Item, + config: AssignItemConfigInterface = {} + ): boolean { + config = defineConfig(config, { + overwrite: false, + background: false, + }); + const primaryKey = this.config.primaryKey; + let itemKey = item._value[primaryKey]; + let increaseCollectionSize = true; + + // Check if Item has valid itemKey, + // otherwise add random itemKey to Item + if (!Object.prototype.hasOwnProperty.call(item._value, primaryKey)) { + LogCodeManager.log('1B:02:05', [this._key, primaryKey]); + itemKey = generateId(); + item.patch( + { [this.config.primaryKey]: itemKey }, + { background: config.background } + ); + item._key = itemKey; + } + + // Check if Item belongs to this Collection + if (item.collection() !== this) { + LogCodeManager.log('1B:03:06', [this._key, item.collection()._key]); + return false; + } + + // Check if Item already exists + if (this.getItem(itemKey, { notExisting: true }) != null) { + if (!config.overwrite) { + this.assignData(item._value); + return true; + } else increaseCollectionSize = false; + } + + // Assign/add Item to Collection + this.data[itemKey] = item; + + // Rebuild Groups that include itemKey + // after adding Item with itemKey to the Collection + // (because otherwise it can't find the Item as it isn't added yet) + this.rebuildGroupsThatIncludeItemKey(itemKey, { + background: config.background, + }); + + if (increaseCollectionSize) this.size++; + + return true; + } + + /** + * Rebuilds all Groups that contain the specified `itemKey`. + * + * @internal + * @itemKey - `itemKey` Groups must contain to be rebuilt. + * @config - Configuration object + */ + public rebuildGroupsThatIncludeItemKey( + itemKey: ItemKey, + config: RebuildGroupsThatIncludeItemKeyConfigInterface = {} + ): void { + config = defineConfig(config, { + background: false, + sideEffects: { + enabled: true, + exclude: [], + }, + }); + + // Rebuild Groups that include itemKey + for (const groupKey in this.groups) { + const group = this.getGroup(groupKey); + if (group?.has(itemKey)) { + // Not necessary because a sideEffect of ingesting the Group + // into the runtime is to rebuilt itself + // group.rebuild(); + + group?.rebuild({ + background: config?.background, + sideEffects: config?.sideEffects, + storage: false, + }); + } + } + } +} + +export type DefaultItem = Record; // same as { [key: string]: any }; +export type CollectionKey = string | number; +export type ItemKey = string | number; + +export interface CreateCollectionConfigInterface { + /** + * Initial Groups of the Collection. + * @default [] + */ + groups?: { [key: string]: Group } | string[]; + /** + * Initial Selectors of the Collection + * @default [] + */ + selectors?: { [key: string]: Selector } | string[]; + /** + * Key/Name identifier of the Collection. + * @default undefined + */ + key?: CollectionKey; + /** + * Key/Name of the property + * which represents the unique Item identifier + * in collected data objects. + * @default 'id' + */ + primaryKey?: string; + /** + * Key/Name identifier of the default Group that is created shortly after instantiation. + * The default Group represents the default pattern of the Collection. + * @default 'default' + */ + defaultGroupKey?: GroupKey; + /** + * Initial data objects of the Collection. + * @default [] + */ + initialData?: Array; +} + +export type CollectionConfig = + | CreateCollectionConfigInterface + | (( + collection: Collection + ) => CreateCollectionConfigInterface); + +export interface CollectionConfigInterface { + /** + * Key/Name of the property + * which represents the unique Item identifier + * in collected data objects. + * @default 'id' + */ + primaryKey: string; + /** + * Key/Name identifier of the default Group that is created shortly after instantiation. + * The default Group represents the default pattern of the Collection. + * @default 'default' + */ + defaultGroupKey: ItemKey; +} + +export interface CollectConfigInterface + extends AssignDataConfigInterface { + /** + * In which way the collected data should be added to the Collection. + * - 'push' = at the end + * - 'unshift' = at the beginning + * https://www.tutorialspoint.com/what-are-the-differences-between-unshift-and-push-methods-in-javascript + * @default 'push' + */ + method?: 'push' | 'unshift'; + /** + * Performs the specified action for each collected data object. + * @default undefined + */ + forEachItem?: ( + data: DataType | Item, + key: ItemKey, + success: boolean, + index: number + ) => void; + /** + * Whether to create a Selector for each collected data object. + * @default false + */ + select?: boolean; +} + +export interface UpdateConfigInterface { + /** + * Whether to merge the data object with changes into the existing Item data object + * or overwrite the existing Item data object entirely. + * @default true + */ + patch?: boolean | PatchOptionConfigInterface; + /** + * Whether to update the data object in background. + * So that the UI isn't notified of these changes and thus doesn't rerender. + * @default false + */ + background?: boolean; +} + +export interface UpdateItemKeyConfigInterface { + /** + * Whether to update the Item key/name identifier in background + * So that the UI isn't notified of these changes and thus doesn't rerender. + * @default false + */ + background?: boolean; +} + +export interface RebuildGroupsThatIncludeItemKeyConfigInterface { + /** + * Whether to rebuilt the Group in background. + * So that the UI isn't notified of these changes and thus doesn't rerender. + * @default false + */ + background?: boolean; + /** + * Whether to execute the defined side effects. + * @default true + */ + sideEffects?: SideEffectConfigInterface; +} + +export interface HasConfigInterface { + /** + * Whether Items that do not officially exist, + * such as placeholder Items, can be found + * @default true + */ + notExisting?: boolean; +} + +export interface CollectionPersistentConfigInterface { + /** + * Whether the Persistent should automatically load + * the persisted value into the Collection after its instantiation. + * @default true + */ + loadValue?: boolean; + /** + * Key/Name identifier of Storages + * in which the Collection value should be or is persisted. + * @default [`defaultStorageKey`] + */ + storageKeys?: StorageKey[]; + /** + * Key/Name identifier of the default Storage of the specified Storage keys. + * + * The Collection 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 interface RemoveItemsConfigInterface { + /** + * Whether to remove not officially existing Items (such as placeholder Items). + * Keep in mind that sometimes it won't remove an Item entirely + * as another Instance (like a Selector) might need to keep reference to it. + * https://github.com/agile-ts/agile/pull/152 + * @default false + */ + notExisting?: boolean; + /** + * Whether to remove Selectors that have selected an Item to be removed. + * @default false + */ + removeSelector?: boolean; +} + +export interface AssignDataConfigInterface { + /** + * When the Item identifier of the to assign data object already exists in the Collection, + * whether to merge the newly assigned data into the existing one + * or overwrite the existing one entirely. + * @default true + */ + patch?: boolean; + /** + * Whether to assign the data object to the Collection in background. + * So that the UI isn't notified of these changes and thus doesn't rerender. + * @default false + */ + background?: boolean; +} + +export interface AssignItemConfigInterface { + /** + * If an Item with the Item identifier already exists, + * whether to overwrite it entirely with the new one. + * @default false + */ + overwrite?: boolean; + /** + * Whether to assign the Item to the Collection in background. + * So that the UI isn't notified of these changes and thus doesn't rerender. + * @default false + */ + background?: boolean; +} diff --git a/packages/core/src/collection/index.ts b/packages/core/src/collection/index.ts index 31b8a107..b386d611 100644 --- a/packages/core/src/collection/index.ts +++ b/packages/core/src/collection/index.ts @@ -1,1697 +1,40 @@ import { + Collection, + CollectionConfig, + DefaultItem, Agile, - Item, - Group, - GroupKey, - Selector, - SelectorKey, - StorageKey, - GroupConfigInterface, - isValidObject, - normalizeArray, - copy, - CollectionPersistent, - GroupAddConfigInterface, - ComputedTracker, - generateId, - SideEffectConfigInterface, - SelectorConfigInterface, - removeProperties, - isFunction, - LogCodeManager, - PatchOptionConfigInterface, - defineConfig, + shared, } from '../internal'; -export class Collection< - DataType extends Object = DefaultItem, - GroupValueType = Array // To extract the Group Type Value in Integration methods like 'useAgile()' -> { - // Agile Instance the Collection belongs to - public agileInstance: () => Agile; - - public config: CollectionConfigInterface; - private initialConfig: CreateCollectionConfigInterface; - - // Key/Name identifier of the Collection - public _key?: CollectionKey; - // Amount of the Items stored in the Collection - public size = 0; - // Items stored in the Collection - public data: { [key: string]: Item } = {}; - // Whether the Collection is persisted in an external Storage - public isPersisted = false; - // Manages the permanent persistent in external Storages - public persistent: CollectionPersistent | undefined; - - // Registered Groups of Collection - public groups: { [key: string]: Group } = {}; - // Registered Selectors of Collection - public selectors: { [key: string]: Selector } = {}; - - // Whether the Collection was instantiated correctly - public isInstantiated = false; - - // Helper property to check whether an unknown instance is a Collection, - // without importing the Collection itself for using 'instanceof' (Treeshaking support) - public isCollection = true; - - /** - * A Collection manages a reactive set of Information - * that we need to remember globally at a later point in time. - * While providing a toolkit to use and mutate this set of Information. - * - * It is designed for arrays of data objects following the same pattern. - * - * Each of these data object must have a unique `primaryKey` to be correctly identified later. - * - * You can create as many global Collections as you need. - * - * [Learn more..](https://agile-ts.org/docs/core/collection/) - * - * @public - * @param agileInstance - Instance of Agile the Collection belongs to. - * @param config - Configuration object - */ - constructor(agileInstance: Agile, config: CollectionConfig = {}) { - this.agileInstance = () => agileInstance; - let _config = typeof config === 'function' ? config(this) : config; - _config = defineConfig(_config, { - primaryKey: 'id', - groups: {}, - selectors: {}, - defaultGroupKey: 'default', - }); - this._key = _config.key; - this.config = { - defaultGroupKey: _config.defaultGroupKey as any, - primaryKey: _config.primaryKey as any, - }; - this.initialConfig = _config; - - this.initGroups(_config.groups as any); - this.initSelectors(_config.selectors as any); - - this.isInstantiated = true; - - // Add 'initialData' to Collection - // (after 'isInstantiated' to add them properly to the Collection) - if (_config.initialData) this.collect(_config.initialData); - - // Reselect Selector Items - // Necessary because the selection of an Item - // hasn't worked with a not correctly 'instantiated' Collection before - for (const key in this.selectors) this.selectors[key].reselect(); - - // Rebuild of Groups - // Not necessary because if Items are added to the Collection, - // (after 'isInstantiated = true') - // the Groups which contain these added Items are rebuilt. - // for (const key in this.groups) this.groups[key].rebuild(); - } - - /** - * Updates the key/name identifier of the Collection. - * - * [Learn more..](https://agile-ts.org/docs/core/collection/properties#key) - * - * @public - * @param value - New key/name identifier. - */ - public set key(value: CollectionKey | undefined) { - this.setKey(value); - } - - /** - * Returns the key/name identifier of the Collection. - * - * [Learn more..](https://agile-ts.org/docs/core/collection/properties#key) - * - * @public - */ - public get key(): CollectionKey | undefined { - return this._key; - } - - /** - * Updates the key/name identifier of the Collection. - * - * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#setkey) - * - * @public - * @param value - New key/name identifier. - */ - public setKey(value: CollectionKey | undefined) { - const oldKey = this._key; - - // Update Collection key - this._key = value; - - // Update key in Persistent (only if oldKey is equal to persistentKey - // because otherwise the persistentKey is detached from the Collection key - // -> not managed by Collection anymore) - if (value != null && this.persistent?._key === oldKey) - this.persistent?.setKey(value); - - return this; - } - - /** - * Creates a new Group without associating it to the Collection. - * - * This way of creating a Group is intended for use in the Collection configuration object, - * where the `constructor()` takes care of the binding. - * - * After a successful initiation of the Collection we recommend using `createGroup()`, - * because it automatically connects the Group to the Collection. - * - * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#group) - * - * @public - * @param initialItems - Key/Name identifiers of the Items to be clustered by the Group. - * @param config - Configuration object - */ - public Group( - initialItems?: Array, - config: GroupConfigInterface = {} - ): Group { - if (this.isInstantiated) { - const key = config.key ?? generateId(); - LogCodeManager.log('1B:02:00'); - return this.createGroup(key, initialItems); - } - - return new Group(this, initialItems, config); - } - - /** - * Creates a new Selector without associating it to the Collection. - * - * This way of creating a Selector is intended for use in the Collection configuration object, - * where the `constructor()` takes care of the binding. - * - * After a successful initiation of the Collection we recommend using `createSelector()`, - * because it automatically connects the Group to the Collection. - * - * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#selector) - * - * @public - * @param initialKey - Key/Name identifier of the Item to be represented by the Selector. - * @param config - Configuration object - */ - public Selector( - initialKey: ItemKey | null, - config: SelectorConfigInterface = {} - ): Selector { - if (this.isInstantiated) { - const key = config.key ?? generateId(); - LogCodeManager.log('1B:02:01'); - return this.createSelector(key, initialKey); - } - - return new Selector(this, initialKey, config); - } - - /** - * Sets up the specified Groups or Group keys - * and assigns them to the Collection if they are valid. - * - * It also instantiates and assigns the default Group to the Collection. - * The default Group reflects the default pattern of the Collection. - * - * @internal - * @param groups - Entire Groups or Group keys to be set up. - */ - public initGroups(groups: { [key: string]: Group } | string[]): void { - if (!groups) return; - let groupsObject: { [key: string]: Group } = {}; - - // If groups is Array of Group keys/names, create the Groups based on these keys - if (Array.isArray(groups)) { - groups.forEach((groupKey) => { - groupsObject[groupKey] = new Group(this, [], { - key: groupKey, - }); - }); - } else groupsObject = groups; - - // Add default Group - groupsObject[this.config.defaultGroupKey] = new Group(this, [], { - key: this.config.defaultGroupKey, - }); - - // Assign missing key/name to Group based on the property key - for (const key in groupsObject) - if (groupsObject[key]._key == null) groupsObject[key].setKey(key); - - this.groups = groupsObject; - } - - /** - * Sets up the specified Selectors or Selector keys - * and assigns them to the Collection if they are valid. - * - * @internal - * @param selectors - Entire Selectors or Selector keys to be set up. - */ - public initSelectors(selectors: { [key: string]: Selector } | string[]) { - if (!selectors) return; - let selectorsObject: { [key: string]: Selector } = {}; - - // If selectors is Array of Selector keys/names, create the Selectors based on these keys - if (Array.isArray(selectors)) { - selectors.forEach((selectorKey) => { - selectorsObject[selectorKey] = new Selector( - this, - selectorKey, - { - key: selectorKey, - } - ); - }); - } else selectorsObject = selectors; - - // Assign missing key/name to Selector based on the property key - for (const key in selectorsObject) - if (selectorsObject[key]._key == null) selectorsObject[key].setKey(key); - - this.selectors = selectorsObject; - } - - /** - * Appends new data objects following the same pattern to the end of the Collection. - * - * Each collected `data object` requires a unique identifier at the primaryKey property (by default 'id') - * to be correctly identified later. - * - * For example, if we collect some kind of user object, - * it must contain such unique identifier at 'id' - * to be added to the Collection. - * ``` - * MY_COLLECTION.collect({id: '1', name: 'jeff'}); // valid - * MY_COLLECTION.collect({name: 'frank'}); // invalid - * ``` - * - * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#collect) - * - * @public - * @param data - Data objects or entire Items to be added. - * @param groupKeys - Group/s to which the specified data objects or Items are to be added. - * @param config - Configuration object - */ - public collect( - data: DataType | Item | Array>, - groupKeys?: GroupKey | Array, - config: CollectConfigInterface = {} - ): this { - const _data = normalizeArray>(data); - const _groupKeys = normalizeArray(groupKeys); - const defaultGroupKey = this.config.defaultGroupKey; - const primaryKey = this.config.primaryKey; - config = defineConfig(config, { - method: 'push', - background: false, - patch: false, - select: false, - }); - - // Add default groupKey, since all Items are added to the default Group - if (!_groupKeys.includes(defaultGroupKey)) _groupKeys.push(defaultGroupKey); - - // Create not existing Groups - _groupKeys.forEach( - (key) => this.groups[key] == null && this.createGroup(key) - ); - - _data.forEach((data, index) => { - let itemKey; - let success = false; - - // Assign Data or Item to Collection - if (data instanceof Item) { - success = this.assignItem(data, { - background: config.background, - }); - itemKey = data._key; - } else { - success = this.assignData(data, { - patch: config.patch, - background: config.background, - }); - itemKey = data[primaryKey]; - } - - // Add itemKey to provided Groups and create corresponding Selector - if (success) { - _groupKeys.forEach((groupKey) => { - this.getGroup(groupKey)?.add(itemKey, { - method: config.method, - background: config.background, - }); - }); - - if (config.select) this.createSelector(itemKey, itemKey); - } - - if (config.forEachItem) config.forEachItem(data, itemKey, success, index); - }); - - return this; - } - - /** - * Updates the Item `data object` with the specified `object with changes`, if the Item exists. - * By default the `object with changes` is merged into the Item `data object` at top level. - * - * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#update) - * - * @public - * @param itemKey - Key/Name identifier of the Item to be updated. - * @param changes - Object with changes to be merged into the Item data object. - * @param config - Configuration object - */ - public update( - itemKey: ItemKey, - changes: DefaultItem | DataType, - config: UpdateConfigInterface = {} - ): Item | undefined { - const item = this.getItem(itemKey, { notExisting: true }); - const primaryKey = this.config.primaryKey; - config = defineConfig(config, { - patch: true, - background: false, - }); - - // Check if the given conditions are suitable for a update action - if (item == null) { - LogCodeManager.log('1B:03:00', [itemKey, this._key]); - return undefined; - } - if (!isValidObject(changes)) { - LogCodeManager.log('1B:03:01', [itemKey, this._key]); - return undefined; - } - - const oldItemKey = item._value[primaryKey]; - const newItemKey = changes[primaryKey] || oldItemKey; - - // Update itemKey if the new itemKey differs from the old one - if (oldItemKey !== newItemKey) - this.updateItemKey(oldItemKey, newItemKey, { - background: config.background, - }); - - // Patch changes into Item data object - if (config.patch) { - // Delete primaryKey property from 'changes object' because if it has changed, - // it is correctly updated in the above called 'updateItemKey()' method - if (changes[primaryKey]) delete changes[primaryKey]; - - let patchConfig: { addNewProperties?: boolean } = - typeof config.patch === 'object' ? config.patch : {}; - patchConfig = { - addNewProperties: true, - ...patchConfig, - }; - - item.patch(changes as any, { - background: config.background, - addNewProperties: patchConfig.addNewProperties, - }); - } - // Apply changes to Item data object - else { - // Ensure that the current Item identifier isn't different from the 'changes object' itemKey - if (changes[this.config.primaryKey] !== itemKey) { - changes[this.config.primaryKey] = itemKey; - LogCodeManager.log('1B:02:02', [], changes); - } - - item.set(changes as any, { - background: config.background, - }); - } - - return item; - } - - /** - * Creates a new Group and associates it to the Collection. - * - * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#createGroup) - * - * @public - * @param groupKey - Unique identifier of the Group to be created. - * @param initialItems - Key/Name identifiers of the Items to be clustered by the Group. - */ - public createGroup( - groupKey: GroupKey, - initialItems: Array = [] - ): Group { - let group = this.getGroup(groupKey, { notExisting: true }); - if (!this.isInstantiated) LogCodeManager.log('1B:02:03'); - - // Check if Group already exists - if (group != null) { - if (!group.isPlaceholder) { - LogCodeManager.log('1B:03:02', [groupKey]); - return group; - } - group.set(initialItems, { overwrite: true }); - return group; - } - - // Create new Group - group = new Group(this, initialItems, { key: groupKey }); - this.groups[groupKey] = group; - - return group; - } - - /** - * Returns a boolean indicating whether a Group with the specified `groupKey` - * exists in the Collection or not. - * - * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#hasgroup) - * - * @public - * @param groupKey - Key/Name identifier of the Group to be checked for existence. - * @param config - Configuration object - */ - public hasGroup( - groupKey: GroupKey | undefined, - config: HasConfigInterface = {} - ): boolean { - return !!this.getGroup(groupKey, config); - } - - /** - * Retrieves a single Group with the specified key/name identifier from the Collection. - * - * If the to retrieve Group doesn't exist, `undefined` is returned. - * - * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getgroup) - * - * @public - * @param groupKey - Key/Name identifier of the Group. - * @param config - Configuration object - */ - public getGroup( - groupKey: GroupKey | undefined | null, - config: HasConfigInterface = {} - ): Group | undefined { - config = defineConfig(config, { - notExisting: false, - }); - - // Retrieve Group - const group = groupKey ? this.groups[groupKey] : undefined; - - // Check if retrieved Group exists - if (group == null || (!config.notExisting && !group.exists)) - return undefined; - - ComputedTracker.tracked(group.observers['value']); - return group; - } - - /** - * Retrieves the default Group from the Collection. - * - * Every Collection should have a default Group, - * which represents the default pattern of the Collection. - * - * If the default Group, for what ever reason, doesn't exist, `undefined` is returned. - * - * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getdefaultgroup) - * - * @public - */ - public getDefaultGroup(): Group | undefined { - return this.getGroup(this.config.defaultGroupKey); - } - - /** - * Retrieves a single Group with the specified key/name identifier from the Collection. - * - * If the to retrieve Group doesn't exist, a reference Group is returned. - * This has the advantage that Components that have the reference Group bound to themselves - * are rerenderd when the original Group is created. - * - * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getgroupwithreference) - * - * @public - * @param groupKey - Key/Name identifier of the Group. - */ - public getGroupWithReference(groupKey: GroupKey): Group { - let group = this.getGroup(groupKey, { notExisting: true }); - - // Create dummy Group to hold reference - if (group == null) { - group = new Group(this, [], { - key: groupKey, - isPlaceholder: true, - }); - this.groups[groupKey] = group; - } - - ComputedTracker.tracked(group.observers['value']); - return group; - } - - /** - * Removes a Group with the specified key/name identifier from the Collection, - * if it exists in the Collection. - * - * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#removegroup) - * - * @public - * @param groupKey - Key/Name identifier of the Group to be removed. - */ - public removeGroup(groupKey: GroupKey): this { - if (this.groups[groupKey] != null) delete this.groups[groupKey]; - return this; - } - - /** - * Returns the count of registered Groups in the Collection. - * - * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getgroupcount) - * - * @public - */ - public getGroupCount(): number { - let size = 0; - Object.keys(this.groups).map(() => size++); - return size; - } - - /** - * Creates a new Selector and associates it to the Collection. - * - * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#createSelector) - * - * @public - * @param selectorKey - Unique identifier of the Selector to be created. - * @param itemKey - Key/Name identifier of the Item to be represented by the Selector. - */ - public createSelector( - selectorKey: SelectorKey, - itemKey: ItemKey | null - ): Selector { - let selector = this.getSelector(selectorKey, { notExisting: true }); - if (!this.isInstantiated) LogCodeManager.log('1B:02:04'); - - // Check if Selector already exists - if (selector != null) { - if (!selector.isPlaceholder) { - LogCodeManager.log('1B:03:03', [selectorKey]); - return selector; - } - selector.select(itemKey, { overwrite: true }); - return selector; - } - - // Create new Selector - selector = new Selector(this, itemKey, { - key: selectorKey, - }); - this.selectors[selectorKey] = selector; - - return selector; - } - - /** - * Creates a new Selector and associates it to the Collection. - * - * The specified `itemKey` is used as the unique identifier key of the new Selector. - * ``` - * MY_COLLECTION.select('1'); - * // is equivalent to - * MY_COLLECTION.createSelector('1', '1'); - * ``` - * - * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#select) - * - * @public - * @param itemKey - Key/Name identifier of the Item to be represented by the Selector - * and used as unique identifier of the Selector. - */ - public select(itemKey: ItemKey): Selector { - return this.createSelector(itemKey, itemKey); - } - - /** - * Returns a boolean indicating whether a Selector with the specified `selectorKey` - * exists in the Collection or not. - * - * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#hasselector) - * - * @public - * @param selectorKey - Key/Name identifier of the Selector to be checked for existence. - * @param config - Configuration object - */ - public hasSelector( - selectorKey: SelectorKey | undefined, - config: HasConfigInterface = {} - ): boolean { - return !!this.getSelector(selectorKey, config); - } - - /** - * Retrieves a single Selector with the specified key/name identifier from the Collection. - * - * If the to retrieve Selector doesn't exist, `undefined` is returned. - * - * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getselector) - * - * @public - * @param selectorKey - Key/Name identifier of the Selector. - * @param config - Configuration object - */ - public getSelector( - selectorKey: SelectorKey | undefined | null, - config: HasConfigInterface = {} - ): Selector | undefined { - config = defineConfig(config, { - notExisting: false, - }); - - // Get Selector - const selector = selectorKey ? this.selectors[selectorKey] : undefined; - - // Check if Selector exists - if (selector == null || (!config.notExisting && !selector.exists)) - return undefined; - - ComputedTracker.tracked(selector.observers['value']); - return selector; - } - - /** - * Retrieves a single Selector with the specified key/name identifier from the Collection. - * - * If the to retrieve Selector doesn't exist, a reference Selector is returned. - * This has the advantage that Components that have the reference Selector bound to themselves - * are rerenderd when the original Selector is created. - * - * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getselectorwithreference) - * - * @public - * @param selectorKey - Key/Name identifier of the Selector. - */ - public getSelectorWithReference( - selectorKey: SelectorKey - ): Selector { - let selector = this.getSelector(selectorKey, { notExisting: true }); - - // Create dummy Selector to hold reference - if (selector == null) { - selector = new Selector(this, null, { - key: selectorKey, - isPlaceholder: true, - }); - this.selectors[selectorKey] = selector; - } - - ComputedTracker.tracked(selector.observers['value']); - return selector; - } - - /** - * Removes a Selector with the specified key/name identifier from the Collection, - * if it exists in the Collection. - * - * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#removeselector) - * - * @public - * @param selectorKey - Key/Name identifier of the Selector to be removed. - */ - public removeSelector(selectorKey: SelectorKey): this { - if (this.selectors[selectorKey] != null) { - this.selectors[selectorKey].unselect(); - delete this.selectors[selectorKey]; - } - return this; - } - - /** - * Returns the count of registered Selectors in the Collection. - * - * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getselectorcount) - * - * @public - */ - public getSelectorCount(): number { - let size = 0; - Object.keys(this.selectors).map(() => size++); - return size; - } - - /** - * Returns a boolean indicating whether a Item with the specified `itemKey` - * exists in the Collection or not. - * - * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#hasitem) - * - * @public - * @param itemKey - Key/Name identifier of the Item. - * @param config - Configuration object - */ - public hasItem( - itemKey: ItemKey | undefined, - config: HasConfigInterface = {} - ): boolean { - return !!this.getItem(itemKey, config); - } - - /** - * Retrieves a single Item with the specified key/name identifier from the Collection. - * - * If the to retrieve Item doesn't exist, `undefined` is returned. - * - * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getitem) - * - * @public - * @param itemKey - Key/Name identifier of the Item. - * @param config - Configuration object - */ - public getItem( - itemKey: ItemKey | undefined | null, - config: HasConfigInterface = {} - ): Item | undefined { - config = defineConfig(config, { - notExisting: false, - }); - - // Get Item - const item = itemKey != null ? this.data[itemKey] : undefined; - - // Check if Item exists - if (item == null || (!config.notExisting && !item.exists)) return undefined; - - ComputedTracker.tracked(item.observers['value']); - return item; - } - - /** - * Retrieves a single Item with the specified key/name identifier from the Collection. - * - * If the to retrieve Item doesn't exist, a reference Item is returned. - * This has the advantage that Components that have the reference Item bound to themselves - * are rerenderd when the original Item is created. - * - * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getitemwithreference) - * - * @public - * @param itemKey - Key/Name identifier of the Item. - */ - public getItemWithReference(itemKey: ItemKey): Item { - let item = this.getItem(itemKey, { notExisting: true }); - - // Create dummy Item to hold reference - if (item == null) item = this.createPlaceholderItem(itemKey, true); - - ComputedTracker.tracked(item.observers['value']); - return item; - } - - /** - * Creates a placeholder Item - * that can be used to hold a reference to a not existing Item. - * - * @internal - * @param itemKey - Unique identifier of the to create placeholder Item. - * @param addToCollection - Whether to add the Item to be created to the Collection. - */ - public createPlaceholderItem( - itemKey: ItemKey, - addToCollection = false - ): Item { - // Create placeholder Item - const item = new Item( - this, - { - [this.config.primaryKey]: itemKey, // Setting primaryKey of the Item to passed itemKey - dummy: 'item', - } as any, - { isPlaceholder: true } - ); - - // Add placeholder Item to Collection - if ( - addToCollection && - !Object.prototype.hasOwnProperty.call(this.data, itemKey) - ) - this.data[itemKey] = item; - - ComputedTracker.tracked(item.observers['value']); - return item; - } - - /** - * Retrieves the value (data object) of a single Item - * with the specified key/name identifier from the Collection. - * - * If the to retrieve Item containing the value doesn't exist, `undefined` is returned. - * - * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getitemvalue) - * - * @public - * @param itemKey - Key/Name identifier of the Item. - * @param config - Configuration object - */ - public getItemValue( - itemKey: ItemKey | undefined, - config: HasConfigInterface = {} - ): DataType | undefined { - const item = this.getItem(itemKey, config); - if (item == null) return undefined; - return item.value; - } - - /** - * Retrieves all Items from the Collection. - * ``` - * MY_COLLECTION.getAllItems(); - * // is equivalent to - * MY_COLLECTION.getDefaultGroup().items; - * ``` - * - * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getallitems) - * - * @public - * @param config - Configuration object - */ - public getAllItems(config: HasConfigInterface = {}): Array> { - config = defineConfig(config, { - notExisting: false, - }); - - const defaultGroup = this.getDefaultGroup(); - let items: Array> = []; - - // If config.notExisting transform the data object into array since it contains all Items, - // otherwise return the default Group Items - if (config.notExisting) { - for (const key in this.data) items.push(this.data[key]); - } else { - // Why default Group Items and not all '.exists === true' Items? - // Because the default Group keeps track of all existing Items. - // It also does control the Collection output in binding methods like 'useAgile()' - // and therefore should do it here too. - items = defaultGroup?.getItems() || []; - } - - return items; - } - - /** - * Retrieves the values (data objects) of all Items from the Collection. - * ``` - * MY_COLLECTION.getAllItemValues(); - * // is equivalent to - * MY_COLLECTION.getDefaultGroup().output; - * ``` - * - * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getallitemvalues) - * - * @public - * @param config - Configuration object - */ - public getAllItemValues(config: HasConfigInterface = {}): Array { - const items = this.getAllItems(config); - return items.map((item) => item.value); - } - - /** - * Preserves the Collection `value` in the corresponding external Storage. - * - * The Collection key/name is used as the unique identifier for the Persistent. - * If that is not desired or the Collection has no unique identifier, - * please specify a separate unique identifier for the Persistent. - * - * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#persist) - * - * @public - * @param config - Configuration object - */ - public persist(config?: CollectionPersistentConfigInterface): this; - /** - * Preserves the Collection `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/collection/methods/#persist) - * - * @public - * @param key - Key/Name identifier of Persistent. - * @param config - Configuration object - */ - public persist( - key?: StorageKey, - config?: CollectionPersistentConfigInterface - ): this; - public persist( - keyOrConfig: StorageKey | CollectionPersistentConfigInterface = {}, - config: CollectionPersistentConfigInterface = {} - ): this { - let _config: CollectionPersistentConfigInterface; - let key: StorageKey | undefined; - - if (isValidObject(keyOrConfig)) { - _config = keyOrConfig as CollectionPersistentConfigInterface; - key = this._key; - } else { - _config = config || {}; - key = keyOrConfig as StorageKey; - } - - _config = defineConfig(_config, { - loadValue: true, - storageKeys: [], - defaultStorageKey: null as any, - }); - - // Check if Collection is already persisted - if (this.persistent != null && this.isPersisted) return this; - - // Create Persistent (-> persist value) - this.persistent = new CollectionPersistent(this, { - instantiate: _config.loadValue, - storageKeys: _config.storageKeys, - key: key, - defaultStorageKey: _config.defaultStorageKey, - }); - - return this; - } - - /** - * Fires immediately after the persisted `value` - * is loaded into the Collection from a corresponding external Storage. - * - * Registering such callback function makes only sense - * when the Collection is [persisted](https://agile-ts.org/docs/core/collection/methods/#persist) in an external Storage. - * - * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#onload) - * - * @public - * @param callback - A function to be executed after the externally persisted `value` was loaded into the Collection. - */ - 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 Collection is already persisted ('isPersisted') fire specified callback immediately - if (this.isPersisted) callback(true); - - return this; - } - - /** - * Removes all Items from the Collection - * and resets all Groups and Selectors of the Collection. - * - * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#reset) - * - * @public - */ - public reset(): this { - // Reset data - this.data = {}; - this.size = 0; - - // Reset Groups - for (const key in this.groups) this.getGroup(key)?.reset(); - - // Reset Selectors - for (const key in this.selectors) this.getSelector(key)?.reset(); - - return this; - } - - /** - * Puts `itemKeys/s` into Group/s. - * - * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#put) - * - * @public - * @param itemKeys - `itemKey/s` to be put into the specified Group/s. - * @param groupKeys - Key/Name Identifier/s of the Group/s the specified `itemKey/s` are to put in. - * @param config - Configuration object - */ - public put( - itemKeys: ItemKey | Array, - groupKeys: GroupKey | Array, - config: GroupAddConfigInterface = {} - ): this { - const _itemKeys = normalizeArray(itemKeys); - const _groupKeys = normalizeArray(groupKeys); - - // Assign itemKeys to Groups - _groupKeys.forEach((groupKey) => { - this.getGroup(groupKey)?.add(_itemKeys, config); - }); - - return this; - } - - /** - * Moves specified `itemKey/s` from one Group to another Group. - * - * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#move) - * - * @public - * @param itemKeys - `itemKey/s` to be moved. - * @param oldGroupKey - Key/Name Identifier of the Group the `itemKey/s` are moved from. - * @param newGroupKey - Key/Name Identifier of the Group the `itemKey/s` are moved in. - * @param config - Configuration object - */ - public move( - itemKeys: ItemKey | Array, - oldGroupKey: GroupKey, - newGroupKey: GroupKey, - config: GroupAddConfigInterface = {} - ): this { - const _itemKeys = normalizeArray(itemKeys); - - // Remove itemKeys from old Group - this.getGroup(oldGroupKey)?.remove( - _itemKeys, - removeProperties(config, ['method', 'overwrite']) - ); - - // Assign itemKeys to new Group - this.getGroup(newGroupKey)?.add(_itemKeys, config); - - return this; - } - - /** - * Updates the key/name identifier of the Item - * and returns a boolean indicating - * whether the Item identifier was updated successfully. - * - * @internal - * @param oldItemKey - Old key/name Item identifier. - * @param newItemKey - New key/name Item identifier. - * @param config - Configuration object - */ - public updateItemKey( - oldItemKey: ItemKey, - newItemKey: ItemKey, - config: UpdateItemKeyConfigInterface = {} - ): boolean { - const item = this.getItem(oldItemKey, { notExisting: true }); - config = defineConfig(config, { - background: false, - }); - - if (item == null || oldItemKey === newItemKey) return false; - - // Check if Item with newItemKey already exists - if (this.hasItem(newItemKey)) { - LogCodeManager.log('1B:03:04', [oldItemKey, newItemKey, this._key]); - return false; - } - - // Update itemKey in data object - delete this.data[oldItemKey]; - this.data[newItemKey] = item; - - // Update key/name of the Item - item.setKey(newItemKey, { - background: config.background, - }); - - // Update Persistent key of the Item if it follows the Item Storage Key pattern - // and therefore differs from the actual Item key - // (-> isn't automatically updated when the Item key is updated) - if ( - item.persistent != null && - item.persistent._key === - CollectionPersistent.getItemStorageKey(oldItemKey, this._key) - ) - item.persistent?.setKey( - CollectionPersistent.getItemStorageKey(newItemKey, this._key) - ); - - // Update itemKey in Groups - for (const groupKey in this.groups) { - const group = this.getGroup(groupKey, { notExisting: true }); - if (group == null || !group.has(oldItemKey)) continue; - group.replace(oldItemKey, newItemKey, { background: config.background }); - } - - // Update itemKey in Selectors - for (const selectorKey in this.selectors) { - const selector = this.getSelector(selectorKey, { notExisting: true }); - if (selector == null) continue; - - // Reselect Item in Selector that has selected the newItemKey. - // Necessary because potential reference placeholder Item got overwritten - // with the new (renamed) Item - // -> has to find the new Item at selected itemKey - // since the placeholder Item got overwritten - if (selector.hasSelected(newItemKey, false)) { - selector.reselect({ - force: true, // Because itemKeys are the same (but not the Items at this itemKey anymore) - background: config.background, - }); - } - - // Select newItemKey in Selector that has selected the oldItemKey - if (selector.hasSelected(oldItemKey, false)) - selector.select(newItemKey, { - background: config.background, - }); - } - - return true; - } - - /** - * Returns all key/name identifiers of the Group/s containing the specified `itemKey`. - * - * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#getgroupkeysthathaveitemkey) - * - * @public - * @param itemKey - `itemKey` to be contained in Group/s. - */ - public getGroupKeysThatHaveItemKey(itemKey: ItemKey): Array { - const groupKeys: Array = []; - for (const groupKey in this.groups) { - const group = this.groups[groupKey]; - if (group?.has(itemKey)) groupKeys.push(groupKey); - } - return groupKeys; - } - - /** - * Removes Item/s from: - * - * - `.everywhere()`: - * Removes Item/s from the entire Collection and all its Groups and Selectors (i.e. from everywhere) - * ``` - * MY_COLLECTION.remove('1').everywhere(); - * // is equivalent to - * MY_COLLECTION.removeItems('1'); - * ``` - * - `.fromGroups()`: - * Removes Item/s only from specified Groups. - * ``` - * MY_COLLECTION.remove('1').fromGroups(['1', '2']); - * // is equivalent to - * MY_COLLECTION.removeFromGroups('1', ['1', '2']); - * ``` - * - * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#remove) - * - * @public - * @param itemKeys - Item/s with identifier/s to be removed. - */ - public remove( - itemKeys: ItemKey | Array - ): { - fromGroups: (groups: Array | ItemKey) => Collection; - everywhere: (config?: RemoveItemsConfigInterface) => Collection; - } { - return { - fromGroups: (groups: Array | ItemKey) => - this.removeFromGroups(itemKeys, groups), - everywhere: (config) => this.removeItems(itemKeys, config || {}), - }; - } - - /** - * Remove Item/s from specified Group/s. - * - * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#removefromgroups) - * - * @public - * @param itemKeys - Key/Name Identifier/s of the Item/s to be removed from the Group/s. - * @param groupKeys - Key/Name Identifier/s of the Group/s the Item/s are to remove from. - */ - public removeFromGroups( - itemKeys: ItemKey | Array, - groupKeys: GroupKey | Array - ): this { - const _itemKeys = normalizeArray(itemKeys); - const _groupKeys = normalizeArray(groupKeys); - - _itemKeys.forEach((itemKey) => { - let removedFromGroupsCount = 0; - - // Remove itemKey from the Groups - _groupKeys.forEach((groupKey) => { - const group = this.getGroup(groupKey, { notExisting: true }); - if (!group?.has(itemKey)) return; - group.remove(itemKey); - removedFromGroupsCount++; - }); - - // If the Item was removed from each Group representing the Item, - // remove it completely - if ( - removedFromGroupsCount >= - this.getGroupKeysThatHaveItemKey(itemKey).length - ) - this.removeItems(itemKey); - }); - - return this; - } - - /** - * Removes Item/s from the entire Collection and all the Collection's Groups and Selectors. - * - * [Learn more..](https://agile-ts.org/docs/core/collection/methods/#removeitems) - * - * @public - * @param itemKeys - Key/Name identifier/s of the Item/s to be removed from the entire Collection. - * @param config - Configuration object - */ - public removeItems( - itemKeys: ItemKey | Array, - config: RemoveItemsConfigInterface = {} - ): this { - config = defineConfig(config, { - notExisting: false, - removeSelector: false, - }); - const _itemKeys = normalizeArray(itemKeys); - - _itemKeys.forEach((itemKey) => { - const item = this.getItem(itemKey, { notExisting: config.notExisting }); - if (item == null) return; - const wasPlaceholder = item.isPlaceholder; - - // Remove Item from the Groups - for (const groupKey in this.groups) { - const group = this.getGroup(groupKey, { notExisting: true }); - if (group?.has(itemKey)) group?.remove(itemKey); - } - - // Remove Item from Storage - item.persistent?.removePersistedValue(); - - // Remove Item from Collection - delete this.data[itemKey]; - - // Reselect or remove Selectors which have represented the removed Item - for (const selectorKey in this.selectors) { - const selector = this.getSelector(selectorKey, { notExisting: true }); - if (selector != null && selector.hasSelected(itemKey, false)) { - if (config.removeSelector) { - // Remove Selector - this.removeSelector(selector._key ?? 'unknown'); - } else { - // Reselect Item in Selector - // in order to create a new dummyItem - // to hold a reference to the now not existing Item - selector.reselect({ force: true }); - } - } - } - - if (!wasPlaceholder) this.size--; - }); - - return this; - } - - /** - * Assigns the provided `data` object to an already existing Item - * with specified key/name identifier found in the `data` object. - * If the Item doesn't exist yet, a new Item with the `data` object as value - * is created and assigned to the Collection. - * - * Returns a boolean indicating - * whether the `data` object was assigned/updated successfully. - * - * @internal - * @param data - Data object - * @param config - Configuration object - */ - public assignData( - data: DataType, - config: AssignDataConfigInterface = {} - ): boolean { - config = defineConfig(config, { - patch: false, - background: false, - }); - const _data = copy(data); // Copy data object to get rid of reference - const primaryKey = this.config.primaryKey; - - if (!isValidObject(_data)) { - LogCodeManager.log('1B:03:05', [this._key]); - return false; - } - - // Check if data object contains valid itemKey, - // otherwise add random itemKey to Item - if (!Object.prototype.hasOwnProperty.call(_data, primaryKey)) { - LogCodeManager.log('1B:02:05', [this._key, primaryKey]); - _data[primaryKey] = generateId(); - } - - const itemKey = _data[primaryKey]; - const item = this.getItem(itemKey, { notExisting: true }); - const wasPlaceholder = item?.isPlaceholder || false; - - // Create new Item or update existing Item - if (item != null) { - if (config.patch) { - item.patch(_data, { background: config.background }); - } else { - item.set(_data, { background: config.background }); - } - } else { - this.assignItem(new Item(this, _data), { - background: config.background, - }); - } - - // Increase size of Collection if Item was previously a placeholder - // (-> hasn't officially existed in Collection before) - if (wasPlaceholder) this.size++; - - return true; - } - - /** - * Assigns the specified Item to the Collection - * at the key/name identifier of the Item. - * - * And returns a boolean indicating - * whether the Item was assigned successfully. - * - * @internal - * @param item - Item to be added. - * @param config - Configuration object - */ - public assignItem( - item: Item, - config: AssignItemConfigInterface = {} - ): boolean { - config = defineConfig(config, { - overwrite: false, - background: false, - }); - const primaryKey = this.config.primaryKey; - let itemKey = item._value[primaryKey]; - let increaseCollectionSize = true; - - // Check if Item has valid itemKey, - // otherwise add random itemKey to Item - if (!Object.prototype.hasOwnProperty.call(item._value, primaryKey)) { - LogCodeManager.log('1B:02:05', [this._key, primaryKey]); - itemKey = generateId(); - item.patch( - { [this.config.primaryKey]: itemKey }, - { background: config.background } - ); - item._key = itemKey; - } - - // Check if Item belongs to this Collection - if (item.collection() !== this) { - LogCodeManager.log('1B:03:06', [this._key, item.collection()._key]); - return false; - } - - // Check if Item already exists - if (this.getItem(itemKey, { notExisting: true }) != null) { - if (!config.overwrite) { - this.assignData(item._value); - return true; - } else increaseCollectionSize = false; - } - - // Assign/add Item to Collection - this.data[itemKey] = item; - - // Rebuild Groups that include itemKey - // after adding Item with itemKey to the Collection - // (because otherwise it can't find the Item as it isn't added yet) - this.rebuildGroupsThatIncludeItemKey(itemKey, { - background: config.background, - }); - - if (increaseCollectionSize) this.size++; - - return true; - } - - /** - * Rebuilds all Groups that contain the specified `itemKey`. - * - * @internal - * @itemKey - `itemKey` Groups must contain to be rebuilt. - * @config - Configuration object - */ - public rebuildGroupsThatIncludeItemKey( - itemKey: ItemKey, - config: RebuildGroupsThatIncludeItemKeyConfigInterface = {} - ): void { - config = defineConfig(config, { - background: false, - sideEffects: { - enabled: true, - exclude: [], - }, - }); - - // Rebuild Groups that include itemKey - for (const groupKey in this.groups) { - const group = this.getGroup(groupKey); - if (group?.has(itemKey)) { - // Not necessary because a sideEffect of ingesting the Group - // into the runtime is to rebuilt itself - // group.rebuild(); - - group?.rebuild({ - background: config?.background, - sideEffects: config?.sideEffects, - storage: false, - }); - } - } - } -} - -export type DefaultItem = Record; // same as { [key: string]: any }; -export type CollectionKey = string | number; -export type ItemKey = string | number; - -export interface CreateCollectionConfigInterface { - /** - * Initial Groups of the Collection. - * @default [] - */ - groups?: { [key: string]: Group } | string[]; - /** - * Initial Selectors of the Collection - * @default [] - */ - selectors?: { [key: string]: Selector } | string[]; - /** - * Key/Name identifier of the Collection. - * @default undefined - */ - key?: CollectionKey; - /** - * Key/Name of the property - * which represents the unique Item identifier - * in collected data objects. - * @default 'id' - */ - primaryKey?: string; - /** - * Key/Name identifier of the default Group that is created shortly after instantiation. - * The default Group represents the default pattern of the Collection. - * @default 'default' - */ - defaultGroupKey?: GroupKey; - /** - * Initial data objects of the Collection. - * @default [] - */ - initialData?: Array; -} - -export type CollectionConfig = - | CreateCollectionConfigInterface - | (( - collection: Collection - ) => CreateCollectionConfigInterface); - -export interface CollectionConfigInterface { - /** - * Key/Name of the property - * which represents the unique Item identifier - * in collected data objects. - * @default 'id' - */ - primaryKey: string; - /** - * Key/Name identifier of the default Group that is created shortly after instantiation. - * The default Group represents the default pattern of the Collection. - * @default 'default' - */ - defaultGroupKey: ItemKey; -} - -export interface CollectConfigInterface - extends AssignDataConfigInterface { - /** - * In which way the collected data should be added to the Collection. - * - 'push' = at the end - * - 'unshift' = at the beginning - * https://www.tutorialspoint.com/what-are-the-differences-between-unshift-and-push-methods-in-javascript - * @default 'push' - */ - method?: 'push' | 'unshift'; - /** - * Performs the specified action for each collected data object. - * @default undefined - */ - forEachItem?: ( - data: DataType | Item, - key: ItemKey, - success: boolean, - index: number - ) => void; - /** - * Whether to create a Selector for each collected data object. - * @default false - */ - select?: boolean; -} - -export interface UpdateConfigInterface { - /** - * Whether to merge the data object with changes into the existing Item data object - * or overwrite the existing Item data object entirely. - * @default true - */ - patch?: boolean | PatchOptionConfigInterface; - /** - * Whether to update the data object in background. - * So that the UI isn't notified of these changes and thus doesn't rerender. - * @default false - */ - background?: boolean; -} - -export interface UpdateItemKeyConfigInterface { - /** - * Whether to update the Item key/name identifier in background - * So that the UI isn't notified of these changes and thus doesn't rerender. - * @default false - */ - background?: boolean; -} - -export interface RebuildGroupsThatIncludeItemKeyConfigInterface { - /** - * Whether to rebuilt the Group in background. - * So that the UI isn't notified of these changes and thus doesn't rerender. - * @default false - */ - background?: boolean; - /** - * Whether to execute the defined side effects. - * @default true - */ - sideEffects?: SideEffectConfigInterface; -} - -export interface HasConfigInterface { - /** - * Whether Items that do not officially exist, - * such as placeholder Items, can be found - * @default true - */ - notExisting?: boolean; -} - -export interface CollectionPersistentConfigInterface { - /** - * Whether the Persistent should automatically load - * the persisted value into the Collection after its instantiation. - * @default true - */ - loadValue?: boolean; - /** - * Key/Name identifier of Storages - * in which the Collection value should be or is persisted. - * @default [`defaultStorageKey`] - */ - storageKeys?: StorageKey[]; - /** - * Key/Name identifier of the default Storage of the specified Storage keys. - * - * The Collection 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 interface RemoveItemsConfigInterface { - /** - * Whether to remove not officially existing Items (such as placeholder Items). - * Keep in mind that sometimes it won't remove an Item entirely - * as another Instance (like a Selector) might need to keep reference to it. - * https://github.com/agile-ts/agile/pull/152 - * @default false - */ - notExisting?: boolean; - /** - * Whether to remove Selectors that have selected an Item to be removed. - * @default false - */ - removeSelector?: boolean; -} - -export interface AssignDataConfigInterface { - /** - * When the Item identifier of the to assign data object already exists in the Collection, - * whether to merge the newly assigned data into the existing one - * or overwrite the existing one entirely. - * @default true - */ - patch?: boolean; - /** - * Whether to assign the data object to the Collection in background. - * So that the UI isn't notified of these changes and thus doesn't rerender. - * @default false - */ - background?: boolean; -} - -export interface AssignItemConfigInterface { - /** - * If an Item with the Item identifier already exists, - * whether to overwrite it entirely with the new one. - * @default false - */ - overwrite?: boolean; - /** - * Whether to assign the Item to the Collection in background. - * So that the UI isn't notified of these changes and thus doesn't rerender. - * @default false - */ - background?: boolean; +export * from './collection'; +// export * from './collection.persistent'; +// export * from './group'; +// export * from './group/group.observer'; +// export * from './item'; +// export * from './selector'; + +/** + * Returns a newly created Collection. + * + * A Collection manages a reactive set of Information + * that we need to remember globally at a later point in time. + * While providing a toolkit to use and mutate this set of Information. + * + * It is designed for arrays of data objects following the same pattern. + * + * Each of these data object must have a unique `primaryKey` to be correctly identified later. + * + * You can create as many global Collections as you need. + * + * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#createcollection) + * + * @public + * @param config - Configuration object + * @param agileInstance - Instance of Agile the Collection belongs to. + */ +export function createCollection( + config?: CollectionConfig, + agileInstance: Agile = shared +): Collection { + return new Collection(agileInstance, config); } diff --git a/packages/core/src/computed/computed.ts b/packages/core/src/computed/computed.ts new file mode 100644 index 00000000..416cb4db --- /dev/null +++ b/packages/core/src/computed/computed.ts @@ -0,0 +1,266 @@ +import { + State, + Agile, + Observer, + StateConfigInterface, + ComputedTracker, + Collection, + StateIngestConfigInterface, + removeProperties, + LogCodeManager, + isAsyncFunction, + extractRelevantObservers, + defineConfig, +} from '../internal'; + +export class Computed extends State< + ComputedValueType +> { + public config: ComputedConfigInterface; + + // Function to compute the Computed Class value + public computeFunction: ComputeFunctionType; + // All dependencies the Computed Class depends on (including hardCoded and automatically detected dependencies) + public deps: Set = new Set(); + // Only hardCoded dependencies the Computed Class depends on + public hardCodedDeps: Array = []; + + // Helper property to check whether an unknown instance is a Computed, + // without importing the Computed itself for using 'instanceof' (Treeshaking support) + public isComputed = true; + + /** + * A Computed is an extension of the State Class + * that computes its value based on a specified compute function. + * + * The computed value will be cached to avoid unnecessary recomputes + * and is only recomputed when one of its direct dependencies changes. + * + * 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/computed/) + * + * @public + * @param agileInstance - Instance of Agile the Computed belongs to. + * @param computeFunction - Function to compute the computed value. + * @param config - Configuration object + */ + constructor( + agileInstance: Agile, + computeFunction: ComputeFunctionType, + config: CreateComputedConfigInterface = {} + ) { + super(agileInstance, null as any, { + key: config.key, + dependents: config.dependents, + }); + config = defineConfig(config, { + computedDeps: [], + autodetect: !isAsyncFunction(computeFunction), + }); + this.agileInstance = () => agileInstance; + this.computeFunction = computeFunction; + this.config = { + autodetect: config.autodetect as any, + }; + + // Extract Observer of passed hardcoded dependency instances + this.hardCodedDeps = extractRelevantObservers( + config.computedDeps as DependableAgileInstancesType[] + ).filter((dep): dep is Observer => dep !== undefined); + this.deps = new Set(this.hardCodedDeps); + + // Make this Observer depend on the specified hard coded dep Observers + this.deps.forEach((observer) => { + observer.addDependent(this.observers['value']); + }); + + // Initial recompute to assign the computed initial value to the Computed + // and autodetect missing dependencies + this.recompute({ autodetect: config.autodetect, overwrite: true }); + } + + /** + * Forces a recomputation of the cached value with the compute function. + * + * [Learn more..](https://agile-ts.org/docs/core/computed/methods/#recompute) + * + * @public + * @param config - Configuration object + */ + public recompute(config: RecomputeConfigInterface = {}): this { + config = defineConfig(config, { + autodetect: false, + }); + this.compute({ autodetect: config.autodetect }).then((result) => { + this.observers['value'].ingestValue( + result, + removeProperties(config, ['autodetect']) + ); + }); + return this; + } + + /** + * Assigns a new function to the Computed Class for computing its value. + * + * The dependencies of the new compute function are automatically detected + * and accordingly updated. + * + * An initial computation is performed with the new function + * to change the obsolete cached value. + * + * [Learn more..](https://agile-ts.org/docs/core/computed/methods/#updatecomputefunction) + * + * @public + * @param computeFunction - New function to compute the value of the Computed Class. + * @param deps - Hard coded dependencies on which the Computed Class depends. + * @param config - Configuration object + */ + public updateComputeFunction( + computeFunction: () => ComputedValueType, + deps: Array = [], + config: RecomputeConfigInterface = {} + ): this { + config = defineConfig(config, { + autodetect: this.config.autodetect, + }); + + // Make this Observer no longer depend on the old dep Observers + this.deps.forEach((observer) => { + observer.removeDependent(this.observers['value']); + }); + + // Update dependencies of Computed + this.hardCodedDeps = extractRelevantObservers(deps).filter( + (dep): dep is Observer => dep !== undefined + ); + this.deps = new Set(this.hardCodedDeps); + + // Make this Observer depend on the new hard coded dep Observers + this.deps.forEach((observer) => { + observer.addDependent(this.observers['value']); + }); + + // Update computeFunction + this.computeFunction = computeFunction; + + // Recompute to assign the new computed value to the Computed + // and autodetect missing dependencies + this.recompute(removeProperties(config, ['overwriteDeps'])); + + return this; + } + + /** + * Computes and returns the new value of the Computed Class + * and autodetects used dependencies in the compute function. + * + * @internal + * @param config - Configuration object + */ + public async compute( + config: ComputeConfigInterface = {} + ): Promise { + config = defineConfig(config, { + autodetect: this.config.autodetect, + }); + + // Start auto tracking of Observers on which the computeFunction might depend + if (config.autodetect) ComputedTracker.track(); + + const computedValue = this.computeFunction(); + + // Handle auto tracked Observers + if (config.autodetect) { + const foundDeps = ComputedTracker.getTrackedObservers(); + + // Clean up old dependencies + this.deps.forEach((observer) => { + if ( + !foundDeps.includes(observer) && + !this.hardCodedDeps.includes(observer) + ) { + this.deps.delete(observer); + observer.removeDependent(this.observers['value']); + } + }); + + // Make this Observer depend on the newly found dep Observers + foundDeps.forEach((observer) => { + if (!this.deps.has(observer)) { + this.deps.add(observer); + observer.addDependent(this.observers['value']); + } + }); + } + + return computedValue; + } + + /** + * Not usable in Computed Class. + */ + public persist(): this { + LogCodeManager.log('19:03:00'); + return this; + } +} + +export type ComputeFunctionType = () => + | ComputedValueType + | Promise; + +export interface CreateComputedConfigInterface extends StateConfigInterface { + /** + * Hard-coded dependencies the Computed Class should depend on. + * @default [] + */ + computedDeps?: Array; + /** + * Whether the Computed should automatically detect + * used dependencies in the specified compute method. + * + * Note that the automatic dependency detection does not work + * in an asynchronous compute method! + * + * @default true if the compute method isn't asynchronous, otherwise false + */ + autodetect?: boolean; +} + +export interface ComputedConfigInterface { + /** + * Whether the Computed can automatically detect + * used dependencies in the compute method. + * + * Note that the automatic dependency detection does not work + * in an asynchronous compute method! + * + * @default true if the compute method isn't asynchronous, otherwise false + */ + autodetect: boolean; +} + +export interface ComputeConfigInterface { + /** + * Whether the Computed can automatically detect + * used dependencies in the compute method. + * + * Note that the automatic dependency detection does not work + * in an asynchronous compute method! + * + * @default true + */ + autodetect?: boolean; +} + +export interface RecomputeConfigInterface + extends StateIngestConfigInterface, + ComputeConfigInterface {} + +export type DependableAgileInstancesType = + | State + | Collection //https://stackoverflow.com/questions/66987727/type-classa-id-number-name-string-is-not-assignable-to-type-classar + | Observer; diff --git a/packages/core/src/computed/index.ts b/packages/core/src/computed/index.ts index 416cb4db..22217093 100644 --- a/packages/core/src/computed/index.ts +++ b/packages/core/src/computed/index.ts @@ -1,266 +1,86 @@ import { - State, - Agile, - Observer, - StateConfigInterface, - ComputedTracker, - Collection, - StateIngestConfigInterface, - removeProperties, - LogCodeManager, - isAsyncFunction, - extractRelevantObservers, + Computed, + ComputeFunctionType, + CreateComputedConfigInterface, + DependableAgileInstancesType, defineConfig, + removeProperties, + CreateAgileSubInstanceInterface, + shared, } from '../internal'; -export class Computed extends State< - ComputedValueType -> { - public config: ComputedConfigInterface; - - // Function to compute the Computed Class value - public computeFunction: ComputeFunctionType; - // All dependencies the Computed Class depends on (including hardCoded and automatically detected dependencies) - public deps: Set = new Set(); - // Only hardCoded dependencies the Computed Class depends on - public hardCodedDeps: Array = []; - - // Helper property to check whether an unknown instance is a Computed, - // without importing the Computed itself for using 'instanceof' (Treeshaking support) - public isComputed = true; - - /** - * A Computed is an extension of the State Class - * that computes its value based on a specified compute function. - * - * The computed value will be cached to avoid unnecessary recomputes - * and is only recomputed when one of its direct dependencies changes. - * - * 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/computed/) - * - * @public - * @param agileInstance - Instance of Agile the Computed belongs to. - * @param computeFunction - Function to compute the computed value. - * @param config - Configuration object - */ - constructor( - agileInstance: Agile, - computeFunction: ComputeFunctionType, - config: CreateComputedConfigInterface = {} - ) { - super(agileInstance, null as any, { - key: config.key, - dependents: config.dependents, - }); - config = defineConfig(config, { - computedDeps: [], - autodetect: !isAsyncFunction(computeFunction), - }); - this.agileInstance = () => agileInstance; - this.computeFunction = computeFunction; - this.config = { - autodetect: config.autodetect as any, - }; - - // Extract Observer of passed hardcoded dependency instances - this.hardCodedDeps = extractRelevantObservers( - config.computedDeps as DependableAgileInstancesType[] - ).filter((dep): dep is Observer => dep !== undefined); - this.deps = new Set(this.hardCodedDeps); - - // Make this Observer depend on the specified hard coded dep Observers - this.deps.forEach((observer) => { - observer.addDependent(this.observers['value']); - }); - - // Initial recompute to assign the computed initial value to the Computed - // and autodetect missing dependencies - this.recompute({ autodetect: config.autodetect, overwrite: true }); - } - - /** - * Forces a recomputation of the cached value with the compute function. - * - * [Learn more..](https://agile-ts.org/docs/core/computed/methods/#recompute) - * - * @public - * @param config - Configuration object - */ - public recompute(config: RecomputeConfigInterface = {}): this { - config = defineConfig(config, { - autodetect: false, - }); - this.compute({ autodetect: config.autodetect }).then((result) => { - this.observers['value'].ingestValue( - result, - removeProperties(config, ['autodetect']) - ); +export * from './computed'; +// export * from './computed.tracker'; + +export interface CreateComputedConfigInterfaceWithAgile + extends CreateAgileSubInstanceInterface, + CreateComputedConfigInterface {} + +/** + * Returns a newly created Computed. + * + * A Computed is an extension of the State Class + * that computes its value based on a specified compute function. + * + * The computed value will be cached to avoid unnecessary recomputes + * and is only recomputed when one of its direct dependencies changes. + * + * 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) + * + * @public + * @param computeFunction - Function to compute the computed value. + * @param config - Configuration object + */ +export function createComputed( + computeFunction: ComputeFunctionType, + config?: CreateComputedConfigInterfaceWithAgile +): Computed; +/** + * Returns a newly created Computed. + * + * A Computed is an extension of the State Class + * that computes its value based on a specified compute function. + * + * The computed value will be cached to avoid unnecessary recomputes + * and is only recomputed when one of its direct dependencies changes. + * + * 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#createcomputed) + * + * @public + * @param computeFunction - Function to compute the computed value. + * @param deps - Hard-coded dependencies on which the Computed Class should depend. + */ +export function createComputed( + computeFunction: ComputeFunctionType, + deps?: Array +): Computed; +export function createComputed( + computeFunction: ComputeFunctionType, + configOrDeps?: + | CreateComputedConfigInterfaceWithAgile + | Array +): Computed { + let _config: CreateComputedConfigInterfaceWithAgile = {}; + + if (Array.isArray(configOrDeps)) { + _config = defineConfig(_config, { + computedDeps: configOrDeps, }); - return this; + } else { + if (configOrDeps) _config = configOrDeps; } - /** - * Assigns a new function to the Computed Class for computing its value. - * - * The dependencies of the new compute function are automatically detected - * and accordingly updated. - * - * An initial computation is performed with the new function - * to change the obsolete cached value. - * - * [Learn more..](https://agile-ts.org/docs/core/computed/methods/#updatecomputefunction) - * - * @public - * @param computeFunction - New function to compute the value of the Computed Class. - * @param deps - Hard coded dependencies on which the Computed Class depends. - * @param config - Configuration object - */ - public updateComputeFunction( - computeFunction: () => ComputedValueType, - deps: Array = [], - config: RecomputeConfigInterface = {} - ): this { - config = defineConfig(config, { - autodetect: this.config.autodetect, - }); - - // Make this Observer no longer depend on the old dep Observers - this.deps.forEach((observer) => { - observer.removeDependent(this.observers['value']); - }); - - // Update dependencies of Computed - this.hardCodedDeps = extractRelevantObservers(deps).filter( - (dep): dep is Observer => dep !== undefined - ); - this.deps = new Set(this.hardCodedDeps); - - // Make this Observer depend on the new hard coded dep Observers - this.deps.forEach((observer) => { - observer.addDependent(this.observers['value']); - }); - - // Update computeFunction - this.computeFunction = computeFunction; - - // Recompute to assign the new computed value to the Computed - // and autodetect missing dependencies - this.recompute(removeProperties(config, ['overwriteDeps'])); + _config = defineConfig(_config, { agileInstance: shared }); - return this; - } - - /** - * Computes and returns the new value of the Computed Class - * and autodetects used dependencies in the compute function. - * - * @internal - * @param config - Configuration object - */ - public async compute( - config: ComputeConfigInterface = {} - ): Promise { - config = defineConfig(config, { - autodetect: this.config.autodetect, - }); - - // Start auto tracking of Observers on which the computeFunction might depend - if (config.autodetect) ComputedTracker.track(); - - const computedValue = this.computeFunction(); - - // Handle auto tracked Observers - if (config.autodetect) { - const foundDeps = ComputedTracker.getTrackedObservers(); - - // Clean up old dependencies - this.deps.forEach((observer) => { - if ( - !foundDeps.includes(observer) && - !this.hardCodedDeps.includes(observer) - ) { - this.deps.delete(observer); - observer.removeDependent(this.observers['value']); - } - }); - - // Make this Observer depend on the newly found dep Observers - foundDeps.forEach((observer) => { - if (!this.deps.has(observer)) { - this.deps.add(observer); - observer.addDependent(this.observers['value']); - } - }); - } - - return computedValue; - } - - /** - * Not usable in Computed Class. - */ - public persist(): this { - LogCodeManager.log('19:03:00'); - return this; - } -} - -export type ComputeFunctionType = () => - | ComputedValueType - | Promise; - -export interface CreateComputedConfigInterface extends StateConfigInterface { - /** - * Hard-coded dependencies the Computed Class should depend on. - * @default [] - */ - computedDeps?: Array; - /** - * Whether the Computed should automatically detect - * used dependencies in the specified compute method. - * - * Note that the automatic dependency detection does not work - * in an asynchronous compute method! - * - * @default true if the compute method isn't asynchronous, otherwise false - */ - autodetect?: boolean; -} - -export interface ComputedConfigInterface { - /** - * Whether the Computed can automatically detect - * used dependencies in the compute method. - * - * Note that the automatic dependency detection does not work - * in an asynchronous compute method! - * - * @default true if the compute method isn't asynchronous, otherwise false - */ - autodetect: boolean; -} - -export interface ComputeConfigInterface { - /** - * Whether the Computed can automatically detect - * used dependencies in the compute method. - * - * Note that the automatic dependency detection does not work - * in an asynchronous compute method! - * - * @default true - */ - autodetect?: boolean; + return new Computed( + _config.agileInstance as any, + computeFunction, + removeProperties(_config, ['agileInstance']) + ); } - -export interface RecomputeConfigInterface - extends StateIngestConfigInterface, - ComputeConfigInterface {} - -export type DependableAgileInstancesType = - | State - | Collection //https://stackoverflow.com/questions/66987727/type-classa-id-number-name-string-is-not-assignable-to-type-classar - | Observer; diff --git a/packages/core/src/integrations/index.ts b/packages/core/src/integrations/index.ts index c13e081b..196a738b 100644 --- a/packages/core/src/integrations/index.ts +++ b/packages/core/src/integrations/index.ts @@ -1,151 +1,2 @@ -import { Agile, Integration, LogCodeManager, defineConfig } from '../internal'; - -const onRegisterInitialIntegrationCallbacks: (( - integration: Integration -) => void)[] = []; - -export class Integrations { - // Agile Instance the Integrations belongs to - public agileInstance: () => Agile; - - // Registered Integrations - public integrations: Set = new Set(); - - // External added Integrations - // that are to integrate into not yet existing Agile Instances - static initialIntegrations: Integration[] = []; - - /** - * Registers the specified Integration in each existing or not-yet created Agile Instance. - * - * @public - * @param integration - Integration to be registered in each Agile Instance. - */ - static addInitialIntegration(integration: Integration): void { - if (integration instanceof Integration) { - // Executed external registered Integration callbacks - onRegisterInitialIntegrationCallbacks.forEach((callback) => - callback(integration) - ); - - Integrations.initialIntegrations.push(integration); - } - } - - /** - * Fires on each external added Integration. - * - * @public - * @param callback - Callback to be fired when an Integration was externally added. - */ - static onRegisterInitialIntegration( - callback: (integration: Integration) => void - ): void { - onRegisterInitialIntegrationCallbacks.push(callback); - Integrations.initialIntegrations.forEach((integration) => { - callback(integration); - }); - } - - /** - * The Integrations Class manages all Integrations for an Agile Instance - * and provides an interface to easily update - * and invoke functions in all registered Integrations. - * - * @internal - * @param agileInstance - Instance of Agile the Integrations belongs to. - * @param config - Configuration object - */ - constructor(agileInstance: Agile, config: IntegrationsConfigInterface = {}) { - config = defineConfig(config, { - autoIntegrate: true, - }); - this.agileInstance = () => agileInstance; - - if (config.autoIntegrate) { - // Setup listener to be notified when an external registered Integration was added - Integrations.onRegisterInitialIntegration((integration) => { - this.integrate(integration); - }); - } - } - - /** - * Integrates the specified Integration into AgileTs - * and sets it to ready when the binding was successful. - * - * @public - * @param integration - Integration to be integrated into AgileTs. - */ - public async integrate(integration: Integration): Promise { - // Check if Integration is valid - if (integration._key == null) { - LogCodeManager.log( - '18:03:00', - [integration._key, this.agileInstance().key], - integration - ); - return false; - } - - // Bind to integrate Integration to AgileTs - if (integration.methods.bind) - integration.ready = await integration.methods.bind(this.agileInstance()); - else integration.ready = true; - - // Integrate Integration - this.integrations.add(integration); - integration.integrated = true; - - LogCodeManager.log( - '18:00:00', - [integration._key, this.agileInstance().key], - integration - ); - - return true; - } - - /** - * Updates the specified UI-Component Instance - * with the updated data object in all registered Integrations that are ready. - * - * In doing so, it calls the `updateMethod()` method - * in all registered Integrations with the specified parameters. - * - * @public - * @param componentInstance - Component Instance to be updated. - * @param updatedData - Data object with updated data. - */ - public update(componentInstance: any, updatedData: Object): void { - this.integrations.forEach((integration) => { - if (!integration.ready) { - LogCodeManager.log('18:02:00', [integration._key]); - return; - } - if (integration.methods.updateMethod) - integration.methods.updateMethod(componentInstance, updatedData); - }); - } - - /** - * Returns a boolean indicating whether any Integration - * has been registered with the Agile Instance or not. - * - * @public - */ - public hasIntegration(): boolean { - return this.integrations.size > 0; - } -} - -export interface IntegrationsConfigInterface { - /** - * Whether external added Integrations - * are to integrate automatically into the Integrations Class. - * For example, when the package '@agile-ts/react' was installed, - * whether to automatically integrate the 'reactIntegration'. - * @default true - */ - autoIntegrate?: boolean; -} +export * from './integrations'; +// export * from './integration'; diff --git a/packages/core/src/integrations/integrations.ts b/packages/core/src/integrations/integrations.ts new file mode 100644 index 00000000..c13e081b --- /dev/null +++ b/packages/core/src/integrations/integrations.ts @@ -0,0 +1,151 @@ +import { Agile, Integration, LogCodeManager, defineConfig } from '../internal'; + +const onRegisterInitialIntegrationCallbacks: (( + integration: Integration +) => void)[] = []; + +export class Integrations { + // Agile Instance the Integrations belongs to + public agileInstance: () => Agile; + + // Registered Integrations + public integrations: Set = new Set(); + + // External added Integrations + // that are to integrate into not yet existing Agile Instances + static initialIntegrations: Integration[] = []; + + /** + * Registers the specified Integration in each existing or not-yet created Agile Instance. + * + * @public + * @param integration - Integration to be registered in each Agile Instance. + */ + static addInitialIntegration(integration: Integration): void { + if (integration instanceof Integration) { + // Executed external registered Integration callbacks + onRegisterInitialIntegrationCallbacks.forEach((callback) => + callback(integration) + ); + + Integrations.initialIntegrations.push(integration); + } + } + + /** + * Fires on each external added Integration. + * + * @public + * @param callback - Callback to be fired when an Integration was externally added. + */ + static onRegisterInitialIntegration( + callback: (integration: Integration) => void + ): void { + onRegisterInitialIntegrationCallbacks.push(callback); + Integrations.initialIntegrations.forEach((integration) => { + callback(integration); + }); + } + + /** + * The Integrations Class manages all Integrations for an Agile Instance + * and provides an interface to easily update + * and invoke functions in all registered Integrations. + * + * @internal + * @param agileInstance - Instance of Agile the Integrations belongs to. + * @param config - Configuration object + */ + constructor(agileInstance: Agile, config: IntegrationsConfigInterface = {}) { + config = defineConfig(config, { + autoIntegrate: true, + }); + this.agileInstance = () => agileInstance; + + if (config.autoIntegrate) { + // Setup listener to be notified when an external registered Integration was added + Integrations.onRegisterInitialIntegration((integration) => { + this.integrate(integration); + }); + } + } + + /** + * Integrates the specified Integration into AgileTs + * and sets it to ready when the binding was successful. + * + * @public + * @param integration - Integration to be integrated into AgileTs. + */ + public async integrate(integration: Integration): Promise { + // Check if Integration is valid + if (integration._key == null) { + LogCodeManager.log( + '18:03:00', + [integration._key, this.agileInstance().key], + integration + ); + return false; + } + + // Bind to integrate Integration to AgileTs + if (integration.methods.bind) + integration.ready = await integration.methods.bind(this.agileInstance()); + else integration.ready = true; + + // Integrate Integration + this.integrations.add(integration); + integration.integrated = true; + + LogCodeManager.log( + '18:00:00', + [integration._key, this.agileInstance().key], + integration + ); + + return true; + } + + /** + * Updates the specified UI-Component Instance + * with the updated data object in all registered Integrations that are ready. + * + * In doing so, it calls the `updateMethod()` method + * in all registered Integrations with the specified parameters. + * + * @public + * @param componentInstance - Component Instance to be updated. + * @param updatedData - Data object with updated data. + */ + public update(componentInstance: any, updatedData: Object): void { + this.integrations.forEach((integration) => { + if (!integration.ready) { + LogCodeManager.log('18:02:00', [integration._key]); + return; + } + if (integration.methods.updateMethod) + integration.methods.updateMethod(componentInstance, updatedData); + }); + } + + /** + * Returns a boolean indicating whether any Integration + * has been registered with the Agile Instance or not. + * + * @public + */ + public hasIntegration(): boolean { + return this.integrations.size > 0; + } +} + +export interface IntegrationsConfigInterface { + /** + * Whether external added Integrations + * are to integrate automatically into the Integrations Class. + * For example, when the package '@agile-ts/react' was installed, + * whether to automatically integrate the 'reactIntegration'. + * @default true + */ + autoIntegrate?: boolean; +} diff --git a/packages/core/src/runtime/index.ts b/packages/core/src/runtime/index.ts index 0dd3419f..de2b0edb 100644 --- a/packages/core/src/runtime/index.ts +++ b/packages/core/src/runtime/index.ts @@ -1,358 +1,7 @@ -import { - Agile, - SubscriptionContainer, - RuntimeJob, - CallbackSubscriptionContainer, - ComponentSubscriptionContainer, - notEqual, - LogCodeManager, - defineConfig, -} from '../internal'; - -export class Runtime { - // Agile Instance the Runtime belongs to - public agileInstance: () => Agile; - - // Job that is currently being performed - public currentJob: RuntimeJob | null = null; - // Jobs to be performed - public jobQueue: Array = []; - - // Jobs that were performed and are ready to re-render - public jobsToRerender: Array = []; - // Jobs that were performed and couldn't be re-rendered yet. - // That is the case when at least one Subscription Container (UI-Component) in the Job - // wasn't ready to update (re-render). - public notReadyJobsToRerender: Set = new Set(); - - // Whether the `jobQueue` is currently being actively processed - public isPerformingJobs = false; - - // Current 'bucket' timeout 'scheduled' for updating the Subscribers (UI-Components) - public bucketTimeout: NodeJS.Timeout | null = null; - - /** - * The Runtime queues and executes incoming Observer-based Jobs - * to prevent [race conditions](https://en.wikipedia.org/wiki/Race_condition#:~:text=A%20race%20condition%20or%20race,the%20possible%20behaviors%20is%20undesirable.) - * and optimized the re-rendering of the Observer's subscribed UI-Components. - * - * Each queued Job is executed when it is its turn - * by calling the Job Observer's `perform()` method. - * - * After successful execution, the Job is added to a re-render queue, - * which is first put into the browser's 'Bucket' and started to work off - * when resources are left. - * - * The re-render queue is designed for optimizing the render count - * by batching multiple re-render Jobs of the same UI-Component - * and ignoring re-render requests for unmounted UI-Components. - * - * @internal - * @param agileInstance - Instance of Agile the Runtime belongs to. - */ - constructor(agileInstance: Agile) { - this.agileInstance = () => agileInstance; - } - - /** - * Adds the specified Observer-based Job to the internal Job queue, - * where it is executed when it is its turn. - * - * After successful execution, the Job is assigned to the re-render queue, - * where all the Observer's subscribed Subscription Containers (UI-Components) - * are updated (re-rendered). - * - * @public - * @param job - Job to be added to the Job queue. - * @param config - Configuration object - */ - public ingest(job: RuntimeJob, config: IngestConfigInterface = {}): void { - config = defineConfig(config, { - perform: !this.isPerformingJobs, - }); - - // Add specified Job to the queue - this.jobQueue.push(job); - - LogCodeManager.logIfTags(['runtime'], '16:01:00', [job._key], job); - - // Run first Job from the queue - if (config.perform) { - const performJob = this.jobQueue.shift(); - if (performJob) this.perform(performJob); - } - } - - /** - * Performs the specified Job - * and assigns it to the re-render queue if necessary. - * - * After the execution of the provided Job, it is checked whether - * there are still Jobs left in the Job queue. - * - If so, the next Job in the `jobQueue` is performed. - * - If not, the `jobsToRerender` queue is started to work off. - * - * @internal - * @param job - Job to be performed. - */ - public perform(job: RuntimeJob): void { - this.isPerformingJobs = true; - this.currentJob = job; - - // Perform Job - job.observer.perform(job); - job.performed = true; - - // Ingest dependents of the Observer into runtime, - // since they depend on the Observer and therefore have properly changed too - job.observer.dependents.forEach((observer) => observer.ingest()); - - // Add Job to rerender queue and reset current Job property - if (job.rerender) this.jobsToRerender.push(job); - this.currentJob = null; - - LogCodeManager.logIfTags(['runtime'], '16:01:01', [job._key], job); - - // Perform Jobs as long as Jobs are left in the queue. - // If no Job is left start updating (re-rendering) Subscription Container (UI-Components) - // of the Job based on the 'jobsToRerender' queue. - if (this.jobQueue.length > 0) { - const performJob = this.jobQueue.shift(); - if (performJob) this.perform(performJob); - } else { - this.isPerformingJobs = false; - if (this.jobsToRerender.length > 0) { - if (this.agileInstance().config.bucket) { - // Check if an bucket timeout is active, if so don't call a new one, - // since if the active timeout is called it will also proceed Jobs - // that were not added before the call - if (this.bucketTimeout == null) { - // https://stackoverflow.com/questions/9083594/call-settimeout-without-delay - this.bucketTimeout = setTimeout(() => { - this.bucketTimeout = null; - this.updateSubscribers(); - }); - } - } else this.updateSubscribers(); - } - } - } - - /** - * Processes the `jobsToRerender` queue by updating (causing a re-render on) - * the subscribed Subscription Containers (UI-Components) of each Job Observer. - * - * It returns a boolean indicating whether - * any Subscription Container (UI-Component) was updated (re-rendered) or not. - * - * @internal - */ - public updateSubscribers(): boolean { - // Build final 'jobsToRerender' array - // based on the new 'jobsToRerender' array and the 'notReadyJobsToRerender' array - const jobsToRerender = this.jobsToRerender.concat( - Array.from(this.notReadyJobsToRerender) - ); - this.notReadyJobsToRerender = new Set(); - this.jobsToRerender = []; - - if (!this.agileInstance().hasIntegration() || jobsToRerender.length <= 0) - return false; - - // Extract the Subscription Container to be re-rendered from the Jobs - const subscriptionContainerToUpdate = this.extractToUpdateSubscriptionContainer( - jobsToRerender - ); - if (subscriptionContainerToUpdate.length <= 0) return false; - - // Update Subscription Container (trigger re-render on the UI-Component they represent) - this.updateSubscriptionContainer(subscriptionContainerToUpdate); - - return true; - } - - /** - * Extracts the Subscription Containers (UI-Components) - * to be updated (re-rendered) from the specified Runtime Jobs. - * - * @internal - * @param jobs - Jobs from which to extract the Subscription Containers to be updated. - */ - public extractToUpdateSubscriptionContainer( - jobs: Array - ): Array { - // https://medium.com/@bretcameron/how-to-make-your-code-faster-using-javascript-sets-b432457a4a77 - const subscriptionsToUpdate = new Set(); - - // Using for loop for performance optimization - // https://stackoverflow.com/questions/43821759/why-array-foreach-is-slower-than-for-loop-in-javascript - for (let i = 0; i < jobs.length; i++) { - const job = jobs[i]; - job.subscriptionContainersToUpdate.forEach((subscriptionContainer) => { - let updateSubscriptionContainer = true; - - // Handle not ready Subscription Container - if (!subscriptionContainer.ready) { - if ( - !job.config.maxTriesToUpdate || - job.timesTriedToUpdateCount < job.config.maxTriesToUpdate - ) { - job.timesTriedToUpdateCount++; - this.notReadyJobsToRerender.add(job); - LogCodeManager.log( - '16:02:00', - [subscriptionContainer.key], - subscriptionContainer - ); - } else { - LogCodeManager.log( - '16:02:01', - [job.config.maxTriesToUpdate], - subscriptionContainer - ); - } - return; - } - - // TODO has to be overthought because when it is a Component based Subscription - // the rerender is triggered via merging the changed properties into the Component. - // Although the 'componentId' might be equal, it doesn't mean - // that the changed properties are equal! (-> changed properties might get missing) - // Check if Subscription Container with same 'componentId' - // is already in the 'subscriptionToUpdate' queue (rerender optimisation) - // updateSubscriptionContainer = - // updateSubscriptionContainer && - // Array.from(subscriptionsToUpdate).findIndex( - // (sc) => sc.componentId === subscriptionContainer.componentId - // ) === -1; - - // Check whether a selected part of the Observer value has changed - updateSubscriptionContainer = - updateSubscriptionContainer && - this.handleSelectors(subscriptionContainer, job); - - // Add Subscription Container to the 'subscriptionsToUpdate' queue - if (updateSubscriptionContainer) { - subscriptionContainer.updatedSubscribers.add(job.observer); - subscriptionsToUpdate.add(subscriptionContainer); - } - - job.subscriptionContainersToUpdate.delete(subscriptionContainer); - }); - } - - return Array.from(subscriptionsToUpdate); - } - - /** - * Updates the specified Subscription Containers. - * - * Updating a Subscription Container triggers a re-render - * on the Component it represents, based on the type of the Subscription Containers. - * - * @internal - * @param subscriptionsToUpdate - Subscription Containers to be updated. - */ - public updateSubscriptionContainer( - subscriptionsToUpdate: Array - ): void { - // Using for loop for performance optimization - // https://stackoverflow.com/questions/43821759/why-array-foreach-is-slower-than-for-loop-in-javascript - for (let i = 0; i < subscriptionsToUpdate.length; i++) { - const subscriptionContainer = subscriptionsToUpdate[i]; - - // Call 'callback function' if Callback based Subscription - if (subscriptionContainer instanceof CallbackSubscriptionContainer) - subscriptionContainer.callback(); - - // Call 'update method' in Integrations if Component based Subscription - if (subscriptionContainer instanceof ComponentSubscriptionContainer) - this.agileInstance().integrations.update( - subscriptionContainer.component, - this.getUpdatedObserverValues(subscriptionContainer) - ); - - subscriptionContainer.updatedSubscribers.clear(); - } - - LogCodeManager.logIfTags( - ['runtime'], - '16:01:02', - [], - subscriptionsToUpdate - ); - } - - /** - * Maps the values of the updated Observers (`updatedSubscribers`) - * of the specified Subscription Container into a key map object. - * - * The key containing the Observer value is extracted from the Observer itself - * or from the Subscription Container's `subscriberKeysWeakMap`. - * - * @internal - * @param subscriptionContainer - Subscription Container from which the `updatedSubscribers` are to be mapped into a key map. - */ - public getUpdatedObserverValues( - subscriptionContainer: SubscriptionContainer - ): { [key: string]: any } { - const props: { [key: string]: any } = {}; - for (const observer of subscriptionContainer.updatedSubscribers) { - const key = - subscriptionContainer.subscriberKeysWeakMap.get(observer) ?? - observer.key; - if (key != null) props[key] = observer.value; - } - return props; - } - - /** - * Returns a boolean indicating whether the specified Subscription Container can be updated or not, - * based on its selector functions (`selectorsWeakMap`). - * - * This is done by checking the '.value' and the '.previousValue' property of the Observer represented by the Job. - * If a selected property differs, the Subscription Container (UI-Component) is allowed to update (re-render) - * and `true` is returned. - * - * If the Subscription Container has no selector function at all, `true` is returned. - * - * @internal - * @param subscriptionContainer - Subscription Container to be checked if it can be updated. - * @param job - Job containing the Observer that is subscribed to the Subscription Container. - */ - public handleSelectors( - subscriptionContainer: SubscriptionContainer, - job: RuntimeJob - ): boolean { - const selectorMethods = subscriptionContainer.selectorsWeakMap.get( - job.observer - )?.methods; - - // If no selector functions found, return true. - // Because no specific part of the Observer was selected. - // -> The Subscription Container should be updated - // no matter what has updated in the Observer. - if (selectorMethods == null) return true; - - // Check if a selected part of the Observer value has changed - const previousValue = job.observer.previousValue; - const newValue = job.observer.value; - for (const selectorMethod of selectorMethods) { - if ( - notEqual(selectorMethod(newValue), selectorMethod(previousValue)) - // || newValueDeepness !== previousValueDeepness // Not possible to check the object deepness - ) - return true; - } - - return false; - } -} - -export interface IngestConfigInterface { - /** - * Whether the ingested Job should be performed immediately - * or added to the queue first and then executed when it is his turn. - */ - perform?: boolean; -} +export * from './runtime'; +// export * from './observer'; +// export * from './runtime.job'; +// export * from './subscription/container/SubscriptionContainer'; +// export * from './subscription/container/CallbackSubscriptionContainer'; +// export * from './subscription/container/ComponentSubscriptionContainer'; +// export * from './subscription/sub.controller'; diff --git a/packages/core/src/runtime/runtime.ts b/packages/core/src/runtime/runtime.ts new file mode 100644 index 00000000..0dd3419f --- /dev/null +++ b/packages/core/src/runtime/runtime.ts @@ -0,0 +1,358 @@ +import { + Agile, + SubscriptionContainer, + RuntimeJob, + CallbackSubscriptionContainer, + ComponentSubscriptionContainer, + notEqual, + LogCodeManager, + defineConfig, +} from '../internal'; + +export class Runtime { + // Agile Instance the Runtime belongs to + public agileInstance: () => Agile; + + // Job that is currently being performed + public currentJob: RuntimeJob | null = null; + // Jobs to be performed + public jobQueue: Array = []; + + // Jobs that were performed and are ready to re-render + public jobsToRerender: Array = []; + // Jobs that were performed and couldn't be re-rendered yet. + // That is the case when at least one Subscription Container (UI-Component) in the Job + // wasn't ready to update (re-render). + public notReadyJobsToRerender: Set = new Set(); + + // Whether the `jobQueue` is currently being actively processed + public isPerformingJobs = false; + + // Current 'bucket' timeout 'scheduled' for updating the Subscribers (UI-Components) + public bucketTimeout: NodeJS.Timeout | null = null; + + /** + * The Runtime queues and executes incoming Observer-based Jobs + * to prevent [race conditions](https://en.wikipedia.org/wiki/Race_condition#:~:text=A%20race%20condition%20or%20race,the%20possible%20behaviors%20is%20undesirable.) + * and optimized the re-rendering of the Observer's subscribed UI-Components. + * + * Each queued Job is executed when it is its turn + * by calling the Job Observer's `perform()` method. + * + * After successful execution, the Job is added to a re-render queue, + * which is first put into the browser's 'Bucket' and started to work off + * when resources are left. + * + * The re-render queue is designed for optimizing the render count + * by batching multiple re-render Jobs of the same UI-Component + * and ignoring re-render requests for unmounted UI-Components. + * + * @internal + * @param agileInstance - Instance of Agile the Runtime belongs to. + */ + constructor(agileInstance: Agile) { + this.agileInstance = () => agileInstance; + } + + /** + * Adds the specified Observer-based Job to the internal Job queue, + * where it is executed when it is its turn. + * + * After successful execution, the Job is assigned to the re-render queue, + * where all the Observer's subscribed Subscription Containers (UI-Components) + * are updated (re-rendered). + * + * @public + * @param job - Job to be added to the Job queue. + * @param config - Configuration object + */ + public ingest(job: RuntimeJob, config: IngestConfigInterface = {}): void { + config = defineConfig(config, { + perform: !this.isPerformingJobs, + }); + + // Add specified Job to the queue + this.jobQueue.push(job); + + LogCodeManager.logIfTags(['runtime'], '16:01:00', [job._key], job); + + // Run first Job from the queue + if (config.perform) { + const performJob = this.jobQueue.shift(); + if (performJob) this.perform(performJob); + } + } + + /** + * Performs the specified Job + * and assigns it to the re-render queue if necessary. + * + * After the execution of the provided Job, it is checked whether + * there are still Jobs left in the Job queue. + * - If so, the next Job in the `jobQueue` is performed. + * - If not, the `jobsToRerender` queue is started to work off. + * + * @internal + * @param job - Job to be performed. + */ + public perform(job: RuntimeJob): void { + this.isPerformingJobs = true; + this.currentJob = job; + + // Perform Job + job.observer.perform(job); + job.performed = true; + + // Ingest dependents of the Observer into runtime, + // since they depend on the Observer and therefore have properly changed too + job.observer.dependents.forEach((observer) => observer.ingest()); + + // Add Job to rerender queue and reset current Job property + if (job.rerender) this.jobsToRerender.push(job); + this.currentJob = null; + + LogCodeManager.logIfTags(['runtime'], '16:01:01', [job._key], job); + + // Perform Jobs as long as Jobs are left in the queue. + // If no Job is left start updating (re-rendering) Subscription Container (UI-Components) + // of the Job based on the 'jobsToRerender' queue. + if (this.jobQueue.length > 0) { + const performJob = this.jobQueue.shift(); + if (performJob) this.perform(performJob); + } else { + this.isPerformingJobs = false; + if (this.jobsToRerender.length > 0) { + if (this.agileInstance().config.bucket) { + // Check if an bucket timeout is active, if so don't call a new one, + // since if the active timeout is called it will also proceed Jobs + // that were not added before the call + if (this.bucketTimeout == null) { + // https://stackoverflow.com/questions/9083594/call-settimeout-without-delay + this.bucketTimeout = setTimeout(() => { + this.bucketTimeout = null; + this.updateSubscribers(); + }); + } + } else this.updateSubscribers(); + } + } + } + + /** + * Processes the `jobsToRerender` queue by updating (causing a re-render on) + * the subscribed Subscription Containers (UI-Components) of each Job Observer. + * + * It returns a boolean indicating whether + * any Subscription Container (UI-Component) was updated (re-rendered) or not. + * + * @internal + */ + public updateSubscribers(): boolean { + // Build final 'jobsToRerender' array + // based on the new 'jobsToRerender' array and the 'notReadyJobsToRerender' array + const jobsToRerender = this.jobsToRerender.concat( + Array.from(this.notReadyJobsToRerender) + ); + this.notReadyJobsToRerender = new Set(); + this.jobsToRerender = []; + + if (!this.agileInstance().hasIntegration() || jobsToRerender.length <= 0) + return false; + + // Extract the Subscription Container to be re-rendered from the Jobs + const subscriptionContainerToUpdate = this.extractToUpdateSubscriptionContainer( + jobsToRerender + ); + if (subscriptionContainerToUpdate.length <= 0) return false; + + // Update Subscription Container (trigger re-render on the UI-Component they represent) + this.updateSubscriptionContainer(subscriptionContainerToUpdate); + + return true; + } + + /** + * Extracts the Subscription Containers (UI-Components) + * to be updated (re-rendered) from the specified Runtime Jobs. + * + * @internal + * @param jobs - Jobs from which to extract the Subscription Containers to be updated. + */ + public extractToUpdateSubscriptionContainer( + jobs: Array + ): Array { + // https://medium.com/@bretcameron/how-to-make-your-code-faster-using-javascript-sets-b432457a4a77 + const subscriptionsToUpdate = new Set(); + + // Using for loop for performance optimization + // https://stackoverflow.com/questions/43821759/why-array-foreach-is-slower-than-for-loop-in-javascript + for (let i = 0; i < jobs.length; i++) { + const job = jobs[i]; + job.subscriptionContainersToUpdate.forEach((subscriptionContainer) => { + let updateSubscriptionContainer = true; + + // Handle not ready Subscription Container + if (!subscriptionContainer.ready) { + if ( + !job.config.maxTriesToUpdate || + job.timesTriedToUpdateCount < job.config.maxTriesToUpdate + ) { + job.timesTriedToUpdateCount++; + this.notReadyJobsToRerender.add(job); + LogCodeManager.log( + '16:02:00', + [subscriptionContainer.key], + subscriptionContainer + ); + } else { + LogCodeManager.log( + '16:02:01', + [job.config.maxTriesToUpdate], + subscriptionContainer + ); + } + return; + } + + // TODO has to be overthought because when it is a Component based Subscription + // the rerender is triggered via merging the changed properties into the Component. + // Although the 'componentId' might be equal, it doesn't mean + // that the changed properties are equal! (-> changed properties might get missing) + // Check if Subscription Container with same 'componentId' + // is already in the 'subscriptionToUpdate' queue (rerender optimisation) + // updateSubscriptionContainer = + // updateSubscriptionContainer && + // Array.from(subscriptionsToUpdate).findIndex( + // (sc) => sc.componentId === subscriptionContainer.componentId + // ) === -1; + + // Check whether a selected part of the Observer value has changed + updateSubscriptionContainer = + updateSubscriptionContainer && + this.handleSelectors(subscriptionContainer, job); + + // Add Subscription Container to the 'subscriptionsToUpdate' queue + if (updateSubscriptionContainer) { + subscriptionContainer.updatedSubscribers.add(job.observer); + subscriptionsToUpdate.add(subscriptionContainer); + } + + job.subscriptionContainersToUpdate.delete(subscriptionContainer); + }); + } + + return Array.from(subscriptionsToUpdate); + } + + /** + * Updates the specified Subscription Containers. + * + * Updating a Subscription Container triggers a re-render + * on the Component it represents, based on the type of the Subscription Containers. + * + * @internal + * @param subscriptionsToUpdate - Subscription Containers to be updated. + */ + public updateSubscriptionContainer( + subscriptionsToUpdate: Array + ): void { + // Using for loop for performance optimization + // https://stackoverflow.com/questions/43821759/why-array-foreach-is-slower-than-for-loop-in-javascript + for (let i = 0; i < subscriptionsToUpdate.length; i++) { + const subscriptionContainer = subscriptionsToUpdate[i]; + + // Call 'callback function' if Callback based Subscription + if (subscriptionContainer instanceof CallbackSubscriptionContainer) + subscriptionContainer.callback(); + + // Call 'update method' in Integrations if Component based Subscription + if (subscriptionContainer instanceof ComponentSubscriptionContainer) + this.agileInstance().integrations.update( + subscriptionContainer.component, + this.getUpdatedObserverValues(subscriptionContainer) + ); + + subscriptionContainer.updatedSubscribers.clear(); + } + + LogCodeManager.logIfTags( + ['runtime'], + '16:01:02', + [], + subscriptionsToUpdate + ); + } + + /** + * Maps the values of the updated Observers (`updatedSubscribers`) + * of the specified Subscription Container into a key map object. + * + * The key containing the Observer value is extracted from the Observer itself + * or from the Subscription Container's `subscriberKeysWeakMap`. + * + * @internal + * @param subscriptionContainer - Subscription Container from which the `updatedSubscribers` are to be mapped into a key map. + */ + public getUpdatedObserverValues( + subscriptionContainer: SubscriptionContainer + ): { [key: string]: any } { + const props: { [key: string]: any } = {}; + for (const observer of subscriptionContainer.updatedSubscribers) { + const key = + subscriptionContainer.subscriberKeysWeakMap.get(observer) ?? + observer.key; + if (key != null) props[key] = observer.value; + } + return props; + } + + /** + * Returns a boolean indicating whether the specified Subscription Container can be updated or not, + * based on its selector functions (`selectorsWeakMap`). + * + * This is done by checking the '.value' and the '.previousValue' property of the Observer represented by the Job. + * If a selected property differs, the Subscription Container (UI-Component) is allowed to update (re-render) + * and `true` is returned. + * + * If the Subscription Container has no selector function at all, `true` is returned. + * + * @internal + * @param subscriptionContainer - Subscription Container to be checked if it can be updated. + * @param job - Job containing the Observer that is subscribed to the Subscription Container. + */ + public handleSelectors( + subscriptionContainer: SubscriptionContainer, + job: RuntimeJob + ): boolean { + const selectorMethods = subscriptionContainer.selectorsWeakMap.get( + job.observer + )?.methods; + + // If no selector functions found, return true. + // Because no specific part of the Observer was selected. + // -> The Subscription Container should be updated + // no matter what has updated in the Observer. + if (selectorMethods == null) return true; + + // Check if a selected part of the Observer value has changed + const previousValue = job.observer.previousValue; + const newValue = job.observer.value; + for (const selectorMethod of selectorMethods) { + if ( + notEqual(selectorMethod(newValue), selectorMethod(previousValue)) + // || newValueDeepness !== previousValueDeepness // Not possible to check the object deepness + ) + return true; + } + + return false; + } +} + +export interface IngestConfigInterface { + /** + * Whether the ingested Job should be performed immediately + * or added to the queue first and then executed when it is his turn. + */ + perform?: boolean; +} diff --git a/packages/core/src/shared.ts b/packages/core/src/shared.ts index 4d31abdd..604d1c89 100644 --- a/packages/core/src/shared.ts +++ b/packages/core/src/shared.ts @@ -1,20 +1,4 @@ -import { - Agile, - Collection, - CollectionConfig, - Computed, - ComputeFunctionType, - CreateComputedConfigInterface, - CreateStorageConfigInterface, - DefaultItem, - defineConfig, - DependableAgileInstancesType, - removeProperties, - runsOnServer, - State, - StateConfigInterface, - Storage, -} from './internal'; +import { Agile, runsOnServer } from './internal'; /** * Shared Agile Instance that is used when no Agile Instance was specified. @@ -35,157 +19,6 @@ export function assignSharedAgileInstance(agileInstance: Agile): void { sharedAgileInstance = agileInstance; } -/** - * Returns a newly created Storage. - * - * A Storage Class serves as an interface to external storages, - * such as the [Async Storage](https://github.com/react-native-async-storage/async-storage) or - * [Local Storage](https://www.w3schools.com/html/html5_webstorage.asp). - * - * It creates the foundation to easily [`persist()`](https://agile-ts.org/docs/core/state/methods#persist) [Agile Sub Instances](https://agile-ts.org/docs/introduction/#agile-sub-instance) - * (like States or Collections) in nearly any external storage. - * - * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#createstorage) - * - * @public - * @param config - Configuration object - */ -export function createStorage(config: CreateStorageConfigInterface): Storage { - return new Storage(config); -} - -/** - * Returns a newly created State. - * - * A State manages 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. - * - * You can create as many global 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 = {} -): State { - config = defineConfig(config, { - agileInstance: sharedAgileInstance, - }); - return new State( - config.agileInstance as any, - initialValue, - removeProperties(config, ['agileInstance']) - ); -} - -/** - * Returns a newly created Computed. - * - * A Computed is an extension of the State Class - * that computes its value based on a specified compute function. - * - * The computed value will be cached to avoid unnecessary recomputes - * and is only recomputed when one of its direct dependencies changes. - * - * 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) - * - * @public - * @param computeFunction - Function to compute the computed value. - * @param config - Configuration object - */ -export function createComputed( - computeFunction: ComputeFunctionType, - config?: CreateComputedConfigInterfaceWithAgile -): Computed; -/** - * Returns a newly created Computed. - * - * A Computed is an extension of the State Class - * that computes its value based on a specified compute function. - * - * The computed value will be cached to avoid unnecessary recomputes - * and is only recomputed when one of its direct dependencies changes. - * - * 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#createcomputed) - * - * @public - * @param computeFunction - Function to compute the computed value. - * @param deps - Hard-coded dependencies on which the Computed Class should depend. - */ -export function createComputed( - computeFunction: ComputeFunctionType, - deps?: Array -): Computed; -export function createComputed( - computeFunction: ComputeFunctionType, - configOrDeps?: - | CreateComputedConfigInterfaceWithAgile - | Array -): Computed { - let _config: CreateComputedConfigInterfaceWithAgile = {}; - - if (Array.isArray(configOrDeps)) { - _config = defineConfig(_config, { - computedDeps: configOrDeps, - }); - } else { - if (configOrDeps) _config = configOrDeps; - } - - _config = defineConfig(_config, { agileInstance: sharedAgileInstance }); - - return new Computed( - _config.agileInstance as any, - computeFunction, - removeProperties(_config, ['agileInstance']) - ); -} - -/** - * Returns a newly created Collection. - * - * A Collection manages a reactive set of Information - * that we need to remember globally at a later point in time. - * While providing a toolkit to use and mutate this set of Information. - * - * It is designed for arrays of data objects following the same pattern. - * - * Each of these data object must have a unique `primaryKey` to be correctly identified later. - * - * You can create as many global Collections as you need. - * - * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#createcollection) - * - * @public - * @param config - Configuration object - * @param agileInstance - Instance of Agile the Collection belongs to. - */ -export function createCollection( - config?: CollectionConfig, - agileInstance: Agile = sharedAgileInstance -): Collection { - return new Collection(agileInstance, config); -} - -export interface CreateComputedConfigInterfaceWithAgile - extends CreateAgileSubInstanceInterface, - CreateComputedConfigInterface {} - -export interface CreateStateConfigInterfaceWithAgile - extends CreateAgileSubInstanceInterface, - StateConfigInterface {} - export interface CreateAgileSubInstanceInterface { /** * Instance of Agile the Instance belongs to. diff --git a/packages/core/src/state/index.ts b/packages/core/src/state/index.ts index 592a4e36..ba8e7e27 100644 --- a/packages/core/src/state/index.ts +++ b/packages/core/src/state/index.ts @@ -1,807 +1,46 @@ import { - Agile, - StorageKey, - copy, - flatMerge, - isValidObject, - StateObserver, - StatePersistent, - Observer, - equal, - isFunction, - notEqual, - generateId, - PersistentKey, - ComputedTracker, - StateIngestConfigInterface, - removeProperties, - LogCodeManager, + State, + StateConfigInterface, defineConfig, + removeProperties, + CreateAgileSubInstanceInterface, + shared, } from '../internal'; -export class State { - // Agile Instance the State belongs to - public agileInstance: () => Agile; - - // Key/Name identifier of the State - public _key?: StateKey; - // Whether the current value differs from the initial value - public isSet = false; - // Whether the State is a placeholder and only exist in the background - public isPlaceholder = false; - - // First value assigned to the State - public initialStateValue: ValueType; - // Current value of the State - public _value: ValueType; - // Previous value of the State - public previousStateValue: ValueType; - // Next value of the State (which can be used for dynamic State updates) - public nextStateValue: ValueType; - - // Manages dependencies to other States and subscriptions of UI-Components. - // It also serves as an interface to the runtime. - public observers: StateObserversInterface = {} as any; - // Registered side effects of changing the State value - public sideEffects: { - [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. - * While providing a toolkit to use and mutate this piece of Information. - * - * You can create as many global States as you need. - * - * [Learn more..](https://agile-ts.org/docs/core/state/) - * - * @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 = {} - ) { - config = defineConfig(config, { - dependents: [], - isPlaceholder: false, - }); - this.agileInstance = () => agileInstance; - this._key = config.key; - this.observers['value'] = new StateObserver(this, { - key: config.key, - dependents: config.dependents, - }); - this.initialStateValue = copy(initialValue); - this._value = copy(initialValue); - 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 }); - } - - /** - * Assigns a new value to the State - * and rerenders all subscribed Components. - * - * [Learn more..](https://agile-ts.org/docs/core/state/properties#value) - * - * @public - * @param value - New State value. - */ - public set value(value: ValueType) { - this.set(value); - } - - /** - * Returns a reference-free version of the current State value. - * - * [Learn more..](https://agile-ts.org/docs/core/state/properties#value) - * - * @public - */ - public get value(): ValueType { - ComputedTracker.tracked(this.observers['value']); - return copy(this._value); - } - - /** - * Updates the key/name identifier of the State. - * - * [Learn more..](https://agile-ts.org/docs/core/state/properties#key) - * - * @public - * @param value - New key/name identifier. - */ - public set key(value: StateKey | undefined) { - this.setKey(value); - } - - /** - * Returns the key/name identifier of the State. - * - * [Learn more..](https://agile-ts.org/docs/core/state/properties#key) - * - * @public - */ - public get key(): StateKey | undefined { - return this._key; - } - - /** - * Updates the key/name identifier of the State. - * - * [Learn more..](https://agile-ts.org/docs/core/state/methods/#setkey) - * - * @public - * @param value - New key/name identifier. - */ - public setKey(value: StateKey | undefined): this { - const oldKey = this._key; - - // Update State key - this._key = value; - - // Update key of Observers - 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; - } - - /** - * Assigns a new value to the State - * and re-renders all subscribed UI-Components. - * - * [Learn more..](https://agile-ts.org/docs/core/state/methods/#set) - * - * @public - * @param value - New State value - * @param config - Configuration object - */ - public set( - value: ValueType | ((value: ValueType) => ValueType), - config: StateIngestConfigInterface = {} - ): this { - config = defineConfig(config, { - force: false, - }); - const _value = isFunction(value) - ? (value as any)(copy(this._value)) - : value; - - // Ingest the State with the new value into the runtime - this.observers['value'].ingestValue(_value, config); - - return this; - } - - /** - * Ingests the State without any specified new value into the runtime. - * - * Since no new value was defined either the new State value is computed - * based on a compute method (Computed Class) - * or the `nextStateValue` is taken as the next State value. - * - * [Learn more..](https://agile-ts.org/docs/core/state/methods/#ingest) - * - * @internal - * @param config - Configuration object - */ - public ingest(config: StateIngestConfigInterface = {}): this { - this.observers['value'].ingest(config); - 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` - * as a side effect of State changes. - * - * For example, it is called when the State value changes from 'jeff' to 'hans'. - * - * A typical side effect of a State change - * could be the updating of the external Storage value. - * - * @internal - * @param key - Key/Name identifier of the to register side effect. - * @param callback - Callback function to be fired on each State value change. - * @param config - Configuration object - */ - public addSideEffect>( - key: string, - callback: SideEffectFunctionType, - config: AddSideEffectConfigInterface = {} - ): this { - config = defineConfig(config, { - weight: 10, - }); - if (!isFunction(callback)) { - LogCodeManager.log('00:03:01', ['Side Effect Callback', 'function']); - return this; - } - this.sideEffects[key] = { - callback: callback as any, - weight: config.weight as any, - }; - return this; - } - - /** - * Removes a side effect callback with the specified key/name identifier from the State. - * - * [Learn more..](https://agile-ts.org/docs/core/state/methods/#removesideeffect) - * - * @internal - * @param key - Key/Name identifier of the side effect callback to be removed. - */ - public removeSideEffect(key: string): this { - delete this.sideEffects[key]; - return this; - } - - /** - * Returns a boolean indicating whether a side effect callback with the specified `key` - * exists in the State or not. - * - * [Learn more..](https://agile-ts.org/docs/core/state/methods/#hassideeffect) - * - * @internal - * @param key - Key/Name identifier of the side effect callback to be checked for existence. - */ - 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; - -export interface StateObserversInterface { - /** - * Observer responsible for the value of the State. - */ - value: StateObserver; -} - -export interface StateConfigInterface { - /** - * Key/Name identifier of the State. - * @default undefined - */ - key?: StateKey; - /** - * Observers that depend on the State. - * @default [] - */ - dependents?: Array; - /** - * Whether the State should be a placeholder - * and therefore should only exist in the background. - * @default false - */ - 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?: { - [key: string]: any; - } -) => void; - -export interface SideEffectInterface { - /** - * Callback function to be called on every State value change. - * @return () => {} - */ - callback: SideEffectFunctionType; - /** - * Weight of the side effect. - * The weight determines the order of execution of the registered side effects. - * The higher the weight, the earlier it is executed. - */ - weight: number; -} - -export interface AddSideEffectConfigInterface { - /** - * Weight of the side effect. - * The weight determines the order of execution of the registered side effects. - * The higher the weight, the earlier it is executed. - */ - weight?: number; +export * from './state'; +// export * from './state.observer'; +// export * from './state.persistent'; +// export * from './state.runtime.job'; + +export interface CreateStateConfigInterfaceWithAgile + extends CreateAgileSubInstanceInterface, + StateConfigInterface {} + +/** + * Returns a newly created State. + * + * A State manages 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. + * + * You can create as many global 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 = {} +): State { + config = defineConfig(config, { + agileInstance: shared, + }); + return new State( + config.agileInstance as any, + initialValue, + removeProperties(config, ['agileInstance']) + ); } diff --git a/packages/core/src/state/state.ts b/packages/core/src/state/state.ts new file mode 100644 index 00000000..592a4e36 --- /dev/null +++ b/packages/core/src/state/state.ts @@ -0,0 +1,807 @@ +import { + Agile, + StorageKey, + copy, + flatMerge, + isValidObject, + StateObserver, + StatePersistent, + Observer, + equal, + isFunction, + notEqual, + generateId, + PersistentKey, + ComputedTracker, + StateIngestConfigInterface, + removeProperties, + LogCodeManager, + defineConfig, +} from '../internal'; + +export class State { + // Agile Instance the State belongs to + public agileInstance: () => Agile; + + // Key/Name identifier of the State + public _key?: StateKey; + // Whether the current value differs from the initial value + public isSet = false; + // Whether the State is a placeholder and only exist in the background + public isPlaceholder = false; + + // First value assigned to the State + public initialStateValue: ValueType; + // Current value of the State + public _value: ValueType; + // Previous value of the State + public previousStateValue: ValueType; + // Next value of the State (which can be used for dynamic State updates) + public nextStateValue: ValueType; + + // Manages dependencies to other States and subscriptions of UI-Components. + // It also serves as an interface to the runtime. + public observers: StateObserversInterface = {} as any; + // Registered side effects of changing the State value + public sideEffects: { + [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. + * While providing a toolkit to use and mutate this piece of Information. + * + * You can create as many global States as you need. + * + * [Learn more..](https://agile-ts.org/docs/core/state/) + * + * @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 = {} + ) { + config = defineConfig(config, { + dependents: [], + isPlaceholder: false, + }); + this.agileInstance = () => agileInstance; + this._key = config.key; + this.observers['value'] = new StateObserver(this, { + key: config.key, + dependents: config.dependents, + }); + this.initialStateValue = copy(initialValue); + this._value = copy(initialValue); + 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 }); + } + + /** + * Assigns a new value to the State + * and rerenders all subscribed Components. + * + * [Learn more..](https://agile-ts.org/docs/core/state/properties#value) + * + * @public + * @param value - New State value. + */ + public set value(value: ValueType) { + this.set(value); + } + + /** + * Returns a reference-free version of the current State value. + * + * [Learn more..](https://agile-ts.org/docs/core/state/properties#value) + * + * @public + */ + public get value(): ValueType { + ComputedTracker.tracked(this.observers['value']); + return copy(this._value); + } + + /** + * Updates the key/name identifier of the State. + * + * [Learn more..](https://agile-ts.org/docs/core/state/properties#key) + * + * @public + * @param value - New key/name identifier. + */ + public set key(value: StateKey | undefined) { + this.setKey(value); + } + + /** + * Returns the key/name identifier of the State. + * + * [Learn more..](https://agile-ts.org/docs/core/state/properties#key) + * + * @public + */ + public get key(): StateKey | undefined { + return this._key; + } + + /** + * Updates the key/name identifier of the State. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#setkey) + * + * @public + * @param value - New key/name identifier. + */ + public setKey(value: StateKey | undefined): this { + const oldKey = this._key; + + // Update State key + this._key = value; + + // Update key of Observers + 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; + } + + /** + * Assigns a new value to the State + * and re-renders all subscribed UI-Components. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#set) + * + * @public + * @param value - New State value + * @param config - Configuration object + */ + public set( + value: ValueType | ((value: ValueType) => ValueType), + config: StateIngestConfigInterface = {} + ): this { + config = defineConfig(config, { + force: false, + }); + const _value = isFunction(value) + ? (value as any)(copy(this._value)) + : value; + + // Ingest the State with the new value into the runtime + this.observers['value'].ingestValue(_value, config); + + return this; + } + + /** + * Ingests the State without any specified new value into the runtime. + * + * Since no new value was defined either the new State value is computed + * based on a compute method (Computed Class) + * or the `nextStateValue` is taken as the next State value. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#ingest) + * + * @internal + * @param config - Configuration object + */ + public ingest(config: StateIngestConfigInterface = {}): this { + this.observers['value'].ingest(config); + 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` + * as a side effect of State changes. + * + * For example, it is called when the State value changes from 'jeff' to 'hans'. + * + * A typical side effect of a State change + * could be the updating of the external Storage value. + * + * @internal + * @param key - Key/Name identifier of the to register side effect. + * @param callback - Callback function to be fired on each State value change. + * @param config - Configuration object + */ + public addSideEffect>( + key: string, + callback: SideEffectFunctionType, + config: AddSideEffectConfigInterface = {} + ): this { + config = defineConfig(config, { + weight: 10, + }); + if (!isFunction(callback)) { + LogCodeManager.log('00:03:01', ['Side Effect Callback', 'function']); + return this; + } + this.sideEffects[key] = { + callback: callback as any, + weight: config.weight as any, + }; + return this; + } + + /** + * Removes a side effect callback with the specified key/name identifier from the State. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#removesideeffect) + * + * @internal + * @param key - Key/Name identifier of the side effect callback to be removed. + */ + public removeSideEffect(key: string): this { + delete this.sideEffects[key]; + return this; + } + + /** + * Returns a boolean indicating whether a side effect callback with the specified `key` + * exists in the State or not. + * + * [Learn more..](https://agile-ts.org/docs/core/state/methods/#hassideeffect) + * + * @internal + * @param key - Key/Name identifier of the side effect callback to be checked for existence. + */ + 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; + +export interface StateObserversInterface { + /** + * Observer responsible for the value of the State. + */ + value: StateObserver; +} + +export interface StateConfigInterface { + /** + * Key/Name identifier of the State. + * @default undefined + */ + key?: StateKey; + /** + * Observers that depend on the State. + * @default [] + */ + dependents?: Array; + /** + * Whether the State should be a placeholder + * and therefore should only exist in the background. + * @default false + */ + 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?: { + [key: string]: any; + } +) => void; + +export interface SideEffectInterface { + /** + * Callback function to be called on every State value change. + * @return () => {} + */ + callback: SideEffectFunctionType; + /** + * Weight of the side effect. + * The weight determines the order of execution of the registered side effects. + * The higher the weight, the earlier it is executed. + */ + weight: number; +} + +export interface AddSideEffectConfigInterface { + /** + * Weight of the side effect. + * The weight determines the order of execution of the registered side effects. + * The higher the weight, the earlier it is executed. + */ + weight?: number; +} diff --git a/packages/core/src/storages/index.ts b/packages/core/src/storages/index.ts index 498f9bb5..a8938be1 100644 --- a/packages/core/src/storages/index.ts +++ b/packages/core/src/storages/index.ts @@ -1,333 +1,24 @@ -import { - Agile, - Storage, - Persistent, - StorageKey, - StorageItemKey, - notEqual, - LogCodeManager, - defineConfig, -} from '../internal'; - -export class Storages { - // Agile Instance the Storages belongs to - public agileInstance: () => Agile; - - public config: StoragesConfigInterface; - - // Registered Storages - public storages: { [key: string]: Storage } = {}; - // Persistent from Instances (for example States) that were persisted - public persistentInstances: Set = new Set(); - - /** - * The Storages Class manages all external Storages for an Agile Instance - * and provides an interface to easily store, - * load and remove values from multiple Storages at once. - * - * @internal - * @param agileInstance - Instance of Agile the Storages belongs to. - * @param config - Configuration object - */ - constructor( - agileInstance: Agile, - config: CreateStoragesConfigInterface = {} - ) { - this.agileInstance = () => agileInstance; - config = defineConfig(config, { - localStorage: false, - defaultStorageKey: null as any, - }); - this.config = { defaultStorageKey: config.defaultStorageKey as any }; - if (config.localStorage) this.instantiateLocalStorage(); - } - - /** - * Instantiates and registers the - * [Local Storage](https://developer.mozilla.org/de/docs/Web/API/Window/localStorage). - * - * Note that the Local Storage is only available in a web environment. - * - * @internal - */ - public instantiateLocalStorage(): boolean { - if (!Storages.localStorageAvailable()) { - LogCodeManager.log('11:02:00'); - return false; - } - const _localStorage = new Storage({ - key: 'localStorage', - async: false, - methods: { - get: localStorage.getItem.bind(localStorage), - set: localStorage.setItem.bind(localStorage), - remove: localStorage.removeItem.bind(localStorage), - }, - }); - return this.register(_localStorage, { default: true }); - } - - /** - * Registers the specified Storage with AgileTs - * and updates the Persistent Instances that have already attempted - * to use the previously unregistered Storage. - * - * @public - * @param storage - Storage to be registered with AgileTs. - * @param config - Configuration object - */ - public register( - storage: Storage, - config: RegisterConfigInterface = {} - ): boolean { - const hasRegisteredAnyStorage = notEqual(this.storages, {}); - - // Check if Storage already exists - if (Object.prototype.hasOwnProperty.call(this.storages, storage.key)) { - LogCodeManager.log('11:03:00', [storage.key]); - return false; - } - - // Assign Storage as default Storage if it is the first one added - if (!hasRegisteredAnyStorage && config.default === false) - LogCodeManager.log('11:02:01'); - if (!hasRegisteredAnyStorage) config.default = true; - - // Register Storage - this.storages[storage.key] = storage; - if (config.default) this.config.defaultStorageKey = storage.key; - - this.persistentInstances.forEach((persistent) => { - // 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; - } - - // If Persistent has no default Storage key, - // reassign Storage keys since the now registered Storage - // might be tagged as default Storage of AgileTs - if (persistent.config.defaultStorageKey == null) { - persistent.assignStorageKeys(); - const isValid = persistent.validatePersistent(); - if (isValid) persistent.initialLoading(); - } - }); - - LogCodeManager.log('13:00:00', [storage.key], storage); - - return true; - } - - /** - * Retrieves a single Storage with the specified key/name identifier - * from the Storages Class. - * - * If the to retrieve Storage doesn't exist, `undefined` is returned. - * - * @public - * @param storageKey - Key/Name identifier of the Storage. - */ - public getStorage( - storageKey: StorageKey | undefined | null - ): Storage | undefined { - if (!storageKey) return undefined; - const storage = this.storages[storageKey]; - if (!storage) { - LogCodeManager.log('11:03:01', [storageKey]); - return undefined; - } - if (!storage.ready) { - LogCodeManager.log('11:03:02', [storageKey]); - return undefined; - } - return storage; - } - - /** - * Retrieves the stored value at the specified Storage Item key - * from the defined external Storage (`storageKey`). - * - * When no Storage has been specified, - * the value is retrieved from the default Storage. - * - * @public - * @param storageItemKey - Key/Name identifier of the value to be retrieved. - * @param storageKey - Key/Name identifier of the external Storage - * from which the value is to be retrieved. - */ - public get( - storageItemKey: StorageItemKey, - storageKey?: StorageKey - ): Promise { - if (!this.hasStorage()) { - LogCodeManager.log('11:03:03'); - return Promise.resolve(undefined); - } - - // Call get method on specified Storage - if (storageKey) { - const storage = this.getStorage(storageKey); - if (storage) return storage.get(storageItemKey); - } - - // Call get method on default Storage - const defaultStorage = this.getStorage(this.config.defaultStorageKey); - return ( - defaultStorage?.get(storageItemKey) || Promise.resolve(undefined) - ); - } - - /** - * Stores or updates the value at the specified Storage Item key - * in the defined external Storages (`storageKeys`). - * - * When no Storage has been specified, - * the value is stored/updated in the default Storage. - * - * @public - * @param storageItemKey - Key/Name identifier of the value to be stored. - * @param value - Value to be stored in an external Storage. - * @param storageKeys - Key/Name identifier of the external Storage - * where the value is to be stored. - */ - public set( - storageItemKey: StorageItemKey, - value: any, - storageKeys?: StorageKey[] - ): void { - if (!this.hasStorage()) { - LogCodeManager.log('11:03:04'); - return; - } - - // Call set method on specified Storages - if (storageKeys != null) { - for (const storageKey of storageKeys) - this.getStorage(storageKey)?.set(storageItemKey, value); - return; - } - - // Call set method on default Storage - const defaultStorage = this.getStorage(this.config.defaultStorageKey); - defaultStorage?.set(storageItemKey, value); - } - - /** - * Removes the value at the specified Storage Item key - * from the defined external Storages (`storageKeys`). - * - * When no Storage has been specified, - * the value is removed from the default Storage. - * - * @public - * @param storageItemKey - Key/Name identifier of the value to be removed. - * @param storageKeys - Key/Name identifier of the external Storage - * from which the value is to be removed. - */ - public remove( - storageItemKey: StorageItemKey, - storageKeys?: StorageKey[] - ): void { - if (!this.hasStorage()) { - LogCodeManager.log('11:03:05'); - return; - } - - // Call remove method on specified Storages - if (storageKeys) { - for (const storageKey of storageKeys) - this.getStorage(storageKey)?.remove(storageItemKey); - return; - } - - // Call remove method on default Storage - const defaultStorage = this.getStorage(this.config.defaultStorageKey); - defaultStorage?.remove(storageItemKey); - } - - /** - * Returns a boolean indicating whether any Storage - * has been registered with the Agile Instance or not. - * - * @public - */ - public hasStorage(): boolean { - return notEqual(this.storages, {}); - } - - /** - * Returns a boolean indication whether the - * [Local Storage](https://developer.mozilla.org/de/docs/Web/API/Window/localStorage) - * is available in the current environment. - * - * @public - */ - static localStorageAvailable(): boolean { - try { - localStorage.setItem('_myDummyKey_', 'myDummyValue'); - localStorage.removeItem('_myDummyKey_'); - return true; - } catch (e) { - return false; - } - } -} - -export interface CreateStoragesConfigInterface { - /** - * Whether to register the Local Storage by default. - * Note that the Local Storage is only available in a web environment. - * @default false - */ - localStorage?: boolean; - /** - * Key/Name identifier of the default Storage. - * - * The default Storage represents the default Storage of the Storages Class, - * on which executed actions are performed if no specific Storage was specified. - * - * Also, the persisted value is loaded from the default Storage by default, - * since only one persisted value can be applied. - * If the loading of the value from the default Storage failed, - * an attempt is made to load the value from the remaining Storages. - * - * @default undefined - */ - defaultStorageKey?: StorageKey; -} - -export interface StoragesConfigInterface { - /** - * Key/Name identifier of the default Storage. - * - * The default Storage represents the default Storage of the Storages Class, - * on which executed actions are performed if no specific Storage was specified. - * - * Also, the persisted value is loaded from the default Storage by default, - * since only one persisted value can be applied. - * If the loading of the value from the default Storage failed, - * an attempt is made to load the value from the remaining Storages. - * - * @default undefined - */ - defaultStorageKey: StorageKey | null; -} - -export interface RegisterConfigInterface { - /** - * Whether the to register Storage should become the default Storage. - * - * The default Storage represents the default Storage of the Storages Class, - * on which executed actions are performed if no specific Storage was specified. - * - * Also, the persisted value is loaded from the default Storage by default, - * since only one persisted value can be applied. - * If the loading of the value from the default Storage failed, - * an attempt is made to load the value from the remaining Storages. - * - * @default false - */ - default?: boolean; +import { CreateStorageConfigInterface, Storage } from '../internal'; + +export * from './storages'; +// export * from './storage'; +// export * from './persistent'; + +/** + * Returns a newly created Storage. + * + * A Storage Class serves as an interface to external storages, + * such as the [Async Storage](https://github.com/react-native-async-storage/async-storage) or + * [Local Storage](https://www.w3schools.com/html/html5_webstorage.asp). + * + * It creates the foundation to easily [`persist()`](https://agile-ts.org/docs/core/state/methods#persist) [Agile Sub Instances](https://agile-ts.org/docs/introduction/#agile-sub-instance) + * (like States or Collections) in nearly any external storage. + * + * [Learn more..](https://agile-ts.org/docs/core/agile-instance/methods#createstorage) + * + * @public + * @param config - Configuration object + */ +export function createStorage(config: CreateStorageConfigInterface): Storage { + return new Storage(config); } diff --git a/packages/core/src/storages/storages.ts b/packages/core/src/storages/storages.ts new file mode 100644 index 00000000..498f9bb5 --- /dev/null +++ b/packages/core/src/storages/storages.ts @@ -0,0 +1,333 @@ +import { + Agile, + Storage, + Persistent, + StorageKey, + StorageItemKey, + notEqual, + LogCodeManager, + defineConfig, +} from '../internal'; + +export class Storages { + // Agile Instance the Storages belongs to + public agileInstance: () => Agile; + + public config: StoragesConfigInterface; + + // Registered Storages + public storages: { [key: string]: Storage } = {}; + // Persistent from Instances (for example States) that were persisted + public persistentInstances: Set = new Set(); + + /** + * The Storages Class manages all external Storages for an Agile Instance + * and provides an interface to easily store, + * load and remove values from multiple Storages at once. + * + * @internal + * @param agileInstance - Instance of Agile the Storages belongs to. + * @param config - Configuration object + */ + constructor( + agileInstance: Agile, + config: CreateStoragesConfigInterface = {} + ) { + this.agileInstance = () => agileInstance; + config = defineConfig(config, { + localStorage: false, + defaultStorageKey: null as any, + }); + this.config = { defaultStorageKey: config.defaultStorageKey as any }; + if (config.localStorage) this.instantiateLocalStorage(); + } + + /** + * Instantiates and registers the + * [Local Storage](https://developer.mozilla.org/de/docs/Web/API/Window/localStorage). + * + * Note that the Local Storage is only available in a web environment. + * + * @internal + */ + public instantiateLocalStorage(): boolean { + if (!Storages.localStorageAvailable()) { + LogCodeManager.log('11:02:00'); + return false; + } + const _localStorage = new Storage({ + key: 'localStorage', + async: false, + methods: { + get: localStorage.getItem.bind(localStorage), + set: localStorage.setItem.bind(localStorage), + remove: localStorage.removeItem.bind(localStorage), + }, + }); + return this.register(_localStorage, { default: true }); + } + + /** + * Registers the specified Storage with AgileTs + * and updates the Persistent Instances that have already attempted + * to use the previously unregistered Storage. + * + * @public + * @param storage - Storage to be registered with AgileTs. + * @param config - Configuration object + */ + public register( + storage: Storage, + config: RegisterConfigInterface = {} + ): boolean { + const hasRegisteredAnyStorage = notEqual(this.storages, {}); + + // Check if Storage already exists + if (Object.prototype.hasOwnProperty.call(this.storages, storage.key)) { + LogCodeManager.log('11:03:00', [storage.key]); + return false; + } + + // Assign Storage as default Storage if it is the first one added + if (!hasRegisteredAnyStorage && config.default === false) + LogCodeManager.log('11:02:01'); + if (!hasRegisteredAnyStorage) config.default = true; + + // Register Storage + this.storages[storage.key] = storage; + if (config.default) this.config.defaultStorageKey = storage.key; + + this.persistentInstances.forEach((persistent) => { + // 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; + } + + // If Persistent has no default Storage key, + // reassign Storage keys since the now registered Storage + // might be tagged as default Storage of AgileTs + if (persistent.config.defaultStorageKey == null) { + persistent.assignStorageKeys(); + const isValid = persistent.validatePersistent(); + if (isValid) persistent.initialLoading(); + } + }); + + LogCodeManager.log('13:00:00', [storage.key], storage); + + return true; + } + + /** + * Retrieves a single Storage with the specified key/name identifier + * from the Storages Class. + * + * If the to retrieve Storage doesn't exist, `undefined` is returned. + * + * @public + * @param storageKey - Key/Name identifier of the Storage. + */ + public getStorage( + storageKey: StorageKey | undefined | null + ): Storage | undefined { + if (!storageKey) return undefined; + const storage = this.storages[storageKey]; + if (!storage) { + LogCodeManager.log('11:03:01', [storageKey]); + return undefined; + } + if (!storage.ready) { + LogCodeManager.log('11:03:02', [storageKey]); + return undefined; + } + return storage; + } + + /** + * Retrieves the stored value at the specified Storage Item key + * from the defined external Storage (`storageKey`). + * + * When no Storage has been specified, + * the value is retrieved from the default Storage. + * + * @public + * @param storageItemKey - Key/Name identifier of the value to be retrieved. + * @param storageKey - Key/Name identifier of the external Storage + * from which the value is to be retrieved. + */ + public get( + storageItemKey: StorageItemKey, + storageKey?: StorageKey + ): Promise { + if (!this.hasStorage()) { + LogCodeManager.log('11:03:03'); + return Promise.resolve(undefined); + } + + // Call get method on specified Storage + if (storageKey) { + const storage = this.getStorage(storageKey); + if (storage) return storage.get(storageItemKey); + } + + // Call get method on default Storage + const defaultStorage = this.getStorage(this.config.defaultStorageKey); + return ( + defaultStorage?.get(storageItemKey) || Promise.resolve(undefined) + ); + } + + /** + * Stores or updates the value at the specified Storage Item key + * in the defined external Storages (`storageKeys`). + * + * When no Storage has been specified, + * the value is stored/updated in the default Storage. + * + * @public + * @param storageItemKey - Key/Name identifier of the value to be stored. + * @param value - Value to be stored in an external Storage. + * @param storageKeys - Key/Name identifier of the external Storage + * where the value is to be stored. + */ + public set( + storageItemKey: StorageItemKey, + value: any, + storageKeys?: StorageKey[] + ): void { + if (!this.hasStorage()) { + LogCodeManager.log('11:03:04'); + return; + } + + // Call set method on specified Storages + if (storageKeys != null) { + for (const storageKey of storageKeys) + this.getStorage(storageKey)?.set(storageItemKey, value); + return; + } + + // Call set method on default Storage + const defaultStorage = this.getStorage(this.config.defaultStorageKey); + defaultStorage?.set(storageItemKey, value); + } + + /** + * Removes the value at the specified Storage Item key + * from the defined external Storages (`storageKeys`). + * + * When no Storage has been specified, + * the value is removed from the default Storage. + * + * @public + * @param storageItemKey - Key/Name identifier of the value to be removed. + * @param storageKeys - Key/Name identifier of the external Storage + * from which the value is to be removed. + */ + public remove( + storageItemKey: StorageItemKey, + storageKeys?: StorageKey[] + ): void { + if (!this.hasStorage()) { + LogCodeManager.log('11:03:05'); + return; + } + + // Call remove method on specified Storages + if (storageKeys) { + for (const storageKey of storageKeys) + this.getStorage(storageKey)?.remove(storageItemKey); + return; + } + + // Call remove method on default Storage + const defaultStorage = this.getStorage(this.config.defaultStorageKey); + defaultStorage?.remove(storageItemKey); + } + + /** + * Returns a boolean indicating whether any Storage + * has been registered with the Agile Instance or not. + * + * @public + */ + public hasStorage(): boolean { + return notEqual(this.storages, {}); + } + + /** + * Returns a boolean indication whether the + * [Local Storage](https://developer.mozilla.org/de/docs/Web/API/Window/localStorage) + * is available in the current environment. + * + * @public + */ + static localStorageAvailable(): boolean { + try { + localStorage.setItem('_myDummyKey_', 'myDummyValue'); + localStorage.removeItem('_myDummyKey_'); + return true; + } catch (e) { + return false; + } + } +} + +export interface CreateStoragesConfigInterface { + /** + * Whether to register the Local Storage by default. + * Note that the Local Storage is only available in a web environment. + * @default false + */ + localStorage?: boolean; + /** + * Key/Name identifier of the default Storage. + * + * The default Storage represents the default Storage of the Storages Class, + * on which executed actions are performed if no specific Storage was specified. + * + * Also, the persisted value is loaded from the default Storage by default, + * since only one persisted value can be applied. + * If the loading of the value from the default Storage failed, + * an attempt is made to load the value from the remaining Storages. + * + * @default undefined + */ + defaultStorageKey?: StorageKey; +} + +export interface StoragesConfigInterface { + /** + * Key/Name identifier of the default Storage. + * + * The default Storage represents the default Storage of the Storages Class, + * on which executed actions are performed if no specific Storage was specified. + * + * Also, the persisted value is loaded from the default Storage by default, + * since only one persisted value can be applied. + * If the loading of the value from the default Storage failed, + * an attempt is made to load the value from the remaining Storages. + * + * @default undefined + */ + defaultStorageKey: StorageKey | null; +} + +export interface RegisterConfigInterface { + /** + * Whether the to register Storage should become the default Storage. + * + * The default Storage represents the default Storage of the Storages Class, + * on which executed actions are performed if no specific Storage was specified. + * + * Also, the persisted value is loaded from the default Storage by default, + * since only one persisted value can be applied. + * If the loading of the value from the default Storage failed, + * an attempt is made to load the value from the remaining Storages. + * + * @default false + */ + default?: boolean; +} diff --git a/packages/core/tests/unit/agile.test.ts b/packages/core/tests/unit/agile.test.ts index c9e3ee21..ee68d7ee 100644 --- a/packages/core/tests/unit/agile.test.ts +++ b/packages/core/tests/unit/agile.test.ts @@ -1,17 +1,13 @@ import { Agile, - State, Runtime, SubController, Integrations, Storage, - Computed, - Collection, Storages, } from '../../src'; import testIntegration from '../helper/test.integration'; import { LogMock } from '../helper/logMock'; -import * as Shared from '../../src/shared'; // https://github.com/facebook/jest/issues/5023 jest.mock('../../src/runtime', () => { diff --git a/packages/core/tests/unit/shared.test.ts b/packages/core/tests/unit/shared.test.ts index fc81e6e8..e93d0d97 100644 --- a/packages/core/tests/unit/shared.test.ts +++ b/packages/core/tests/unit/shared.test.ts @@ -14,11 +14,11 @@ import { import { LogMock } from '../helper/logMock'; jest.mock('../../src/storages/storage'); -jest.mock('../../src/collection'); -jest.mock('../../src/computed'); +jest.mock('../../src/collection/collection'); +jest.mock('../../src/computed/computed'); // https://github.com/facebook/jest/issues/5023 -jest.mock('../../src/state', () => { +jest.mock('../../src/state/state', () => { return { State: jest.fn(), }; From 458d66582594d890d2da8d93ec78fd25124963c8 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Wed, 4 Aug 2021 07:00:56 +0200 Subject: [PATCH 16/93] tree shaking logger in prod --- .../react/develop/simple-counter/package.json | 5 +- .../react/develop/simple-counter/yarn.lock | 5 ++ packages/core/src/logCodeManager.ts | 65 ++++++++++++------- 3 files changed, 50 insertions(+), 25 deletions(-) diff --git a/examples/react/develop/simple-counter/package.json b/examples/react/develop/simple-counter/package.json index ecaceccb..b8878d88 100644 --- a/examples/react/develop/simple-counter/package.json +++ b/examples/react/develop/simple-counter/package.json @@ -4,6 +4,7 @@ "private": true, "dependencies": { "@agile-ts/core": "file:.yalc/@agile-ts/core", + "@agile-ts/logger": "file:.yalc/@agile-ts/logger", "@agile-ts/react": "file:.yalc/@agile-ts/react", "react": "17.0.2", "react-dom": "17.0.2", @@ -18,8 +19,8 @@ "test": "react-scripts test", "eject": "react-scripts eject", "analyze": "yarn run build && source-map-explorer 'build/static/js/*.js'", - "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 @agile-ts/react @agile-ts/logger & yarn install", + "install:prod:agile": "yarn add @agile-ts/core @agile-ts/react @agile-ts/logger & yarn install" }, "browserslist": { "production": [ diff --git a/examples/react/develop/simple-counter/yarn.lock b/examples/react/develop/simple-counter/yarn.lock index bb661d94..9ca92cf9 100644 --- a/examples/react/develop/simple-counter/yarn.lock +++ b/examples/react/develop/simple-counter/yarn.lock @@ -7,6 +7,11 @@ dependencies: "@agile-ts/utils" "^0.0.7" +"@agile-ts/logger@file:.yalc/@agile-ts/logger": + version "0.0.7" + dependencies: + "@agile-ts/utils" "^0.0.7" + "@agile-ts/react@file:.yalc/@agile-ts/react": version "0.1.2" diff --git a/packages/core/src/logCodeManager.ts b/packages/core/src/logCodeManager.ts index eedd445f..cd24f598 100644 --- a/packages/core/src/logCodeManager.ts +++ b/packages/core/src/logCodeManager.ts @@ -1,11 +1,3 @@ -// TODO https://stackoverflow.com/questions/68148235/require-module-inside-a-function-doesnt-work -export let loggerPackage: any = null; -try { - loggerPackage = require('@agile-ts/logger'); -} catch (e) { - // empty catch block -} - // The Log Code Manager keeps track // and manages all important Logs of AgileTs. // @@ -31,7 +23,7 @@ const logCodeTypes = { // --- // 00:00:|00| third digits are based on the Log Message (ascending counted) -const logCodeMessages = { +const niceLogCodeMessages = { // Agile '10:00:00': 'Created new AgileInstance.', '10:02:00': @@ -171,6 +163,9 @@ const logCodeMessages = { '00:03:01': "'${0}' has to be of the type ${1}!", }; +const logCodeMessages: typeof niceLogCodeMessages = + process.env.NODE_ENV === 'dev' ? niceLogCodeMessages : ({} as any); + /** * Returns the log message according to the specified log code. * @@ -183,7 +178,8 @@ function getLog>( logCode: T, replacers: any[] = [] ): string { - let result = logCodeMessages[logCode] ?? `'${logCode}' is a unknown logCode!`; + let result = logCodeMessages[logCode]; + if (result == null) return logCode; // Replace '${x}' with the specified replacer instances for (let i = 0; i < replacers.length; i++) { @@ -255,25 +251,48 @@ 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. * * @internal */ -export const LogCodeManager = { - getLog, - log, - logCodeLogTypes: logCodeTypes, - logCodeMessages: logCodeMessages, - // Not doing 'logger: loggerPackage?.sharedAgileLogger' - // because only by calling a function (now 'getLogger()') the 'sharedLogger' is refetched - getLogger: () => { - return loggerPackage?.sharedAgileLogger ?? null; - }, - logIfTags, -}; +let tempLogCodeManager; +if (process.env.NODE_ENV === 'dev') { + 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; + }, + logIfTags, + }; +} else { + tempLogCodeManager = { + 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 + getLogger: () => { + return null; + }, + logIfTags: (tags, logCode, replacers) => log(logCode, replacers), + }; +} +export const LogCodeManager = tempLogCodeManager; export type LogCodesArrayType = { [K in keyof T]: T[K] extends string ? K : never; From fafdfa545c5b64aaff139f8ba869054ad6e5fb98 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Wed, 4 Aug 2021 20:47:19 +0200 Subject: [PATCH 17/93] created treeshaking example --- .../1000fields/bench/agilets/collection.tsx | 4 +- .../1000fields/bench/agilets/nestedState.tsx | 4 +- .../react/1000fields/bench/agilets/state.tsx | 4 +- .../computed/bench/agilets/autoTracking.tsx | 3 +- .../computed/bench/agilets/hardCoded.tsx | 3 +- .../react/counter/bench/agilets.tsx | 3 +- benchmark/benchmarks/react/counter/index.ts | 20 +-- benchmark/run.ts | 33 ++++- .../react/develop/simple-counter/package.json | 7 +- .../react/develop/simple-counter/src/index.js | 12 +- .../src/{App.js => state-manager/Agile.js} | 6 +- .../simple-counter/src/state-manager/Jotai.js | 45 +++++++ .../src/state-manager/NanoStores.js | 55 ++++++++ .../src/state-manager/Recoil.js | 48 +++++++ .../src/state-manager/ReduxToolkit.js | 98 ++++++++++++++ .../react/develop/simple-counter/yarn.lock | 124 +++++++++++++++++- 16 files changed, 431 insertions(+), 38 deletions(-) rename examples/react/develop/simple-counter/src/{App.js => state-manager/Agile.js} (92%) create mode 100644 examples/react/develop/simple-counter/src/state-manager/Jotai.js create mode 100644 examples/react/develop/simple-counter/src/state-manager/NanoStores.js create mode 100644 examples/react/develop/simple-counter/src/state-manager/Recoil.js create mode 100644 examples/react/develop/simple-counter/src/state-manager/ReduxToolkit.js diff --git a/benchmark/benchmarks/react/1000fields/bench/agilets/collection.tsx b/benchmark/benchmarks/react/1000fields/bench/agilets/collection.tsx index 7269c0a0..f30fcef4 100644 --- a/benchmark/benchmarks/react/1000fields/bench/agilets/collection.tsx +++ b/benchmark/benchmarks/react/1000fields/bench/agilets/collection.tsx @@ -1,10 +1,8 @@ import React from 'react'; import ReactDom from 'react-dom'; -import { createCollection, LogCodeManager } from '@agile-ts/core'; +import { createCollection } from '@agile-ts/core'; import { useAgile, useValue } from '@agile-ts/react'; -LogCodeManager.getLogger().isActive = false; - export default function (target: HTMLElement, fieldsCount: number) { const FIELDS = createCollection({ initialData: Array.from(Array(fieldsCount).keys()).map((i) => ({ diff --git a/benchmark/benchmarks/react/1000fields/bench/agilets/nestedState.tsx b/benchmark/benchmarks/react/1000fields/bench/agilets/nestedState.tsx index 377b6b01..88fe5ba6 100644 --- a/benchmark/benchmarks/react/1000fields/bench/agilets/nestedState.tsx +++ b/benchmark/benchmarks/react/1000fields/bench/agilets/nestedState.tsx @@ -1,10 +1,8 @@ import * as React from 'react'; import * as ReactDom from 'react-dom'; -import { createState, LogCodeManager, State } from '@agile-ts/core'; +import { createState, State } from '@agile-ts/core'; import { useAgile } from '@agile-ts/react'; -LogCodeManager.getLogger().isActive = false; - export default function (target: HTMLElement, fieldsCount: number) { const FIELDS = createState( Array.from(Array(fieldsCount).keys()).map((i) => diff --git a/benchmark/benchmarks/react/1000fields/bench/agilets/state.tsx b/benchmark/benchmarks/react/1000fields/bench/agilets/state.tsx index 93f9798a..08069b33 100644 --- a/benchmark/benchmarks/react/1000fields/bench/agilets/state.tsx +++ b/benchmark/benchmarks/react/1000fields/bench/agilets/state.tsx @@ -1,10 +1,8 @@ import React from 'react'; import ReactDom from 'react-dom'; -import { createState, LogCodeManager } from '@agile-ts/core'; +import { createState } from '@agile-ts/core'; import { useAgile } from '@agile-ts/react'; -LogCodeManager.getLogger().isActive = false; - export default function (target: HTMLElement, fieldsCount: number) { const FIELDS = createState( Array.from(Array(fieldsCount).keys()).map((i) => `Field #${i + 1}`) diff --git a/benchmark/benchmarks/react/computed/bench/agilets/autoTracking.tsx b/benchmark/benchmarks/react/computed/bench/agilets/autoTracking.tsx index 0b596405..df85d176 100644 --- a/benchmark/benchmarks/react/computed/bench/agilets/autoTracking.tsx +++ b/benchmark/benchmarks/react/computed/bench/agilets/autoTracking.tsx @@ -1,9 +1,8 @@ import React from 'react'; import ReactDom from 'react-dom'; -import { createComputed, createState, LogCodeManager } from '@agile-ts/core'; +import { createComputed, createState } from '@agile-ts/core'; import { useAgile } from '@agile-ts/react'; -LogCodeManager.getLogger().isActive = false; const COUNT = createState(0); const COMPUTED_COUNT = createComputed(() => { return COUNT.value * 5; diff --git a/benchmark/benchmarks/react/computed/bench/agilets/hardCoded.tsx b/benchmark/benchmarks/react/computed/bench/agilets/hardCoded.tsx index 59cc57a8..ac782b55 100644 --- a/benchmark/benchmarks/react/computed/bench/agilets/hardCoded.tsx +++ b/benchmark/benchmarks/react/computed/bench/agilets/hardCoded.tsx @@ -1,9 +1,8 @@ import React from 'react'; import ReactDom from 'react-dom'; -import { createComputed, createState, LogCodeManager } from '@agile-ts/core'; +import { createComputed, createState } from '@agile-ts/core'; import { useAgile } from '@agile-ts/react'; -LogCodeManager.getLogger().isActive = false; const COUNT = createState(0); const COMPUTED_COUNT = createComputed( () => { diff --git a/benchmark/benchmarks/react/counter/bench/agilets.tsx b/benchmark/benchmarks/react/counter/bench/agilets.tsx index 309b2f2a..e4e8ddb1 100644 --- a/benchmark/benchmarks/react/counter/bench/agilets.tsx +++ b/benchmark/benchmarks/react/counter/bench/agilets.tsx @@ -1,9 +1,8 @@ import React from 'react'; import ReactDom from 'react-dom'; -import { createState, LogCodeManager } from '@agile-ts/core'; +import { createState } from '@agile-ts/core'; import { useAgile } from '@agile-ts/react'; -LogCodeManager.getLogger().isActive = false; const COUNT = createState(0); const App = () => { diff --git a/benchmark/benchmarks/react/counter/index.ts b/benchmark/benchmarks/react/counter/index.ts index f3e2e78f..067a85de 100644 --- a/benchmark/benchmarks/react/counter/index.ts +++ b/benchmark/benchmarks/react/counter/index.ts @@ -64,16 +64,16 @@ const results: CycleResultInterface[] = []; // Add Tests to the Benchmark Test Suite suite .add('AgileTs', configTest(agilets)) - .add('Hookstate', configTest(hookstate)) - .add('Jotai', configTest(jotai)) - .add('Mobx', configTest(mobx)) - .add('Nano Stores', configTest(nanostores)) - .add('PulseJs', configTest(pulsejs)) - .add('Recoil', configTest(recoil)) - .add('Redux', configTest(redux)) - .add('Redux-Toolkit', configTest(reduxToolkit)) - .add('Valtio', configTest(valtio)) - .add('Zustand', configTest(zustand)) + // .add('Hookstate', configTest(hookstate)) + // .add('Jotai', configTest(jotai)) + // .add('Mobx', configTest(mobx)) + // .add('Nano Stores', configTest(nanostores)) + // .add('PulseJs', configTest(pulsejs)) + // .add('Recoil', configTest(recoil)) + // .add('Redux', configTest(redux)) + // .add('Redux-Toolkit', configTest(reduxToolkit)) + // .add('Valtio', configTest(valtio)) + // .add('Zustand', configTest(zustand)) // Add Listener .on('start', function (this: any) { diff --git a/benchmark/run.ts b/benchmark/run.ts index 3ce2b57a..a0583978 100644 --- a/benchmark/run.ts +++ b/benchmark/run.ts @@ -2,23 +2,27 @@ import dotenv from 'dotenv'; import esbuild from 'esbuild'; import playwright from 'playwright'; import chalk from 'chalk'; +import fs from 'fs'; // Loads environment variables from the '.env' file dotenv.config(); +// TODO implement yargs https://yargs.js.org/ + // https://nodejs.org/docs/latest/api/process.html#process_process_argv // Extract entry (at third parameter) from the executed command // yarn run ./path/to/entry -> './path/to/entry' is extracted const entry = process.argv.slice(2)[0]; -const dev = process.argv.slice(2)[1] === '--dev' || process.env.DEV === 'true'; +const isDev = + process.argv.slice(2)[1] === '--dev' || process.env.DEV === 'true'; if (entry == null) { throw new Error( "No valid entry was provided! Valid entry example: 'yarn run ./benchmarks/react/counter'" ); } -const startBenchmark = async () => { - console.log(chalk.blue('Starting the benchmark server..\n')); +const startSpeedBench = async () => { + console.log(chalk.blue('Starting the speed benchmark server..\n')); // Bundle Benchmark Test Suite // and launch the server on which the Test Suite is executed @@ -35,7 +39,7 @@ const startBenchmark = async () => { target: 'es2015', format: 'cjs', // https://esbuild.github.io/api/#format-commonjs platform: 'browser', - minify: !dev, // https://esbuild.github.io/api/#minify + minify: !isDev, // https://esbuild.github.io/api/#minify bundle: true, // https://esbuild.github.io/api/#bundle sourcemap: 'external', // https://esbuild.github.io/api/#sourcemap// https://github.com/evanw/esbuild/issues/69 } @@ -54,7 +58,7 @@ const startBenchmark = async () => { const page = await context.newPage(); // Option to open and test the Benchmark Test Suite in the browser manually - if (dev) { + if (isDev) { console.log( `${chalk.blue('[i]')} ${chalk.gray( `Development mode is ${chalk.green(`active`)}` @@ -113,5 +117,22 @@ const startBenchmark = async () => { server.stop(); }; +const startBundleBench = async () => { + const bundle = await esbuild.build({ + inject: ['./lodash.ts'], // https://esbuild.github.io/api/#inject + entryPoints: [entry], // https://esbuild.github.io/api/#entry-points + outfile: './public/bundle.js', + target: 'es2015', + format: 'cjs', // https://esbuild.github.io/api/#format-commonjs + platform: 'browser', + minify: !isDev, // https://esbuild.github.io/api/#minify + bundle: true, // https://esbuild.github.io/api/#bundle + sourcemap: 'external', // https://esbuild.github.io/api/#sourcemap// https://github.com/evanw/esbuild/issues/69 + metafile: true, // https://esbuild.github.io/api/#metafile + }); + + fs; +}; + // Execute the Benchmark -startBenchmark(); +startSpeedBench(); diff --git a/examples/react/develop/simple-counter/package.json b/examples/react/develop/simple-counter/package.json index b8878d88..eebd90c7 100644 --- a/examples/react/develop/simple-counter/package.json +++ b/examples/react/develop/simple-counter/package.json @@ -6,9 +6,14 @@ "@agile-ts/core": "file:.yalc/@agile-ts/core", "@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", "react": "17.0.2", "react-dom": "17.0.2", - "react-scripts": "4.0.0" + "react-redux": "^7.2.4", + "react-scripts": "4.0.0", + "recoil": "^0.4.0" }, "devDependencies": { "source-map-explorer": "^2.5.2" diff --git a/examples/react/develop/simple-counter/src/index.js b/examples/react/develop/simple-counter/src/index.js index dd1c38f9..ae2db727 100644 --- a/examples/react/develop/simple-counter/src/index.js +++ b/examples/react/develop/simple-counter/src/index.js @@ -1,11 +1,19 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import App from './App'; +import * as Agile from './state-manager/Agile'; +import * as Jotai from './state-manager/Jotai'; +import * as NanoStores from './state-manager/NanoStores'; +import * as Recoil from './state-manager/Recoil'; +import * as ReduxToolkit from './state-manager/ReduxToolkit'; ReactDOM.render( - + + + + + , document.getElementById('root') ); diff --git a/examples/react/develop/simple-counter/src/App.js b/examples/react/develop/simple-counter/src/state-manager/Agile.js similarity index 92% rename from examples/react/develop/simple-counter/src/App.js rename to examples/react/develop/simple-counter/src/state-manager/Agile.js index 21bdfe55..e86b199e 100644 --- a/examples/react/develop/simple-counter/src/App.js +++ b/examples/react/develop/simple-counter/src/state-manager/Agile.js @@ -1,3 +1,4 @@ +import React from 'react'; import { createState } from '@agile-ts/core'; import { useAgile, useValue } from '@agile-ts/react'; @@ -32,8 +33,9 @@ const CounterC = () => { ); }; -const App = () => ( +export const App = () => (
+

Agile

@@ -42,5 +44,3 @@ const App = () => (
); - -export default App; diff --git a/examples/react/develop/simple-counter/src/state-manager/Jotai.js b/examples/react/develop/simple-counter/src/state-manager/Jotai.js new file mode 100644 index 00000000..093bb7b1 --- /dev/null +++ b/examples/react/develop/simple-counter/src/state-manager/Jotai.js @@ -0,0 +1,45 @@ +import React from 'react'; +import { atom, useAtom } from 'jotai'; + +const COUNTER_A = atom(1); +const COUNTER_B = atom(2); +const COUNTER_C = atom(3); + +const CounterA = () => { + const [count, setCount] = useAtom(COUNTER_A); + return ( +
+ A: {count} +
+ ); +}; + +const CounterB = () => { + const [count, setCount] = useAtom(COUNTER_B); + return ( +
+ B: {count} +
+ ); +}; + +const CounterC = () => { + const [count, setCount] = useAtom(COUNTER_C); + return ( +
+ C: {count} +
+ ); +}; + +export const App = () => ( +
+

Jotai

+ + + + + + +
+); diff --git a/examples/react/develop/simple-counter/src/state-manager/NanoStores.js b/examples/react/develop/simple-counter/src/state-manager/NanoStores.js new file mode 100644 index 00000000..f62e8067 --- /dev/null +++ b/examples/react/develop/simple-counter/src/state-manager/NanoStores.js @@ -0,0 +1,55 @@ +import React from 'react'; +import { createStore, update } from 'nanostores'; +import { useStore } from 'nanostores/react'; + +const COUNTER_A = createStore(() => { + COUNTER_A.set(1); +}); +const COUNTER_B = createStore(() => { + COUNTER_B.set(1); +}); +const COUNTER_C = createStore(() => { + COUNTER_C.set(1); +}); + +const CounterA = () => { + const count = useStore(COUNTER_A); + return ( +
+ A: {count}{' '} + +
+ ); +}; + +const CounterB = () => { + const count = useStore(COUNTER_B); + return ( +
+ B: {count}{' '} + +
+ ); +}; + +const CounterC = () => { + const count = useStore(COUNTER_C); + return ( +
+ C: {count}{' '} + +
+ ); +}; + +export const App = () => ( +
+

Nano Stores

+ + + + + + +
+); diff --git a/examples/react/develop/simple-counter/src/state-manager/Recoil.js b/examples/react/develop/simple-counter/src/state-manager/Recoil.js new file mode 100644 index 00000000..04af2cd9 --- /dev/null +++ b/examples/react/develop/simple-counter/src/state-manager/Recoil.js @@ -0,0 +1,48 @@ +import React from 'react'; +import { atom, RecoilRoot, useRecoilState } from 'recoil'; + +const COUNTER_A = atom({ + key: 'counterA', + default: 1, +}); +const COUNTER_B = atom({ key: 'counterB', default: 2 }); +const COUNTER_C = atom({ key: 'counterC', default: 3 }); + +const CounterA = () => { + const [count, setCount] = useRecoilState(COUNTER_A); + return ( +
+ A: {count} +
+ ); +}; + +const CounterB = () => { + const [count, setCount] = useRecoilState(COUNTER_B); + return ( +
+ B: {count} +
+ ); +}; + +const CounterC = () => { + const [count, setCount] = useRecoilState(COUNTER_C); + return ( +
+ C: {count} +
+ ); +}; + +export const App = () => ( + +

Recoil

+ + + + + + +
+); diff --git a/examples/react/develop/simple-counter/src/state-manager/ReduxToolkit.js b/examples/react/develop/simple-counter/src/state-manager/ReduxToolkit.js new file mode 100644 index 00000000..ae022656 --- /dev/null +++ b/examples/react/develop/simple-counter/src/state-manager/ReduxToolkit.js @@ -0,0 +1,98 @@ +import React from 'react'; +import { configureStore, createSlice } from '@reduxjs/toolkit'; +import { Provider, useSelector, useDispatch } from 'react-redux'; + +const counterSlice_A = createSlice({ + name: 'counterA', + initialState: { + value: 0, + }, + reducers: { + increment: (state) => { + state.value += 1; + }, + }, +}); + +const counterSlice_B = createSlice({ + name: 'counterB', + initialState: { + value: 0, + }, + reducers: { + increment: (state) => { + state.value += 1; + }, + }, +}); + +const counterSlice_C = createSlice({ + name: 'counterC', + initialState: { + value: 0, + }, + reducers: { + increment: (state) => { + state.value += 1; + }, + }, +}); + +const store = configureStore({ + reducer: { + counterA: counterSlice_A.reducer, + counterB: counterSlice_B.reducer, + counterC: counterSlice_C.reducer, + }, +}); + +const CounterA = () => { + const count = useSelector((state) => state.counterA?.value); + const dispatch = useDispatch(); + return ( +
+ A: {count}{' '} + +
+ ); +}; + +const CounterB = () => { + const count = useSelector((state) => state.counterB?.value); + const dispatch = useDispatch(); + return ( +
+ B: {count}{' '} + +
+ ); +}; + +const CounterC = () => { + const count = useSelector((state) => state.counterC?.value); + const dispatch = useDispatch(); + return ( +
+ C: {count}{' '} + +
+ ); +}; + +export const App = () => ( + +

Redux Toolkit

+ + + + + + +
+); diff --git a/examples/react/develop/simple-counter/yarn.lock b/examples/react/develop/simple-counter/yarn.lock index 9ca92cf9..ebaf211d 100644 --- a/examples/react/develop/simple-counter/yarn.lock +++ b/examples/react/develop/simple-counter/yarn.lock @@ -1650,6 +1650,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.12.1", "@babel/runtime@^7.9.2": + version "7.14.8" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.8.tgz#7119a56f421018852694290b9f9148097391b446" + integrity sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.10.4", "@babel/template@^7.12.13", "@babel/template@^7.3.3": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.13.tgz#530265be8a2589dbb37523844c5bcb55947fb327" @@ -2004,6 +2011,16 @@ schema-utils "^2.6.5" source-map "^0.7.3" +"@reduxjs/toolkit@^1.6.1": + version "1.6.1" + resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.6.1.tgz#7bc83b47352a663bf28db01e79d17ba54b98ade9" + integrity sha512-pa3nqclCJaZPAyBhruQtiRwtTjottRrVJqziVZcWzI73i6L3miLTtUyWfauwv08HWtiXLx1xGyGt+yLFfW/d0A== + dependencies: + immer "^9.0.1" + redux "^4.1.0" + redux-thunk "^2.3.0" + reselect "^4.0.0" + "@rollup/plugin-node-resolve@^7.1.1": version "7.1.3" resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-7.1.3.tgz#80de384edfbd7bfc9101164910f86078151a3eca" @@ -2228,6 +2245,14 @@ dependencies: "@types/node" "*" +"@types/hoist-non-react-statics@^3.3.0": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" + integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + "@types/html-minifier-terser@^5.0.0": version "5.1.1" resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz#3c9ee980f1a10d6021ae6632ca3e79ca2ec4fb50" @@ -2287,11 +2312,35 @@ resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.2.1.tgz#374e31645d58cb18a07b3ecd8e9dede4deb2cccd" integrity sha512-DxZZbyMAM9GWEzXL+BMZROWz9oo6A9EilwwOMET2UVu2uZTqMWS5S69KVtuVKaRjCUpcrOXRalet86/OpG4kqw== +"@types/prop-types@*": + version "15.7.4" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11" + integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== + "@types/q@^1.5.1": version "1.5.4" resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24" integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug== +"@types/react-redux@^7.1.16": + version "7.1.18" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.18.tgz#2bf8fd56ebaae679a90ebffe48ff73717c438e04" + integrity sha512-9iwAsPyJ9DLTRH+OFeIrm9cAbIj1i2ANL3sKQFATqnPWRbg+jEFXyZOKHiQK/N86pNRXbb4HRxAxo0SIX1XwzQ== + dependencies: + "@types/hoist-non-react-statics" "^3.3.0" + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + redux "^4.0.0" + +"@types/react@*": + version "17.0.15" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.15.tgz#c7533dc38025677e312606502df7656a6ea626d0" + integrity sha512-uTKHDK9STXFHLaKv6IMnwp52fm0hwU+N89w/p9grdUqcFA6WuqDyPhaWopbNyE1k/VhgzmHl8pu1L4wITtmlLw== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + "@types/resolve@0.0.8": version "0.0.8" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-0.0.8.tgz#f26074d238e02659e323ce1a13d041eee280e194" @@ -2299,6 +2348,11 @@ dependencies: "@types/node" "*" +"@types/scheduler@*": + version "0.16.2" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" + integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== + "@types/source-list-map@*": version "0.1.2" resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" @@ -4382,6 +4436,11 @@ cssstyle@^2.2.0: dependencies: cssom "~0.3.6" +csstype@^3.0.2: + version "3.0.8" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.8.tgz#d2266a792729fb227cd216fb572f43728e1ad340" + integrity sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw== + cyclist@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" @@ -5883,6 +5942,11 @@ gzip-size@^6.0.0: dependencies: duplexer "^0.1.2" +hamt_plus@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/hamt_plus/-/hamt_plus-1.0.2.tgz#e21c252968c7e33b20f6a1b094cd85787a265601" + integrity sha1-4hwlKWjH4zsg9qGwlM2FeHomVgE= + handle-thing@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" @@ -5995,6 +6059,13 @@ hmac-drbg@^1.0.1: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" +hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + hoopy@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/hoopy/-/hoopy-0.1.4.tgz#609207d661100033a9a9402ad3dea677381c1b1d" @@ -6213,6 +6284,11 @@ immer@8.0.1: resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656" integrity sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA== +immer@^9.0.1: + version "9.0.5" + resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.5.tgz#a7154f34fe7064f15f00554cc94c66cc0bf453ec" + integrity sha512-2WuIehr2y4lmYz9gaQzetPR2ECniCifk4ORaQbU3g5EalLt+0IVTosEPJ5BoYl/75ky2mivzdRzV8wWgQGOSYQ== + import-cwd@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" @@ -7178,6 +7254,11 @@ jest@26.6.0: import-local "^3.0.2" jest-cli "^26.6.0" +jotai@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/jotai/-/jotai-1.2.2.tgz#631fd7ad44e9ac26cdf9874d52282c1cfe032807" + integrity sha512-iqkkUdWsH2Mk4HY1biba/8kA77+8liVBy8E0d8Nce29qow4h9mzdDhpTasAruuFYPycw6JvfZgL5RB0JJuIZjw== + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -7893,6 +7974,11 @@ nanomatch@^1.2.9: snapdragon "^0.8.1" to-regex "^3.0.1" +nanostores@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/nanostores/-/nanostores-0.4.1.tgz#61c7a4aadca063bd1992e80f9a0e8b20d55dee0f" + integrity sha512-GfrWjngWVTBa3YSPijFGrxyYYEE017ONA/t6d6X6G99ccfED+eZEIciH+KKCqbvZhvRbqGH0aHDhRYNBwFw8hg== + native-url@^0.2.6: version "0.2.6" resolved "https://registry.yarnpkg.com/native-url/-/native-url-0.2.6.tgz#ca1258f5ace169c716ff44eccbddb674e10399ae" @@ -9556,7 +9642,7 @@ react-error-overlay@^6.0.9: resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a" integrity sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew== -react-is@^16.8.1: +react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -9566,6 +9652,18 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== +react-redux@^7.2.4: + version "7.2.4" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.4.tgz#1ebb474032b72d806de2e0519cd07761e222e225" + integrity sha512-hOQ5eOSkEJEXdpIKbnRyl04LhaWabkDPV+Ix97wqQX3T3d2NQ8DUblNXXtNMavc7DpswyQM6xfaN4HQDKNY2JA== + dependencies: + "@babel/runtime" "^7.12.1" + "@types/react-redux" "^7.1.16" + hoist-non-react-statics "^3.3.2" + loose-envify "^1.4.0" + prop-types "^15.7.2" + react-is "^16.13.1" + react-refresh@^0.8.3: version "0.8.3" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f" @@ -9718,6 +9816,13 @@ readdirp@~3.5.0: dependencies: picomatch "^2.2.1" +recoil@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/recoil/-/recoil-0.4.0.tgz#2a933ba7c043cbf6f50f52da0828a879c7cd7d69" + integrity sha512-FZ2ljI4ldZU820V0APbKOtS4bPwPJHvpDBQEl+Cf47DMaM35wuLXl2u37E0TSgdvKAZBOUsIwcBnzE+ncODRxQ== + dependencies: + hamt_plus "1.0.2" + recursive-readdir@2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f" @@ -9725,6 +9830,18 @@ recursive-readdir@2.2.2: dependencies: minimatch "3.0.4" +redux-thunk@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622" + integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw== + +redux@^4.0.0, redux@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.1.tgz#76f1c439bb42043f985fbd9bf21990e60bd67f47" + integrity sha512-hZQZdDEM25UY2P493kPYuKqviVwZ58lEmGQNeQ+gXa+U0gYPUBf7NKYazbe3m+bs/DzM/ahN12DbF+NG8i0CWw== + dependencies: + "@babel/runtime" "^7.9.2" + regenerate-unicode-properties@^8.2.0: version "8.2.0" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec" @@ -9897,6 +10014,11 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= +reselect@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7" + integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA== + resolve-cwd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" From fcc6b76b3317bd5f0ecfb449e27d5596cd36c707 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Thu, 5 Aug 2021 19:01:01 +0200 Subject: [PATCH 18/93] extract metafile from bundle (benchmark test) --- benchmark/run.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/benchmark/run.ts b/benchmark/run.ts index a0583978..41ab8d9c 100644 --- a/benchmark/run.ts +++ b/benchmark/run.ts @@ -2,7 +2,6 @@ import dotenv from 'dotenv'; import esbuild from 'esbuild'; import playwright from 'playwright'; import chalk from 'chalk'; -import fs from 'fs'; // Loads environment variables from the '.env' file dotenv.config(); @@ -131,7 +130,11 @@ const startBundleBench = async () => { metafile: true, // https://esbuild.github.io/api/#metafile }); - fs; + // Extract metafile from bundle (https://esbuild.github.io/api/#metafile) + const metafile = bundle.metafile; + + console.log(metafile); + // TODO analyze metafile }; // Execute the Benchmark From 123b45e95e3f53c854eca6d28ba114d8711d54df Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sat, 7 Aug 2021 20:26:14 +0200 Subject: [PATCH 19/93] fixed typo --- .../{ => benchmarks}/benchmarkManager.ts | 0 .../benchmarks/react/1000fields/index.ts | 2 +- benchmark/benchmarks/react/computed/index.ts | 2 +- .../react/counter/bench/agilets.tsx | 2 +- benchmark/benchmarks/react/counter/index.ts | 2 +- .../typescript/defineConfig/index.ts | 2 +- benchmark/package.json | 2 +- .../{run.ts => runtime/benchmarkTypes.ts} | 57 +++++++++++-------- benchmark/runtime/run.ts | 22 +++++++ packages/react/src/hooks/useAgile.ts | 2 +- 10 files changed, 62 insertions(+), 31 deletions(-) rename benchmark/{ => benchmarks}/benchmarkManager.ts (100%) rename benchmark/{run.ts => runtime/benchmarkTypes.ts} (82%) create mode 100644 benchmark/runtime/run.ts diff --git a/benchmark/benchmarkManager.ts b/benchmark/benchmarks/benchmarkManager.ts similarity index 100% rename from benchmark/benchmarkManager.ts rename to benchmark/benchmarks/benchmarkManager.ts diff --git a/benchmark/benchmarks/react/1000fields/index.ts b/benchmark/benchmarks/react/1000fields/index.ts index 9b251c1d..9c1385be 100644 --- a/benchmark/benchmarks/react/1000fields/index.ts +++ b/benchmark/benchmarks/react/1000fields/index.ts @@ -6,7 +6,7 @@ import { endBenchmarkLog, getCycleResult, startBenchmarkLog, -} from '../../../benchmarkManager'; +} from '../../benchmarkManager'; // Files to run the Benchmark on import agileCollection from './bench/agilets/collection'; diff --git a/benchmark/benchmarks/react/computed/index.ts b/benchmark/benchmarks/react/computed/index.ts index e70b9c70..eb9d2fed 100644 --- a/benchmark/benchmarks/react/computed/index.ts +++ b/benchmark/benchmarks/react/computed/index.ts @@ -6,7 +6,7 @@ import { endBenchmarkLog, getCycleResult, startBenchmarkLog, -} from '../../../benchmarkManager'; +} from '../../benchmarkManager'; // Files to run the Benchmark on import agileAutoTracking from './bench/agilets/autoTracking'; diff --git a/benchmark/benchmarks/react/counter/bench/agilets.tsx b/benchmark/benchmarks/react/counter/bench/agilets.tsx index e4e8ddb1..7434d9ed 100644 --- a/benchmark/benchmarks/react/counter/bench/agilets.tsx +++ b/benchmark/benchmarks/react/counter/bench/agilets.tsx @@ -6,7 +6,7 @@ import { useAgile } from '@agile-ts/react'; const COUNT = createState(0); const App = () => { - const count = useAgile(COUNT, undefined); + const count = useAgile(COUNT); return

COUNT.set((state) => state + 1)}>{count}

; }; diff --git a/benchmark/benchmarks/react/counter/index.ts b/benchmark/benchmarks/react/counter/index.ts index 067a85de..78be7f50 100644 --- a/benchmark/benchmarks/react/counter/index.ts +++ b/benchmark/benchmarks/react/counter/index.ts @@ -6,7 +6,7 @@ import { endBenchmarkLog, getCycleResult, startBenchmarkLog, -} from '../../../benchmarkManager'; +} from '../../benchmarkManager'; // Files to run the Benchmark on import agilets from './bench/agilets'; diff --git a/benchmark/benchmarks/typescript/defineConfig/index.ts b/benchmark/benchmarks/typescript/defineConfig/index.ts index 29caa427..79846e99 100644 --- a/benchmark/benchmarks/typescript/defineConfig/index.ts +++ b/benchmark/benchmarks/typescript/defineConfig/index.ts @@ -5,7 +5,7 @@ import { endBenchmarkLog, getCycleResult, startBenchmarkLog, -} from '../../../benchmarkManager'; +} from '../../benchmarkManager'; // Files to run the Benchmark on import * as referencer from './bench/referencer'; diff --git a/benchmark/package.json b/benchmark/package.json index d7cf2247..3e852c96 100644 --- a/benchmark/package.json +++ b/benchmark/package.json @@ -7,7 +7,7 @@ "homepage": "https://agile-ts.org/", "description": "Benchmark Tests", "scripts": { - "test": "node -r esbuild-register run.ts", + "test": "node -r esbuild-register runtime/run.ts", "test:counter": "yarn test ./benchmarks/react/counter", "test:1000fields": "yarn test ./benchmarks/react/1000fields", "test:computed": "yarn test ./benchmarks/react/computed", diff --git a/benchmark/run.ts b/benchmark/runtime/benchmarkTypes.ts similarity index 82% rename from benchmark/run.ts rename to benchmark/runtime/benchmarkTypes.ts index 41ab8d9c..abb6b31d 100644 --- a/benchmark/run.ts +++ b/benchmark/runtime/benchmarkTypes.ts @@ -1,26 +1,9 @@ -import dotenv from 'dotenv'; +import chalk from 'chalk'; import esbuild from 'esbuild'; import playwright from 'playwright'; -import chalk from 'chalk'; - -// Loads environment variables from the '.env' file -dotenv.config(); - -// TODO implement yargs https://yargs.js.org/ +import fs from 'fs'; -// https://nodejs.org/docs/latest/api/process.html#process_process_argv -// Extract entry (at third parameter) from the executed command -// yarn run ./path/to/entry -> './path/to/entry' is extracted -const entry = process.argv.slice(2)[0]; -const isDev = - process.argv.slice(2)[1] === '--dev' || process.env.DEV === 'true'; -if (entry == null) { - throw new Error( - "No valid entry was provided! Valid entry example: 'yarn run ./benchmarks/react/counter'" - ); -} - -const startSpeedBench = async () => { +export const startSpeedBench = async (entry: string, isDev: boolean) => { console.log(chalk.blue('Starting the speed benchmark server..\n')); // Bundle Benchmark Test Suite @@ -116,7 +99,7 @@ const startSpeedBench = async () => { server.stop(); }; -const startBundleBench = async () => { +export const startBundleBench = async (entry: string, isDev: boolean) => { const bundle = await esbuild.build({ inject: ['./lodash.ts'], // https://esbuild.github.io/api/#inject entryPoints: [entry], // https://esbuild.github.io/api/#entry-points @@ -130,12 +113,38 @@ const startBundleBench = async () => { metafile: true, // https://esbuild.github.io/api/#metafile }); + console.log( + `${chalk.blue('[i]')} ${chalk.gray( + `Entry was ${chalk.green(`successfully`)} bundled` + )}` + ); + + if (isDev) { + console.log( + `${chalk.blue('[i]')} ${chalk.gray( + `Development mode is ${chalk.green(`active`)}` + )}` + ); + } + // Extract metafile from bundle (https://esbuild.github.io/api/#metafile) const metafile = bundle.metafile; + // Calculate bundle file size + let bundleSize = 0; + bundle.outputFiles?.map((file) => { + const stats = fs.statSync(file.path); + const fileSizeInBytes = stats.size; + const fileSizeInKilobytes = fileSizeInBytes / 1024; + bundleSize += fileSizeInKilobytes; + }); + + console.log( + `${chalk.blue('[i]')} ${chalk.gray( + `Total bundle size of the bundle is ${chalk.blueBright.bold(bundleSize)}` + )}` + ); + console.log(metafile); // TODO analyze metafile }; - -// Execute the Benchmark -startSpeedBench(); diff --git a/benchmark/runtime/run.ts b/benchmark/runtime/run.ts new file mode 100644 index 00000000..39643368 --- /dev/null +++ b/benchmark/runtime/run.ts @@ -0,0 +1,22 @@ +import dotenv from 'dotenv'; +import { startSpeedBench } from './benchmarkTypes'; + +// Loads environment variables from the '.env' file +dotenv.config(); + +// TODO implement yargs https://yargs.js.org/ + +// https://nodejs.org/docs/latest/api/process.html#process_process_argv +// Extract entry (at third parameter) from the executed command +// yarn run ./path/to/entry -> './path/to/entry' is extracted +const entry = process.argv.slice(2)[0]; +const isDev = + process.argv.slice(2)[1] === '--dev' || process.env.DEV === 'true'; +if (entry == null) { + throw new Error( + "No valid entry was provided! Valid entry example: 'yarn run ./benchmarks/react/counter'" + ); +} + +// Execute the Benchmark +startSpeedBench(entry, isDev); diff --git a/packages/react/src/hooks/useAgile.ts b/packages/react/src/hooks/useAgile.ts index 1cb215cc..56d51419 100644 --- a/packages/react/src/hooks/useAgile.ts +++ b/packages/react/src/hooks/useAgile.ts @@ -109,7 +109,7 @@ export function useAgile< // 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 is adjusted in the useSelector hook) + // however the type can be adjusted in the useSelector hook) if (config.selector && isValidObject(value, true)) { return config.selector(value); } From 809efa4f833889394ed89987658f5d20961a878f Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sat, 7 Aug 2021 21:31:37 +0200 Subject: [PATCH 20/93] implemented yargs --- benchmark/package.json | 4 +- benchmark/runtime/run.ts | 51 ++++++++++++++--- benchmark/yarn.lock | 115 ++++++++++++++++++++++++++++++++++++--- 3 files changed, 152 insertions(+), 18 deletions(-) diff --git a/benchmark/package.json b/benchmark/package.json index 3e852c96..52dcd5bf 100644 --- a/benchmark/package.json +++ b/benchmark/package.json @@ -45,13 +45,15 @@ "redux": "^4.1.0", "typescript": "^4.3.5", "valtio": "^1.0.6", + "yargs": "^17.1.0", "zustand": "^3.5.5" }, "devDependencies": { "@types/benchmark": "^2.1.0", "@types/node": "^16.0.0", "@types/react": "^17.0.13", - "@types/react-dom": "^17.0.8" + "@types/react-dom": "^17.0.8", + "@types/yargs": "^17.0.2" }, "bugs": { "url": "https://github.com/agile-ts/agile/issues" diff --git a/benchmark/runtime/run.ts b/benchmark/runtime/run.ts index 39643368..254d6ed4 100644 --- a/benchmark/runtime/run.ts +++ b/benchmark/runtime/run.ts @@ -1,22 +1,55 @@ import dotenv from 'dotenv'; -import { startSpeedBench } from './benchmarkTypes'; +import { startBundleBench, startSpeedBench } from './benchmarkTypes'; +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; // Loads environment variables from the '.env' file dotenv.config(); // TODO implement yargs https://yargs.js.org/ -// https://nodejs.org/docs/latest/api/process.html#process_process_argv -// Extract entry (at third parameter) from the executed command -// yarn run ./path/to/entry -> './path/to/entry' is extracted -const entry = process.argv.slice(2)[0]; -const isDev = - process.argv.slice(2)[1] === '--dev' || process.env.DEV === 'true'; +// hideBind handles the 'process.argv.slice' logic +const argv = yargs(hideBin(process.argv)) + .option('_', { + type: 'string', + default: ['./benchmarks/react/counter'], + description: 'What benchmark to execute', + }) + .option('dev', { + type: 'boolean', + default: false, + description: + 'Whether to start the benchmark/s in developer mode for better debugging.', + }) + .option('type', { + type: 'string', + default: 'speed', + description: 'What type of benchmark to be executed', + }).argv; + +console.log(argv); + +const entry = argv._[0]; +const isDev = argv.dev; +const benchmarkType = argv.type; + if (entry == null) { throw new Error( "No valid entry was provided! Valid entry example: 'yarn run ./benchmarks/react/counter'" ); } -// Execute the Benchmark -startSpeedBench(entry, isDev); +// Benchmarks that can be executed marked with a unique identifier +const benchmarks: { + [key: string]: (entry: string, isDev: boolean) => Promise; +} = { + speed: startSpeedBench, + bundle: startBundleBench, +}; + +// Execute Benchmark based on the specified Benchmark type +const toExecuteBenchmark = benchmarks[benchmarkType]; +if (toExecuteBenchmark != null) toExecuteBenchmark(entry, isDev); +else { + benchmarks['speed'](entry, isDev); +} diff --git a/benchmark/yarn.lock b/benchmark/yarn.lock index 16d56ebc..5e123e81 100644 --- a/benchmark/yarn.lock +++ b/benchmark/yarn.lock @@ -3,17 +3,17 @@ "@agile-ts/core@file:.yalc/@agile-ts/core": - version "0.1.0" + version "0.2.0-alpha.3" dependencies: - "@agile-ts/utils" "^0.0.5" + "@agile-ts/utils" "^0.0.7" "@agile-ts/react@file:.yalc/@agile-ts/react": - version "0.1.0" + version "0.1.2" -"@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/runtime@^7.12.1", "@babel/runtime@^7.9.2": version "7.14.6" @@ -101,6 +101,18 @@ resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275" integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA== +"@types/yargs-parser@*": + version "20.2.1" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.1.tgz#3b9ce2489919d9e4fea439b76916abc34b2df129" + integrity sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw== + +"@types/yargs@^17.0.2": + version "17.0.2" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.2.tgz#8fb2e0f4cdc7ab2a1a570106e56533f31225b584" + integrity sha512-JhZ+pNdKMfB0rXauaDlrIvm+U7V4m03PPOSVoPS66z8gf+G4Z/UW8UlrVIj2MRQOBzuoEvYtjS0bqYwnpZaS9Q== + dependencies: + "@types/yargs-parser" "*" + "@types/yauzl@^2.9.1": version "2.9.2" resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.9.2.tgz#c48e5d56aff1444409e39fa164b0b4d4552a7b7a" @@ -115,7 +127,12 @@ agent-base@6: dependencies: debug "4" -ansi-styles@^4.1.0: +ansi-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== @@ -156,6 +173,15 @@ chalk@^4.1.1: ansi-styles "^4.1.0" supports-color "^7.1.0" +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + color-convert@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" @@ -200,6 +226,11 @@ dotenv@^10.0.0: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81" integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + end-of-stream@^1.1.0: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" @@ -220,6 +251,11 @@ esbuild@^0.12.14, esbuild@^0.12.8: resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.12.14.tgz#43157dbd0b36d939247d4eb4909a4886ac40f82e" integrity sha512-z8p+6FGiplR7a3pPonXREbm+8IeXjBGvDpVidZmGB/AJMsJSfGCU+n7KOMCazA9AwvagadRWBhiKorC0w9WJvw== +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + escape-string-regexp@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" @@ -248,6 +284,11 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + get-stream@^5.1.0: version "5.2.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" @@ -315,6 +356,11 @@ inherits@2: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + jotai@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/jotai/-/jotai-1.1.2.tgz#3f211e0c03c74e95ea6fd7a69c1d2b65731009bf" @@ -537,6 +583,11 @@ regenerator-runtime@^0.13.4: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + reselect@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7" @@ -574,6 +625,22 @@ stack-utils@^2.0.3: dependencies: escape-string-regexp "^2.0.0" +string-width@^4.1.0, string-width@^4.2.0: + version "4.2.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" + integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.0" + +strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== + dependencies: + ansi-regex "^5.0.0" + supports-color@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" @@ -593,6 +660,15 @@ valtio@^1.0.6: dependencies: proxy-compare "2.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -603,6 +679,29 @@ ws@^7.4.6: resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.1.tgz#44fc000d87edb1d9c53e51fbc69a0ac1f6871d66" integrity sha512-2c6faOUH/nhoQN6abwMloF7Iyl0ZS2E9HGtsiLrWn0zOOMWlhtDmdf/uihDt6jnuCxgtwGBNy6Onsoy2s2O2Ow== +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs@^17.1.0: + version "17.1.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.1.0.tgz#0cd9827a0572c9a1795361c4d1530e53ada168cf" + integrity sha512-SQr7qqmQ2sNijjJGHL4u7t8vyDZdZ3Ahkmo4sc1w5xI9TBX0QDdG/g4SFnxtWOsGLjwHQue57eFALfwFCnixgg== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + yauzl@^2.10.0: version "2.10.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" From 21a4deb24b275f439e019860400428250da0ca66 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sun, 8 Aug 2021 06:50:53 +0200 Subject: [PATCH 21/93] fixed typo --- benchmark/runtime/run.ts | 10 +++------- .../react/develop/functional-component-ts/src/App.tsx | 3 ++- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/benchmark/runtime/run.ts b/benchmark/runtime/run.ts index 254d6ed4..b4a7946b 100644 --- a/benchmark/runtime/run.ts +++ b/benchmark/runtime/run.ts @@ -6,13 +6,11 @@ import { hideBin } from 'yargs/helpers'; // Loads environment variables from the '.env' file dotenv.config(); -// TODO implement yargs https://yargs.js.org/ - // hideBind handles the 'process.argv.slice' logic const argv = yargs(hideBin(process.argv)) .option('_', { type: 'string', - default: ['./benchmarks/react/counter'], + // default: ['./benchmarks/react/counter'], description: 'What benchmark to execute', }) .option('dev', { @@ -25,12 +23,10 @@ const argv = yargs(hideBin(process.argv)) type: 'string', default: 'speed', description: 'What type of benchmark to be executed', - }).argv; - -console.log(argv); + }).argv as any; const entry = argv._[0]; -const isDev = argv.dev; +const isDev = argv.dev || process.env.DEV === 'true'; const benchmarkType = argv.type; if (entry == null) { diff --git a/examples/react/develop/functional-component-ts/src/App.tsx b/examples/react/develop/functional-component-ts/src/App.tsx index 40ecce70..860f0656 100644 --- a/examples/react/develop/functional-component-ts/src/App.tsx +++ b/examples/react/develop/functional-component-ts/src/App.tsx @@ -14,6 +14,7 @@ import { STATE_OBJECT, } from './core'; import { generateId } from '@agile-ts/utils'; +import { globalBind } from '@agile-ts/core'; let rerenderCount = 0; let rerenderCountInCountupView = 0; @@ -69,7 +70,7 @@ const App = (props: any) => { // Create global Instance of Core (for better debugging) useEffect(() => { - // globalBind('__core__', { ...require('./core') }); + globalBind('__core__', { ...require('./core') }); }, []); const CountupView = () => { From 3a01dc28d6661df43c4c8555fea14d0af74b9761 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sun, 8 Aug 2021 07:50:02 +0200 Subject: [PATCH 22/93] fixed agile bench --- .../1000fields/bench/agilets/collection.tsx | 8 ++++++-- .../1000fields/bench/agilets/nestedState.tsx | 8 ++++++-- .../react/1000fields/bench/agilets/state.tsx | 8 ++++++-- .../computed/bench/agilets/autoTracking.tsx | 8 ++++++-- .../computed/bench/agilets/hardCoded.tsx | 8 ++++++-- .../react/counter/bench/agilets.tsx | 8 ++++++-- benchmark/benchmarks/react/counter/index.ts | 20 +++++++++---------- packages/core/src/logCodeManager.ts | 2 +- 8 files changed, 47 insertions(+), 23 deletions(-) diff --git a/benchmark/benchmarks/react/1000fields/bench/agilets/collection.tsx b/benchmark/benchmarks/react/1000fields/bench/agilets/collection.tsx index f30fcef4..0d9482bb 100644 --- a/benchmark/benchmarks/react/1000fields/bench/agilets/collection.tsx +++ b/benchmark/benchmarks/react/1000fields/bench/agilets/collection.tsx @@ -1,7 +1,11 @@ import React from 'react'; import ReactDom from 'react-dom'; -import { createCollection } from '@agile-ts/core'; -import { useAgile, useValue } from '@agile-ts/react'; +import { createCollection, shared } from '@agile-ts/core'; +import reactIntegration, { useAgile, useValue } from '@agile-ts/react'; +import { assignSharedAgileLoggerConfig } from '@agile-ts/logger'; + +assignSharedAgileLoggerConfig({ active: false }); +shared.integrate(reactIntegration); export default function (target: HTMLElement, fieldsCount: number) { const FIELDS = createCollection({ diff --git a/benchmark/benchmarks/react/1000fields/bench/agilets/nestedState.tsx b/benchmark/benchmarks/react/1000fields/bench/agilets/nestedState.tsx index 88fe5ba6..313154df 100644 --- a/benchmark/benchmarks/react/1000fields/bench/agilets/nestedState.tsx +++ b/benchmark/benchmarks/react/1000fields/bench/agilets/nestedState.tsx @@ -1,7 +1,11 @@ import * as React from 'react'; import * as ReactDom from 'react-dom'; -import { createState, State } from '@agile-ts/core'; -import { useAgile } from '@agile-ts/react'; +import { createState, shared, State } from '@agile-ts/core'; +import reactIntegration, { useAgile } from '@agile-ts/react'; +import { assignSharedAgileLoggerConfig } from '@agile-ts/logger'; + +assignSharedAgileLoggerConfig({ active: false }); +shared.integrate(reactIntegration); export default function (target: HTMLElement, fieldsCount: number) { const FIELDS = createState( diff --git a/benchmark/benchmarks/react/1000fields/bench/agilets/state.tsx b/benchmark/benchmarks/react/1000fields/bench/agilets/state.tsx index 08069b33..6db059cd 100644 --- a/benchmark/benchmarks/react/1000fields/bench/agilets/state.tsx +++ b/benchmark/benchmarks/react/1000fields/bench/agilets/state.tsx @@ -1,7 +1,11 @@ import React from 'react'; import ReactDom from 'react-dom'; -import { createState } from '@agile-ts/core'; -import { useAgile } from '@agile-ts/react'; +import { createState, shared } from '@agile-ts/core'; +import reactIntegration, { useAgile } from '@agile-ts/react'; +import { assignSharedAgileLoggerConfig } from '@agile-ts/logger'; + +assignSharedAgileLoggerConfig({ active: false }); +shared.integrate(reactIntegration); export default function (target: HTMLElement, fieldsCount: number) { const FIELDS = createState( diff --git a/benchmark/benchmarks/react/computed/bench/agilets/autoTracking.tsx b/benchmark/benchmarks/react/computed/bench/agilets/autoTracking.tsx index df85d176..c87de1e6 100644 --- a/benchmark/benchmarks/react/computed/bench/agilets/autoTracking.tsx +++ b/benchmark/benchmarks/react/computed/bench/agilets/autoTracking.tsx @@ -1,7 +1,11 @@ import React from 'react'; import ReactDom from 'react-dom'; -import { createComputed, createState } from '@agile-ts/core'; -import { useAgile } from '@agile-ts/react'; +import { createComputed, createState, shared } from '@agile-ts/core'; +import reactIntegration, { useAgile } from '@agile-ts/react'; +import { assignSharedAgileLoggerConfig } from '@agile-ts/logger'; + +assignSharedAgileLoggerConfig({ active: false }); +shared.integrate(reactIntegration); const COUNT = createState(0); const COMPUTED_COUNT = createComputed(() => { diff --git a/benchmark/benchmarks/react/computed/bench/agilets/hardCoded.tsx b/benchmark/benchmarks/react/computed/bench/agilets/hardCoded.tsx index ac782b55..55e4c693 100644 --- a/benchmark/benchmarks/react/computed/bench/agilets/hardCoded.tsx +++ b/benchmark/benchmarks/react/computed/bench/agilets/hardCoded.tsx @@ -1,7 +1,11 @@ import React from 'react'; import ReactDom from 'react-dom'; -import { createComputed, createState } from '@agile-ts/core'; -import { useAgile } from '@agile-ts/react'; +import { createComputed, createState, shared } from '@agile-ts/core'; +import reactIntegration, { useAgile } from '@agile-ts/react'; +import { assignSharedAgileLoggerConfig } from '@agile-ts/logger'; + +assignSharedAgileLoggerConfig({ active: false }); +shared.integrate(reactIntegration); const COUNT = createState(0); const COMPUTED_COUNT = createComputed( diff --git a/benchmark/benchmarks/react/counter/bench/agilets.tsx b/benchmark/benchmarks/react/counter/bench/agilets.tsx index 7434d9ed..1c9de3f7 100644 --- a/benchmark/benchmarks/react/counter/bench/agilets.tsx +++ b/benchmark/benchmarks/react/counter/bench/agilets.tsx @@ -1,7 +1,11 @@ import React from 'react'; import ReactDom from 'react-dom'; -import { createState } from '@agile-ts/core'; -import { useAgile } from '@agile-ts/react'; +import { createState, shared } from '@agile-ts/core'; +import reactIntegration, { useAgile } from '@agile-ts/react'; +import { assignSharedAgileLoggerConfig } from '@agile-ts/logger'; + +assignSharedAgileLoggerConfig({ active: false }); +shared.integrate(reactIntegration); const COUNT = createState(0); diff --git a/benchmark/benchmarks/react/counter/index.ts b/benchmark/benchmarks/react/counter/index.ts index 78be7f50..4580a0d2 100644 --- a/benchmark/benchmarks/react/counter/index.ts +++ b/benchmark/benchmarks/react/counter/index.ts @@ -64,16 +64,16 @@ const results: CycleResultInterface[] = []; // Add Tests to the Benchmark Test Suite suite .add('AgileTs', configTest(agilets)) - // .add('Hookstate', configTest(hookstate)) - // .add('Jotai', configTest(jotai)) - // .add('Mobx', configTest(mobx)) - // .add('Nano Stores', configTest(nanostores)) - // .add('PulseJs', configTest(pulsejs)) - // .add('Recoil', configTest(recoil)) - // .add('Redux', configTest(redux)) - // .add('Redux-Toolkit', configTest(reduxToolkit)) - // .add('Valtio', configTest(valtio)) - // .add('Zustand', configTest(zustand)) + .add('Hookstate', configTest(hookstate)) + .add('Jotai', configTest(jotai)) + .add('Mobx', configTest(mobx)) + .add('Nano Stores', configTest(nanostores)) + .add('PulseJs', configTest(pulsejs)) + .add('Recoil', configTest(recoil)) + .add('Redux', configTest(redux)) + .add('Redux-Toolkit', configTest(reduxToolkit)) + .add('Valtio', configTest(valtio)) + .add('Zustand', configTest(zustand)) // Add Listener .on('start', function (this: any) { diff --git a/packages/core/src/logCodeManager.ts b/packages/core/src/logCodeManager.ts index cd24f598..642c0441 100644 --- a/packages/core/src/logCodeManager.ts +++ b/packages/core/src/logCodeManager.ts @@ -258,7 +258,7 @@ function logIfTags>( * @internal */ let tempLogCodeManager; -if (process.env.NODE_ENV === 'dev') { +if (typeof process === 'object' && process.env.NODE_ENV !== 'production') { tempLogCodeManager = { getLog, log, From 59b4c8b53e2b438d0dd94c34bc7376b9bc7c41ba Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Mon, 9 Aug 2021 19:42:00 +0200 Subject: [PATCH 23/93] updated benchmark to esm --- benchmark/runtime/benchmarkTypes.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/benchmark/runtime/benchmarkTypes.ts b/benchmark/runtime/benchmarkTypes.ts index abb6b31d..43783052 100644 --- a/benchmark/runtime/benchmarkTypes.ts +++ b/benchmark/runtime/benchmarkTypes.ts @@ -19,7 +19,7 @@ export const startSpeedBench = async (entry: string, isDev: boolean) => { entryPoints: [entry], // https://esbuild.github.io/api/#entry-points outfile: './public/bundle.js', target: 'es2015', - format: 'cjs', // https://esbuild.github.io/api/#format-commonjs + format: 'esm', // https://esbuild.github.io/api/#format-commonjs platform: 'browser', minify: !isDev, // https://esbuild.github.io/api/#minify bundle: true, // https://esbuild.github.io/api/#bundle @@ -105,7 +105,7 @@ export const startBundleBench = async (entry: string, isDev: boolean) => { entryPoints: [entry], // https://esbuild.github.io/api/#entry-points outfile: './public/bundle.js', target: 'es2015', - format: 'cjs', // https://esbuild.github.io/api/#format-commonjs + format: 'esm', // https://esbuild.github.io/api/#format-commonjs platform: 'browser', minify: !isDev, // https://esbuild.github.io/api/#minify bundle: true, // https://esbuild.github.io/api/#bundle From 653f8e7b0c01a39a0978e0d995f14428afe6a041 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Tue, 10 Aug 2021 06:57:34 +0200 Subject: [PATCH 24/93] fixed tests --- packages/core/src/logCodeManager.ts | 9 ++++++--- packages/core/src/utils.ts | 3 +++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/core/src/logCodeManager.ts b/packages/core/src/logCodeManager.ts index 642c0441..e6138a7d 100644 --- a/packages/core/src/logCodeManager.ts +++ b/packages/core/src/logCodeManager.ts @@ -1,3 +1,5 @@ +import { isDev } from './utils'; + // The Log Code Manager keeps track // and manages all important Logs of AgileTs. // @@ -163,8 +165,9 @@ const niceLogCodeMessages = { '00:03:01': "'${0}' has to be of the type ${1}!", }; -const logCodeMessages: typeof niceLogCodeMessages = - process.env.NODE_ENV === 'dev' ? niceLogCodeMessages : ({} as any); +const logCodeMessages: typeof niceLogCodeMessages = isDev + ? niceLogCodeMessages + : ({} as any); /** * Returns the log message according to the specified log code. @@ -258,7 +261,7 @@ function logIfTags>( * @internal */ let tempLogCodeManager; -if (typeof process === 'object' && process.env.NODE_ENV !== 'production') { +if (isDev) { tempLogCodeManager = { getLog, log, diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index 3272d416..1f77201e 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -272,3 +272,6 @@ export const runsOnServer = (): boolean => { typeof window.document.createElement !== 'undefined' ); }; + +export const isDev = + typeof process === 'object' && process.env.NODE_ENV !== 'production'; From 3c24b005b4c9e5c510227820174ba576cc9a4e96 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Tue, 10 Aug 2021 08:00:25 +0200 Subject: [PATCH 25/93] removed is dev variable because if not directly checking if the env is prod, webpack can't detect it --- packages/core/src/logCodeManager.ts | 13 +++++++------ packages/core/src/utils.ts | 3 --- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/core/src/logCodeManager.ts b/packages/core/src/logCodeManager.ts index e6138a7d..87839dfc 100644 --- a/packages/core/src/logCodeManager.ts +++ b/packages/core/src/logCodeManager.ts @@ -1,5 +1,3 @@ -import { isDev } from './utils'; - // The Log Code Manager keeps track // and manages all important Logs of AgileTs. // @@ -165,9 +163,12 @@ const niceLogCodeMessages = { '00:03:01': "'${0}' has to be of the type ${1}!", }; -const logCodeMessages: typeof niceLogCodeMessages = isDev - ? niceLogCodeMessages - : ({} as any); +// Note: Not outsource the 'production' env check, +// because then webpack can't treeshake based on the current env +const logCodeMessages: typeof niceLogCodeMessages = + typeof process === 'object' && process.env.NODE_ENV !== 'production' + ? niceLogCodeMessages + : ({} as any); /** * Returns the log message according to the specified log code. @@ -261,7 +262,7 @@ function logIfTags>( * @internal */ let tempLogCodeManager; -if (isDev) { +if (typeof process === 'object' && process.env.NODE_ENV !== 'production') { tempLogCodeManager = { getLog, log, diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index 1f77201e..3272d416 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -272,6 +272,3 @@ export const runsOnServer = (): boolean => { typeof window.document.createElement !== 'undefined' ); }; - -export const isDev = - typeof process === 'object' && process.env.NODE_ENV !== 'production'; From f7cfe79ec19ab1ded3b7d682f3dbfafb24cf0488 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Wed, 11 Aug 2021 06:48:03 +0200 Subject: [PATCH 26/93] fixed typos --- examples/react/release/boxes/package.json | 2 +- packages/api/tsconfig.esm.json | 2 +- packages/core/src/logCodeManager.ts | 11 +++++++++-- .../core/tests/unit/collection/collection.test.ts | 3 +++ packages/core/tests/unit/computed/computed.test.ts | 3 +++ packages/core/tsconfig.esm.json | 2 +- packages/event/tsconfig.esm.json | 2 +- packages/logger/tsconfig.esm.json | 2 +- packages/multieditor/tsconfig.esm.json | 2 +- packages/proxytree/tsconfig.esm.json | 2 +- packages/react/tsconfig.esm.json | 2 +- packages/utils/tsconfig.esm.json | 2 +- packages/vue/src/bindAgileInstances.ts | 2 +- packages/vue/tsconfig.esm.json | 2 +- 14 files changed, 26 insertions(+), 13 deletions(-) diff --git a/examples/react/release/boxes/package.json b/examples/react/release/boxes/package.json index ebb40840..479c96d1 100644 --- a/examples/react/release/boxes/package.json +++ b/examples/react/release/boxes/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { - "@agile-ts/core": "file:.yalc/@agile-ts/core", + "@agile-ts/core": "^0.1.3", "@agile-ts/logger": "^0.0.7", "@agile-ts/proxytree": "^0.0.5", "@agile-ts/react": "^0.1.2", diff --git a/packages/api/tsconfig.esm.json b/packages/api/tsconfig.esm.json index 9512f0ad..a00a08bf 100644 --- a/packages/api/tsconfig.esm.json +++ b/packages/api/tsconfig.esm.json @@ -3,7 +3,7 @@ "compilerOptions": { "module": "ES2015", "outDir": "dist/esm", - "declaration": false, // already generated via 'tsconfig.json' in root + "declaration": false, // already generated via 'tsconfig.json' in the root dist folder "removeComments": true } } diff --git a/packages/core/src/logCodeManager.ts b/packages/core/src/logCodeManager.ts index 87839dfc..3ab5f107 100644 --- a/packages/core/src/logCodeManager.ts +++ b/packages/core/src/logCodeManager.ts @@ -261,7 +261,14 @@ function logIfTags>( * * @internal */ -let tempLogCodeManager; +let tempLogCodeManager: { + getLog: typeof getLog; + log: typeof log; + logCodeLogTypes: typeof logCodeTypes; + logCodeMessages: typeof logCodeMessages; + getLogger: () => any; + logIfTags: typeof logIfTags; +}; if (typeof process === 'object' && process.env.NODE_ENV !== 'production') { tempLogCodeManager = { getLog, @@ -277,13 +284,13 @@ if (typeof process === 'object' && process.env.NODE_ENV !== 'production') { } catch (e) { // empty catch block } - return loggerPackage?.sharedAgileLogger ?? null; }, logIfTags, }; } else { tempLogCodeManager = { + // Log only logCode getLog: (logCode, replacers) => logCode, log, logCodeLogTypes: logCodeTypes, diff --git a/packages/core/tests/unit/collection/collection.test.ts b/packages/core/tests/unit/collection/collection.test.ts index e5b0e686..086079f4 100644 --- a/packages/core/tests/unit/collection/collection.test.ts +++ b/packages/core/tests/unit/collection/collection.test.ts @@ -55,6 +55,7 @@ describe('Collection Tests', () => { expect(collection.persistent).toBeUndefined(); expect(collection.groups).toStrictEqual({}); expect(collection.selectors).toStrictEqual({}); + expect(collection.isCollection).toBeTruthy(); expect(Collection.prototype.initGroups).toHaveBeenCalledWith({}); expect(Collection.prototype.initSelectors).toHaveBeenCalledWith({}); @@ -93,6 +94,7 @@ describe('Collection Tests', () => { expect(collection.persistent).toBeUndefined(); expect(collection.groups).toStrictEqual({}); expect(collection.selectors).toStrictEqual({}); + expect(collection.isCollection).toBeTruthy(); expect(Collection.prototype.initGroups).toHaveBeenCalledWith([ 'group1', @@ -146,6 +148,7 @@ describe('Collection Tests', () => { expect(collection.persistent).toBeUndefined(); expect(collection.groups).toStrictEqual({}); expect(collection.selectors).toStrictEqual({}); + expect(collection.isCollection).toBeTruthy(); expect(Collection.prototype.initGroups).toHaveBeenCalledWith({ group1: expect.any(Group), diff --git a/packages/core/tests/unit/computed/computed.test.ts b/packages/core/tests/unit/computed/computed.test.ts index 47ec4b26..160ddd2c 100644 --- a/packages/core/tests/unit/computed/computed.test.ts +++ b/packages/core/tests/unit/computed/computed.test.ts @@ -31,6 +31,7 @@ describe('Computed Tests', () => { expect(computed.computeFunction).toBe(computedFunction); expect(computed.config).toStrictEqual({ autodetect: true }); + expect(computed.isComputed).toBeTruthy(); expect(Array.from(computed.deps)).toStrictEqual([]); expect(computed.hardCodedDeps).toStrictEqual([]); expect(Utils.extractRelevantObservers).toHaveBeenCalledWith([]); @@ -80,6 +81,7 @@ describe('Computed Tests', () => { expect(computed.computeFunction).toBe(computedFunction); expect(computed.config).toStrictEqual({ autodetect: false }); + expect(computed.isComputed).toBeTruthy(); expect(Array.from(computed.deps)).toStrictEqual([ dummyObserver2, dummyStateObserver, @@ -134,6 +136,7 @@ describe('Computed Tests', () => { expect(computed.computeFunction).toBe(computedFunction); expect(computed.config).toStrictEqual({ autodetect: false }); + expect(computed.isComputed).toBeTruthy(); expect(Array.from(computed.deps)).toStrictEqual([]); expect(computed.hardCodedDeps).toStrictEqual([]); expect(Utils.extractRelevantObservers).toHaveBeenCalledWith([]); diff --git a/packages/core/tsconfig.esm.json b/packages/core/tsconfig.esm.json index 9512f0ad..a00a08bf 100644 --- a/packages/core/tsconfig.esm.json +++ b/packages/core/tsconfig.esm.json @@ -3,7 +3,7 @@ "compilerOptions": { "module": "ES2015", "outDir": "dist/esm", - "declaration": false, // already generated via 'tsconfig.json' in root + "declaration": false, // already generated via 'tsconfig.json' in the root dist folder "removeComments": true } } diff --git a/packages/event/tsconfig.esm.json b/packages/event/tsconfig.esm.json index 9512f0ad..a00a08bf 100644 --- a/packages/event/tsconfig.esm.json +++ b/packages/event/tsconfig.esm.json @@ -3,7 +3,7 @@ "compilerOptions": { "module": "ES2015", "outDir": "dist/esm", - "declaration": false, // already generated via 'tsconfig.json' in root + "declaration": false, // already generated via 'tsconfig.json' in the root dist folder "removeComments": true } } diff --git a/packages/logger/tsconfig.esm.json b/packages/logger/tsconfig.esm.json index 9512f0ad..a00a08bf 100644 --- a/packages/logger/tsconfig.esm.json +++ b/packages/logger/tsconfig.esm.json @@ -3,7 +3,7 @@ "compilerOptions": { "module": "ES2015", "outDir": "dist/esm", - "declaration": false, // already generated via 'tsconfig.json' in root + "declaration": false, // already generated via 'tsconfig.json' in the root dist folder "removeComments": true } } diff --git a/packages/multieditor/tsconfig.esm.json b/packages/multieditor/tsconfig.esm.json index 9512f0ad..a00a08bf 100644 --- a/packages/multieditor/tsconfig.esm.json +++ b/packages/multieditor/tsconfig.esm.json @@ -3,7 +3,7 @@ "compilerOptions": { "module": "ES2015", "outDir": "dist/esm", - "declaration": false, // already generated via 'tsconfig.json' in root + "declaration": false, // already generated via 'tsconfig.json' in the root dist folder "removeComments": true } } diff --git a/packages/proxytree/tsconfig.esm.json b/packages/proxytree/tsconfig.esm.json index 9512f0ad..a00a08bf 100644 --- a/packages/proxytree/tsconfig.esm.json +++ b/packages/proxytree/tsconfig.esm.json @@ -3,7 +3,7 @@ "compilerOptions": { "module": "ES2015", "outDir": "dist/esm", - "declaration": false, // already generated via 'tsconfig.json' in root + "declaration": false, // already generated via 'tsconfig.json' in the root dist folder "removeComments": true } } diff --git a/packages/react/tsconfig.esm.json b/packages/react/tsconfig.esm.json index 9512f0ad..a00a08bf 100644 --- a/packages/react/tsconfig.esm.json +++ b/packages/react/tsconfig.esm.json @@ -3,7 +3,7 @@ "compilerOptions": { "module": "ES2015", "outDir": "dist/esm", - "declaration": false, // already generated via 'tsconfig.json' in root + "declaration": false, // already generated via 'tsconfig.json' in the root dist folder "removeComments": true } } diff --git a/packages/utils/tsconfig.esm.json b/packages/utils/tsconfig.esm.json index 9512f0ad..a00a08bf 100644 --- a/packages/utils/tsconfig.esm.json +++ b/packages/utils/tsconfig.esm.json @@ -3,7 +3,7 @@ "compilerOptions": { "module": "ES2015", "outDir": "dist/esm", - "declaration": false, // already generated via 'tsconfig.json' in root + "declaration": false, // already generated via 'tsconfig.json' in the root dist folder "removeComments": true } } diff --git a/packages/vue/src/bindAgileInstances.ts b/packages/vue/src/bindAgileInstances.ts index 73d5508e..52ffdcff 100644 --- a/packages/vue/src/bindAgileInstances.ts +++ b/packages/vue/src/bindAgileInstances.ts @@ -1,12 +1,12 @@ import Vue from 'vue'; import { Agile, - Collection, extractRelevantObservers, Observer, State, } from '@agile-ts/core'; import { isValidObject, normalizeArray } from '@agile-ts/utils'; +import type { Collection } from '@agile-ts/core'; // Only import Collection and Group type for better Treeshaking export function bindAgileInstances( deps: DepsType, diff --git a/packages/vue/tsconfig.esm.json b/packages/vue/tsconfig.esm.json index 9512f0ad..a00a08bf 100644 --- a/packages/vue/tsconfig.esm.json +++ b/packages/vue/tsconfig.esm.json @@ -3,7 +3,7 @@ "compilerOptions": { "module": "ES2015", "outDir": "dist/esm", - "declaration": false, // already generated via 'tsconfig.json' in root + "declaration": false, // already generated via 'tsconfig.json' in the root dist folder "removeComments": true } } From 9de3ba1cc1f912afb31bdb90a8b3cf9c03809536 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Thu, 12 Aug 2021 06:07:36 +0200 Subject: [PATCH 27/93] fixed typos --- examples/react/develop/simple-counter/package.json | 2 +- packages/core/src/logCodeManager.ts | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/react/develop/simple-counter/package.json b/examples/react/develop/simple-counter/package.json index eebd90c7..0b329bb3 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/logger": "file:.yalc/@agile-ts/logger", "@agile-ts/react": "file:.yalc/@agile-ts/react", + "@agile-ts/logger": "file:.yalc/@agile-ts/logger", "@reduxjs/toolkit": "^1.6.1", "jotai": "^1.2.2", "nanostores": "^0.4.1", diff --git a/packages/core/src/logCodeManager.ts b/packages/core/src/logCodeManager.ts index 3ab5f107..44cab8f6 100644 --- a/packages/core/src/logCodeManager.ts +++ b/packages/core/src/logCodeManager.ts @@ -166,7 +166,7 @@ const niceLogCodeMessages = { // Note: Not outsource the 'production' env check, // because then webpack can't treeshake based on the current env const logCodeMessages: typeof niceLogCodeMessages = - typeof process === 'object' && process.env.NODE_ENV !== 'production' + typeof process === 'object' && process.env.NODE_ENV === 'development' ? niceLogCodeMessages : ({} as any); @@ -269,7 +269,7 @@ let tempLogCodeManager: { getLogger: () => any; logIfTags: typeof logIfTags; }; -if (typeof process === 'object' && process.env.NODE_ENV !== 'production') { +if (typeof process === 'object' && process.env.NODE_ENV === 'development') { tempLogCodeManager = { getLog, log, @@ -300,7 +300,9 @@ if (typeof process === 'object' && process.env.NODE_ENV !== 'production') { getLogger: () => { return null; }, - logIfTags: (tags, logCode, replacers) => log(logCode, replacers), + logIfTags: (tags, logCode, replacers) => { + /* empty */ + }, }; } export const LogCodeManager = tempLogCodeManager; From 86cf54a75e50ef6cc93bd589e8dbcd0105f90c16 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Thu, 12 Aug 2021 06:23:21 +0200 Subject: [PATCH 28/93] fixed typo --- packages/core/src/logCodeManager.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/logCodeManager.ts b/packages/core/src/logCodeManager.ts index 44cab8f6..819563fa 100644 --- a/packages/core/src/logCodeManager.ts +++ b/packages/core/src/logCodeManager.ts @@ -166,7 +166,7 @@ const niceLogCodeMessages = { // Note: Not outsource the 'production' env check, // because then webpack can't treeshake based on the current env const logCodeMessages: typeof niceLogCodeMessages = - typeof process === 'object' && process.env.NODE_ENV === 'development' + typeof process === 'object' && process.env.NODE_ENV !== 'production' ? niceLogCodeMessages : ({} as any); @@ -269,7 +269,7 @@ let tempLogCodeManager: { getLogger: () => any; logIfTags: typeof logIfTags; }; -if (typeof process === 'object' && process.env.NODE_ENV === 'development') { +if (typeof process === 'object' && process.env.NODE_ENV !== 'production') { tempLogCodeManager = { getLog, log, From 942ab19110d898d4c4732c87f98c6dd0f32ce88b Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Thu, 12 Aug 2021 06:54:35 +0200 Subject: [PATCH 29/93] manual release of @agile-ts/react and @agile-ts/core --- packages/core/package.json | 2 +- packages/react/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/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/", From 4c4d1b5453ea9fd1698fb606ca1e16d426a69d88 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Fri, 13 Aug 2021 06:52:17 +0200 Subject: [PATCH 30/93] started to outsource persist logic for better treeshaking --- packages/core/src/agile.ts | 46 ------------------------ packages/core/src/state/state.ts | 2 -- packages/core/src/storages/index.ts | 23 +++++++++++- packages/core/src/storages/persistent.ts | 19 ++++++++-- packages/core/src/storages/storages.ts | 10 +++--- 5 files changed, 44 insertions(+), 56 deletions(-) diff --git a/packages/core/src/agile.ts b/packages/core/src/agile.ts index 23a02432..797107df 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; @@ -75,9 +70,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 +99,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 +110,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 +122,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/state/state.ts b/packages/core/src/state/state.ts index 592a4e36..d9d97164 100644 --- a/packages/core/src/state/state.ts +++ b/packages/core/src/state/state.ts @@ -54,8 +54,6 @@ export class State { // 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; diff --git a/packages/core/src/storages/index.ts b/packages/core/src/storages/index.ts index a8938be1..0d105fc8 100644 --- a/packages/core/src/storages/index.ts +++ b/packages/core/src/storages/index.ts @@ -1,4 +1,5 @@ -import { CreateStorageConfigInterface, Storage } from '../internal'; +import { CreateStorageConfigInterface, generateId, Storage } from '../internal'; +import type { Storages, Persistent } from '../internal'; export * from './storages'; // export * from './storage'; @@ -22,3 +23,23 @@ export * from './storages'; export function createStorage(config: CreateStorageConfigInterface): Storage { return new Storage(config); } + +// Handles the permanent persistence of Agile Classes +export const storageManagers: { + [key: string]: Storages; +} = {}; +export let defaultStorageManagerKey: string | null = null; + +export const registerStorageManager = ( + instance: Storages, + key: string = generateId() +) => { + if (Object.prototype.hasOwnProperty.call(storageManagers, key)) { + // TODO + return; + } + + storageManagers[key] = instance; + + if (defaultStorageManagerKey == null) defaultStorageManagerKey = key; +}; diff --git a/packages/core/src/storages/persistent.ts b/packages/core/src/storages/persistent.ts index e4134fca..ceb8aa04 100644 --- a/packages/core/src/storages/persistent.ts +++ b/packages/core/src/storages/persistent.ts @@ -1,9 +1,11 @@ import { Agile, copy, + defaultStorageManagerKey, defineConfig, LogCodeManager, StorageKey, + storageManagers, } from '../internal'; export class Persistent { @@ -27,6 +29,8 @@ export class Persistent { // Key/Name identifier of the Storages the Persistent value is stored in public storageKeys: StorageKey[] = []; + public storageManagerKey: string; + /** * A Persistent manages the permanent persistence * of an Agile Class such as the `State Class` in external Storages. @@ -48,8 +52,9 @@ export class Persistent { instantiate: true, storageKeys: [], defaultStorageKey: null as any, + storageManagerKey: defaultStorageManagerKey, }); - this.agileInstance().storages.persistentInstances.add(this); + this.storageManagerKey = config.storageManagerKey as any; this.config = { defaultStorageKey: config.defaultStorageKey as any }; // Instantiate Persistent @@ -127,6 +132,9 @@ export class Persistent { this._key = this.formatKey(config.key) ?? Persistent.placeHolderKey; this.assignStorageKeys(config.storageKeys, config.defaultStorageKey); this.validatePersistent(); + storageManagers[this.storageManagerKey].persistentInstances[ + this._key + ] = this; } /** @@ -140,6 +148,7 @@ export class Persistent { */ public validatePersistent(): boolean { let isValid = true; + const storages = storageManagers[this.storageManagerKey]; // Validate Persistent key/name identifier if (this._key === Persistent.placeHolderKey) { @@ -155,7 +164,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 (!storages.storages[key]) { LogCodeManager.log('12:03:02', [this._key, key]); isValid = false; } @@ -180,7 +189,7 @@ export class Persistent { storageKeys: StorageKey[] = [], defaultStorageKey?: StorageKey ): void { - const storages = this.agileInstance().storages; + const storages = storageManagers[this.storageManagerKey]; const _storageKeys = copy(storageKeys); // Assign specified default Storage key to the 'storageKeys' array @@ -318,6 +327,10 @@ export interface CreatePersistentConfigInterface { * @default true */ instantiate?: boolean; + /** + * TODO + */ + storageManagerKey?: string; } export interface PersistentConfigInterface { diff --git a/packages/core/src/storages/storages.ts b/packages/core/src/storages/storages.ts index 498f9bb5..dd0f9ea1 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,14 @@ export class Storages { this.storages[storage.key] = storage; if (config.default) this.config.defaultStorageKey = storage.key; - this.persistentInstances.forEach((persistent) => { + for (const persistentKey in this.persistentInstances) { + const persistent = this.persistentInstances[persistentKey]; + // 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 +115,7 @@ export class Storages { const isValid = persistent.validatePersistent(); if (isValid) persistent.initialLoading(); } - }); + } LogCodeManager.log('13:00:00', [storage.key], storage); From 0cd44aae2619fa04db4bd7c34d996ec97c67d156 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sat, 14 Aug 2021 14:28:38 +0200 Subject: [PATCH 31/93] fixed typo --- packages/core/src/state/state.ts | 30 ++++++++++++++++++++--------- packages/core/src/storages/index.ts | 2 +- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/packages/core/src/state/state.ts b/packages/core/src/state/state.ts index d9d97164..ccc50bb4 100644 --- a/packages/core/src/state/state.ts +++ b/packages/core/src/state/state.ts @@ -17,6 +17,8 @@ import { removeProperties, LogCodeManager, defineConfig, + storageManagers, + defaultStorageManagerKey, } from '../internal'; export class State { @@ -432,15 +434,25 @@ export class State { }); // 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, - }); + if (this.isPersisted) return this; + + const storageManager = storageManagers[defaultStorageManagerKey || '']; + + // Check if a Storage Manager exists + if (storageManager != null && key != null) { + // TODO tree shake persistent when Storage not registered + storageManager.persistentInstances[key] = new StatePersistent( + this, + { + instantiate: _config.loadValue, + storageKeys: _config.storageKeys, + key: key, + defaultStorageKey: _config.defaultStorageKey, + } + ); + } else { + // TODO add error + } return this; } diff --git a/packages/core/src/storages/index.ts b/packages/core/src/storages/index.ts index 0d105fc8..0e7c44ce 100644 --- a/packages/core/src/storages/index.ts +++ b/packages/core/src/storages/index.ts @@ -1,5 +1,5 @@ import { CreateStorageConfigInterface, generateId, Storage } from '../internal'; -import type { Storages, Persistent } from '../internal'; +import type { Storages } from '../internal'; export * from './storages'; // export * from './storage'; From 88df4659038f70c131794c20ce64b75231306c16 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sun, 15 Aug 2021 18:44:16 +0200 Subject: [PATCH 32/93] added persistableState --- packages/core/src/collection/group/index.ts | 4 +- packages/core/src/collection/item.ts | 8 +- packages/core/src/collection/selector.ts | 4 +- packages/core/src/internal.ts | 1 + packages/core/src/state/persistableState.ts | 123 ++++++++++++++++++++ packages/core/src/state/state.ts | 116 ------------------ 6 files changed, 132 insertions(+), 124 deletions(-) create mode 100644 packages/core/src/state/persistableState.ts diff --git a/packages/core/src/collection/group/index.ts b/packages/core/src/collection/group/index.ts index 44aef2c5..7c9a3df7 100644 --- a/packages/core/src/collection/group/index.ts +++ b/packages/core/src/collection/group/index.ts @@ -1,5 +1,5 @@ import { - State, + PersistableState, 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 PersistableState> { // 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..6a170896 100644 --- a/packages/core/src/collection/item.ts +++ b/packages/core/src/collection/item.ts @@ -1,5 +1,5 @@ import { - State, + PersistableState, Collection, StateKey, StateRuntimeJobConfigInterface, @@ -12,9 +12,9 @@ import { defineConfig, } from '../internal'; -export class Item extends State< - DataType -> { +export class Item< + DataType extends Object = DefaultItem +> extends PersistableState { // Collection the Group belongs to public collection: () => Collection; diff --git a/packages/core/src/collection/selector.ts b/packages/core/src/collection/selector.ts index 937c5458..279c316e 100644 --- a/packages/core/src/collection/selector.ts +++ b/packages/core/src/collection/selector.ts @@ -4,13 +4,13 @@ import { defineConfig, Item, ItemKey, - State, + PersistableState, StateRuntimeJobConfigInterface, } from '../internal'; export class Selector< DataType extends Object = DefaultItem -> extends State { +> extends PersistableState { // Collection the Selector belongs to public collection: () => Collection; diff --git a/packages/core/src/internal.ts b/packages/core/src/internal.ts index a1c8852c..4ddcde87 100644 --- a/packages/core/src/internal.ts +++ b/packages/core/src/internal.ts @@ -38,6 +38,7 @@ export * from './state'; export * from './state/state.observer'; export * from './state/state.persistent'; export * from './state/state.runtime.job'; +export * from './state/persistableState'; // Computed export * from './computed'; diff --git a/packages/core/src/state/persistableState.ts b/packages/core/src/state/persistableState.ts new file mode 100644 index 00000000..8c83f8f7 --- /dev/null +++ b/packages/core/src/state/persistableState.ts @@ -0,0 +1,123 @@ +import { + defineConfig, + isFunction, + isValidObject, + LogCodeManager, + PersistentKey, + State, + StateKey, + StatePersistent, + StatePersistentConfigInterface, +} from '../internal'; + +export class PersistableState 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; + + public setKey(value: StateKey | undefined): this { + const oldKey = this._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; + } + + /** + * 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; + } +} diff --git a/packages/core/src/state/state.ts b/packages/core/src/state/state.ts index ccc50bb4..bca39f1e 100644 --- a/packages/core/src/state/state.ts +++ b/packages/core/src/state/state.ts @@ -5,20 +5,16 @@ import { flatMerge, isValidObject, StateObserver, - StatePersistent, Observer, equal, isFunction, notEqual, generateId, - PersistentKey, ComputedTracker, StateIngestConfigInterface, removeProperties, LogCodeManager, defineConfig, - storageManagers, - defaultStorageManagerKey, } from '../internal'; export class State { @@ -54,9 +50,6 @@ export class State { // Method for dynamically computing the existence of the State public computeExistsMethod: ComputeExistsMethod; - // Whether the State is persisted in an external Storage - public isPersisted = false; - // When an interval is active, the 'intervalId' to clear the interval is temporary stored here public currentInterval?: NodeJS.Timer | number; @@ -159,8 +152,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 +159,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; } @@ -384,107 +369,6 @@ export class State { 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.isPersisted) return this; - - const storageManager = storageManagers[defaultStorageManagerKey || '']; - - // Check if a Storage Manager exists - if (storageManager != null && key != null) { - // TODO tree shake persistent when Storage not registered - storageManager.persistentInstances[key] = new StatePersistent( - this, - { - instantiate: _config.loadValue, - storageKeys: _config.storageKeys, - key: key, - defaultStorageKey: _config.defaultStorageKey, - } - ); - } else { - // TODO add error - } - - 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. From c4ac13baa44564f6b7cbf6633b49d2d6c37741ff Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sun, 15 Aug 2021 18:53:23 +0200 Subject: [PATCH 33/93] fixed typos --- .../src/collection/collection.persistent.ts | 7 +++--- packages/core/src/shared.ts | 3 +-- packages/core/src/state/state.persistent.ts | 15 +++++++------ packages/core/src/storages/index.ts | 20 +++++------------ packages/core/src/storages/persistent.ts | 22 ++++--------------- 5 files changed, 22 insertions(+), 45 deletions(-) diff --git a/packages/core/src/collection/collection.persistent.ts b/packages/core/src/collection/collection.persistent.ts index 2f2b7136..c789fab4 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, + storageManager, } 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 storageManager?.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); + storageManager?.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); + storageManager?.remove(_storageItemKey, this.storageKeys); // Remove default Group from the Storage defaultGroup.persistent?.removePersistedValue(defaultGroupStorageKey); 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/state.persistent.ts b/packages/core/src/state/state.persistent.ts index d978196c..31322839 100644 --- a/packages/core/src/state/state.persistent.ts +++ b/packages/core/src/state/state.persistent.ts @@ -1,14 +1,15 @@ import { CreatePersistentConfigInterface, defineConfig, + PersistableState, Persistent, PersistentKey, - State, + storageManager, } from '../internal'; export class StatePersistent extends Persistent { // State the Persistent belongs to - public state: () => State; + public state: () => PersistableState; static storeValueSideEffectKey = 'rebuildStateStorageValue'; @@ -20,7 +21,7 @@ export class StatePersistent extends Persistent { * @param config - Configuration object */ constructor( - state: State, + state: PersistableState, 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 storageManager?.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); + storageManager?.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: PersistableState, storageItemKey: PersistentKey, config: { [key: string]: any } = {} ) { if (config['storage'] == null || config.storage) { - this.agileInstance().storages.set( + storageManager?.set( storageItemKey, this.state().getPersistableValue(), this.storageKeys diff --git a/packages/core/src/storages/index.ts b/packages/core/src/storages/index.ts index 0e7c44ce..a0ef25f2 100644 --- a/packages/core/src/storages/index.ts +++ b/packages/core/src/storages/index.ts @@ -25,21 +25,11 @@ export function createStorage(config: CreateStorageConfigInterface): Storage { } // Handles the permanent persistence of Agile Classes -export const storageManagers: { - [key: string]: Storages; -} = {}; -export let defaultStorageManagerKey: string | null = null; +export let storageManager: Storages | null = null; -export const registerStorageManager = ( - instance: Storages, - key: string = generateId() -) => { - if (Object.prototype.hasOwnProperty.call(storageManagers, key)) { - // TODO - return; +export const registerStorageManager = (instance: Storages) => { + if (storageManager != null) { + // TODO print warning } - - storageManagers[key] = instance; - - if (defaultStorageManagerKey == null) defaultStorageManagerKey = key; + storageManager = instance; }; diff --git a/packages/core/src/storages/persistent.ts b/packages/core/src/storages/persistent.ts index ceb8aa04..ace74de9 100644 --- a/packages/core/src/storages/persistent.ts +++ b/packages/core/src/storages/persistent.ts @@ -1,11 +1,10 @@ import { Agile, copy, - defaultStorageManagerKey, defineConfig, LogCodeManager, StorageKey, - storageManagers, + storageManager, } from '../internal'; export class Persistent { @@ -29,8 +28,6 @@ export class Persistent { // Key/Name identifier of the Storages the Persistent value is stored in public storageKeys: StorageKey[] = []; - public storageManagerKey: string; - /** * A Persistent manages the permanent persistence * of an Agile Class such as the `State Class` in external Storages. @@ -52,9 +49,7 @@ export class Persistent { instantiate: true, storageKeys: [], defaultStorageKey: null as any, - storageManagerKey: defaultStorageManagerKey, }); - this.storageManagerKey = config.storageManagerKey as any; this.config = { defaultStorageKey: config.defaultStorageKey as any }; // Instantiate Persistent @@ -132,9 +127,6 @@ export class Persistent { this._key = this.formatKey(config.key) ?? Persistent.placeHolderKey; this.assignStorageKeys(config.storageKeys, config.defaultStorageKey); this.validatePersistent(); - storageManagers[this.storageManagerKey].persistentInstances[ - this._key - ] = this; } /** @@ -148,7 +140,6 @@ export class Persistent { */ public validatePersistent(): boolean { let isValid = true; - const storages = storageManagers[this.storageManagerKey]; // Validate Persistent key/name identifier if (this._key === Persistent.placeHolderKey) { @@ -164,7 +155,7 @@ export class Persistent { // Check if the Storages exist at the specified Storage keys this.storageKeys.map((key) => { - if (!storages.storages[key]) { + if (!storageManager?.storages[key]) { LogCodeManager.log('12:03:02', [this._key, key]); isValid = false; } @@ -189,7 +180,6 @@ export class Persistent { storageKeys: StorageKey[] = [], defaultStorageKey?: StorageKey ): void { - const storages = storageManagers[this.storageManagerKey]; const _storageKeys = copy(storageKeys); // Assign specified default Storage key to the 'storageKeys' array @@ -200,10 +190,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 = storageManager?.config.defaultStorageKey; if (defaultStorageKey != null) { this.config.defaultStorageKey = defaultStorageKey; - _storageKeys.push(storages.config.defaultStorageKey as any); + _storageKeys.push(storageManager?.config.defaultStorageKey as any); } } else { this.config.defaultStorageKey = defaultStorageKey ?? _storageKeys[0]; @@ -327,10 +317,6 @@ export interface CreatePersistentConfigInterface { * @default true */ instantiate?: boolean; - /** - * TODO - */ - storageManagerKey?: string; } export interface PersistentConfigInterface { From 7f335e9bbbfb8e5b9712d5d37f813c077d57ebad Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sun, 15 Aug 2021 19:42:34 +0200 Subject: [PATCH 34/93] fixed typos --- examples/react/release/boxes/package.json | 2 +- packages/core/src/logCodeManager.ts | 13 +++++++------ packages/core/src/state/index.ts | 15 +++++++++++++++ packages/core/src/state/persistableState.ts | 9 +++++++++ packages/core/src/state/state.ts | 9 --------- 5 files changed, 32 insertions(+), 16 deletions(-) diff --git a/examples/react/release/boxes/package.json b/examples/react/release/boxes/package.json index 479c96d1..ebb40840 100644 --- a/examples/react/release/boxes/package.json +++ b/examples/react/release/boxes/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { - "@agile-ts/core": "^0.1.3", + "@agile-ts/core": "file:.yalc/@agile-ts/core", "@agile-ts/logger": "^0.0.7", "@agile-ts/proxytree": "^0.0.5", "@agile-ts/react": "^0.1.2", diff --git a/packages/core/src/logCodeManager.ts b/packages/core/src/logCodeManager.ts index 819563fa..bb5c9ac6 100644 --- a/packages/core/src/logCodeManager.ts +++ b/packages/core/src/logCodeManager.ts @@ -270,6 +270,13 @@ let tempLogCodeManager: { logIfTags: typeof logIfTags; }; 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, @@ -278,12 +285,6 @@ if (typeof process === 'object' && process.env.NODE_ENV !== 'production') { // 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; }, logIfTags, diff --git a/packages/core/src/state/index.ts b/packages/core/src/state/index.ts index ba8e7e27..37192cd8 100644 --- a/packages/core/src/state/index.ts +++ b/packages/core/src/state/index.ts @@ -5,6 +5,7 @@ import { removeProperties, CreateAgileSubInstanceInterface, shared, + PersistableState, } from '../internal'; export * from './state'; @@ -44,3 +45,17 @@ export function createState( removeProperties(config, ['agileInstance']) ); } + +export function createPersistableState( + initialValue: ValueType, + config: CreateStateConfigInterfaceWithAgile = {} +): PersistableState { + config = defineConfig(config, { + agileInstance: shared, + }); + return new PersistableState( + config.agileInstance as any, + initialValue, + removeProperties(config, ['agileInstance']) + ); +} diff --git a/packages/core/src/state/persistableState.ts b/packages/core/src/state/persistableState.ts index 8c83f8f7..b9eb51c3 100644 --- a/packages/core/src/state/persistableState.ts +++ b/packages/core/src/state/persistableState.ts @@ -120,4 +120,13 @@ export class PersistableState extends State { return this; } + + /** + * Returns the persistable value of the State. + * + * @internal + */ + public getPersistableValue(): any { + return this._value; + } } diff --git a/packages/core/src/state/state.ts b/packages/core/src/state/state.ts index bca39f1e..2c23bb5c 100644 --- a/packages/core/src/state/state.ts +++ b/packages/core/src/state/state.ts @@ -590,15 +590,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; From e9c67caaab05d0223eebb520ef17bf548c6df3e9 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Mon, 16 Aug 2021 06:19:16 +0200 Subject: [PATCH 35/93] added enhanced state with all the enhanced features like undo, .. --- packages/core/src/collection/group/index.ts | 4 +- packages/core/src/collection/item.ts | 8 +- packages/core/src/collection/selector.ts | 4 +- packages/core/src/internal.ts | 2 +- packages/core/src/state/index.ts | 25 +- packages/core/src/state/persistableState.ts | 132 ----- packages/core/src/state/state.enhanced.ts | 526 ++++++++++++++++++++ packages/core/src/state/state.persistent.ts | 8 +- packages/core/src/state/state.ts | 377 -------------- 9 files changed, 559 insertions(+), 527 deletions(-) delete mode 100644 packages/core/src/state/persistableState.ts create mode 100644 packages/core/src/state/state.enhanced.ts diff --git a/packages/core/src/collection/group/index.ts b/packages/core/src/collection/group/index.ts index 7c9a3df7..dc339159 100644 --- a/packages/core/src/collection/group/index.ts +++ b/packages/core/src/collection/group/index.ts @@ -1,5 +1,5 @@ import { - PersistableState, + 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 PersistableState> { +> 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 6a170896..db98104f 100644 --- a/packages/core/src/collection/item.ts +++ b/packages/core/src/collection/item.ts @@ -1,5 +1,5 @@ import { - PersistableState, + EnhancedState, Collection, StateKey, StateRuntimeJobConfigInterface, @@ -12,9 +12,9 @@ import { defineConfig, } from '../internal'; -export class Item< - DataType extends Object = DefaultItem -> extends PersistableState { +export class Item extends EnhancedState< + DataType +> { // Collection the Group belongs to public collection: () => Collection; diff --git a/packages/core/src/collection/selector.ts b/packages/core/src/collection/selector.ts index 279c316e..76795f35 100644 --- a/packages/core/src/collection/selector.ts +++ b/packages/core/src/collection/selector.ts @@ -4,13 +4,13 @@ import { defineConfig, Item, ItemKey, - PersistableState, + EnhancedState, StateRuntimeJobConfigInterface, } from '../internal'; export class Selector< DataType extends Object = DefaultItem -> extends PersistableState { +> extends EnhancedState { // Collection the Selector belongs to public collection: () => Collection; diff --git a/packages/core/src/internal.ts b/packages/core/src/internal.ts index 4ddcde87..26581a63 100644 --- a/packages/core/src/internal.ts +++ b/packages/core/src/internal.ts @@ -38,7 +38,7 @@ export * from './state'; export * from './state/state.observer'; export * from './state/state.persistent'; export * from './state/state.runtime.job'; -export * from './state/persistableState'; +export * from './state/state.enhanced'; // Computed export * from './computed'; diff --git a/packages/core/src/state/index.ts b/packages/core/src/state/index.ts index 37192cd8..b99ec509 100644 --- a/packages/core/src/state/index.ts +++ b/packages/core/src/state/index.ts @@ -5,7 +5,7 @@ import { removeProperties, CreateAgileSubInstanceInterface, shared, - PersistableState, + EnhancedState, } from '../internal'; export * from './state'; @@ -32,7 +32,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 { @@ -46,14 +46,29 @@ export function createState( ); } -export function createPersistableState( +/** + * Returns a newly created State. + * + * A State manages 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. + * + * You can create as many global 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 = {} -): PersistableState { +): EnhancedState { config = defineConfig(config, { agileInstance: shared, }); - return new PersistableState( + return new EnhancedState( config.agileInstance as any, initialValue, removeProperties(config, ['agileInstance']) diff --git a/packages/core/src/state/persistableState.ts b/packages/core/src/state/persistableState.ts deleted file mode 100644 index b9eb51c3..00000000 --- a/packages/core/src/state/persistableState.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { - defineConfig, - isFunction, - isValidObject, - LogCodeManager, - PersistentKey, - State, - StateKey, - StatePersistent, - StatePersistentConfigInterface, -} from '../internal'; - -export class PersistableState 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; - - public setKey(value: StateKey | undefined): this { - const oldKey = this._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; - } - - /** - * 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; - } -} diff --git a/packages/core/src/state/state.enhanced.ts b/packages/core/src/state/state.enhanced.ts new file mode 100644 index 00000000..5dcde925 --- /dev/null +++ b/packages/core/src/state/state.enhanced.ts @@ -0,0 +1,526 @@ +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; + + 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.persistent.ts b/packages/core/src/state/state.persistent.ts index 31322839..45bc8d46 100644 --- a/packages/core/src/state/state.persistent.ts +++ b/packages/core/src/state/state.persistent.ts @@ -1,7 +1,7 @@ import { CreatePersistentConfigInterface, defineConfig, - PersistableState, + EnhancedState, Persistent, PersistentKey, storageManager, @@ -9,7 +9,7 @@ import { export class StatePersistent extends Persistent { // State the Persistent belongs to - public state: () => PersistableState; + public state: () => EnhancedState; static storeValueSideEffectKey = 'rebuildStateStorageValue'; @@ -21,7 +21,7 @@ export class StatePersistent extends Persistent { * @param config - Configuration object */ constructor( - state: PersistableState, + state: EnhancedState, config: CreatePersistentConfigInterface = {} ) { super(state.agileInstance(), { @@ -185,7 +185,7 @@ export class StatePersistent extends Persistent { * @param config - Configuration object */ public rebuildStorageSideEffect( - state: PersistableState, + state: EnhancedState, storageItemKey: PersistentKey, config: { [key: string]: any } = {} ) { diff --git a/packages/core/src/state/state.ts b/packages/core/src/state/state.ts index 2c23bb5c..a14cfd85 100644 --- a/packages/core/src/state/state.ts +++ b/packages/core/src/state/state.ts @@ -45,14 +45,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; - - // 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. @@ -87,9 +79,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 }); @@ -206,331 +195,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; - } - - /** - * 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` @@ -620,47 +284,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?: { From feefba5fa396cf4acb61cbe2b77146b1dab2ff8b Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Mon, 16 Aug 2021 06:47:07 +0200 Subject: [PATCH 36/93] fixed typo --- .../develop/simple-counter/src/state-manager/Agile.js | 8 ++++---- packages/core/src/state/state.observer.ts | 7 ++++--- packages/core/src/state/state.ts | 7 ------- packages/core/src/storages/index.ts | 2 +- 4 files changed, 9 insertions(+), 15 deletions(-) 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..d67b75de 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,10 @@ import React from 'react'; -import { createState } from '@agile-ts/core'; +import { createLightState } from '@agile-ts/core'; import { useAgile, useValue } from '@agile-ts/react'; -const COUNTER_A = createState(1); -const COUNTER_B = createState(2); -const COUNTER_C = createState(3); +const COUNTER_A = createLightState(1); +const COUNTER_B = createLightState(2); +const COUNTER_C = createLightState(3); const CounterA = () => { const count = useAgile(COUNTER_A); diff --git a/packages/core/src/state/state.observer.ts b/packages/core/src/state/state.observer.ts index ab02dc7c..b7cd7327 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 EnhancedState).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.ts b/packages/core/src/state/state.ts index a14cfd85..c4b5b0ab 100644 --- a/packages/core/src/state/state.ts +++ b/packages/core/src/state/state.ts @@ -1,18 +1,11 @@ import { Agile, - StorageKey, copy, - flatMerge, - isValidObject, StateObserver, Observer, - equal, isFunction, - notEqual, - generateId, ComputedTracker, StateIngestConfigInterface, - removeProperties, LogCodeManager, defineConfig, } from '../internal'; diff --git a/packages/core/src/storages/index.ts b/packages/core/src/storages/index.ts index a0ef25f2..28993733 100644 --- a/packages/core/src/storages/index.ts +++ b/packages/core/src/storages/index.ts @@ -1,4 +1,4 @@ -import { CreateStorageConfigInterface, generateId, Storage } from '../internal'; +import { CreateStorageConfigInterface, Storage } from '../internal'; import type { Storages } from '../internal'; export * from './storages'; From eb8852262c2ef5f0f7c5015f4a878576f1a98bd4 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Mon, 16 Aug 2021 07:56:40 +0200 Subject: [PATCH 37/93] updated tree-shaking test --- examples/plainjs/develop/tree-shaking/package.json | 8 +++----- examples/plainjs/develop/tree-shaking/src/index.js | 7 ++----- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/examples/plainjs/develop/tree-shaking/package.json b/examples/plainjs/develop/tree-shaking/package.json index 53515c71..9f6bb167 100644 --- a/examples/plainjs/develop/tree-shaking/package.json +++ b/examples/plainjs/develop/tree-shaking/package.json @@ -6,8 +6,8 @@ "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", @@ -16,8 +16,6 @@ "webpack-cli": "^4.7.2" }, "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); From b7ec4a2dc0efad9d2e241c455de13fea1e309519 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Mon, 16 Aug 2021 18:28:32 +0200 Subject: [PATCH 38/93] fixed typos --- .../src/collection/collection.persistent.ts | 8 ++--- packages/core/src/computed/index.ts | 8 ++--- packages/core/src/state/index.ts | 8 ++--- packages/core/src/state/state.persistent.ts | 8 ++--- packages/core/src/storages/index.ts | 36 ++++++++++++++++--- packages/core/src/storages/persistent.ts | 8 ++--- 6 files changed, 52 insertions(+), 24 deletions(-) diff --git a/packages/core/src/collection/collection.persistent.ts b/packages/core/src/collection/collection.persistent.ts index c789fab4..2b2232f1 100644 --- a/packages/core/src/collection/collection.persistent.ts +++ b/packages/core/src/collection/collection.persistent.ts @@ -11,7 +11,7 @@ import { Persistent, PersistentKey, StorageKey, - storageManager, + getStorageManager, } from '../internal'; export class CollectionPersistent< @@ -85,7 +85,7 @@ export class CollectionPersistent< // Check if Collection is already persisted // (indicated by the persistence of 'true' at '_storageItemKey') - const isPersisted = await storageManager?.get( + const isPersisted = await getStorageManager()?.get( _storageItemKey, this.config.defaultStorageKey as any ); @@ -208,7 +208,7 @@ export class CollectionPersistent< ); // Set flag in Storage to indicate that the Collection is persisted - storageManager?.set(_storageItemKey, true, this.storageKeys); + getStorageManager()?.set(_storageItemKey, true, this.storageKeys); // Persist default Group defaultGroup.persist(defaultGroupStorageKey, { @@ -283,7 +283,7 @@ export class CollectionPersistent< ); // Remove Collection is persisted indicator flag from Storage - storageManager?.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/computed/index.ts b/packages/core/src/computed/index.ts index 22217093..35219792 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. * @@ -84,3 +80,7 @@ export function createComputed( removeProperties(_config, ['agileInstance']) ); } + +export interface CreateComputedConfigInterfaceWithAgile + extends CreateAgileSubInstanceInterface, + CreateComputedConfigInterface {} diff --git a/packages/core/src/state/index.ts b/packages/core/src/state/index.ts index b99ec509..2c8f1104 100644 --- a/packages/core/src/state/index.ts +++ b/packages/core/src/state/index.ts @@ -13,10 +13,6 @@ export * from './state'; // export * from './state.persistent'; // export * from './state.runtime.job'; -export interface CreateStateConfigInterfaceWithAgile - extends CreateAgileSubInstanceInterface, - StateConfigInterface {} - /** * Returns a newly created State. * @@ -74,3 +70,7 @@ export function createState( removeProperties(config, ['agileInstance']) ); } + +export interface CreateStateConfigInterfaceWithAgile + extends CreateAgileSubInstanceInterface, + StateConfigInterface {} diff --git a/packages/core/src/state/state.persistent.ts b/packages/core/src/state/state.persistent.ts index 45bc8d46..5a1d7f58 100644 --- a/packages/core/src/state/state.persistent.ts +++ b/packages/core/src/state/state.persistent.ts @@ -2,9 +2,9 @@ import { CreatePersistentConfigInterface, defineConfig, EnhancedState, + getStorageManager, Persistent, PersistentKey, - storageManager, } from '../internal'; export class StatePersistent extends Persistent { @@ -73,7 +73,7 @@ export class StatePersistent extends Persistent { const _storageItemKey = storageItemKey ?? this._key; // Load State value from the default Storage - const loadedValue = await storageManager?.get( + const loadedValue = await getStorageManager()?.get( _storageItemKey, this.config.defaultStorageKey as any ); @@ -151,7 +151,7 @@ export class StatePersistent extends Persistent { if (!this.ready) return false; const _storageItemKey = storageItemKey || this._key; this.state().removeSideEffect(StatePersistent.storeValueSideEffectKey); - storageManager?.remove(_storageItemKey, this.storageKeys); + getStorageManager()?.remove(_storageItemKey, this.storageKeys); this.isPersisted = false; return true; } @@ -190,7 +190,7 @@ export class StatePersistent extends Persistent { config: { [key: string]: any } = {} ) { if (config['storage'] == null || config.storage) { - storageManager?.set( + getStorageManager()?.set( storageItemKey, this.state().getPersistableValue(), this.storageKeys diff --git a/packages/core/src/storages/index.ts b/packages/core/src/storages/index.ts index 28993733..c5d9f797 100644 --- a/packages/core/src/storages/index.ts +++ b/packages/core/src/storages/index.ts @@ -1,10 +1,21 @@ -import { CreateStorageConfigInterface, Storage } from '../internal'; -import type { Storages } from '../internal'; +import { + CreateStorageConfigInterface, + Storage, + Storages, + shared, + CreateStoragesConfigInterface, + CreateAgileSubInstanceInterface, + defineConfig, + removeProperties, +} 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. * @@ -24,8 +35,21 @@ export function createStorage(config: CreateStorageConfigInterface): Storage { return new Storage(config); } -// Handles the permanent persistence of Agile Classes -export let storageManager: Storages | null = null; +export function createStorageManager( + config: CreateStorageManagerConfigInterfaceWithAgile = {} +): Storages { + config = defineConfig(config, { + agileInstance: shared, + }); + return new Storages( + config.agileInstance as any, + removeProperties(config, ['agileInstance']) + ); +} + +export function getStorageManager(): Storages | null { + return storageManager; +} export const registerStorageManager = (instance: Storages) => { if (storageManager != null) { @@ -33,3 +57,7 @@ export const registerStorageManager = (instance: Storages) => { } 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 ace74de9..8d869790 100644 --- a/packages/core/src/storages/persistent.ts +++ b/packages/core/src/storages/persistent.ts @@ -2,9 +2,9 @@ import { Agile, copy, defineConfig, + getStorageManager, LogCodeManager, StorageKey, - storageManager, } from '../internal'; export class Persistent { @@ -155,7 +155,7 @@ export class Persistent { // Check if the Storages exist at the specified Storage keys this.storageKeys.map((key) => { - if (!storageManager?.storages[key]) { + if (!getStorageManager()?.storages[key]) { LogCodeManager.log('12:03:02', [this._key, key]); isValid = false; } @@ -190,10 +190,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 = storageManager?.config.defaultStorageKey; + const defaultStorageKey = getStorageManager()?.config.defaultStorageKey; if (defaultStorageKey != null) { this.config.defaultStorageKey = defaultStorageKey; - _storageKeys.push(storageManager?.config.defaultStorageKey as any); + _storageKeys.push(getStorageManager()?.config.defaultStorageKey as any); } } else { this.config.defaultStorageKey = defaultStorageKey ?? _storageKeys[0]; From 136309752dde1222b95cfed8b5ed3412c1fc80a0 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Tue, 17 Aug 2021 06:31:19 +0200 Subject: [PATCH 39/93] fixed typos --- .../react/develop/simple-counter/src/state-manager/Agile.js | 6 ++++-- packages/core/src/internal.ts | 2 +- packages/core/src/state/index.ts | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) 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 d67b75de..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,7 +1,9 @@ import React from 'react'; import { createLightState } from '@agile-ts/core'; -import { useAgile, useValue } from '@agile-ts/react'; +import { useAgile } from '@agile-ts/react'; +// 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); @@ -16,7 +18,7 @@ const CounterA = () => { }; const CounterB = () => { - const count = useValue(COUNTER_B); + const count = useAgile(COUNTER_B); return (
B: {count} diff --git a/packages/core/src/internal.ts b/packages/core/src/internal.ts index 26581a63..f504535e 100644 --- a/packages/core/src/internal.ts +++ b/packages/core/src/internal.ts @@ -36,9 +36,9 @@ 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'; -export * from './state/state.enhanced'; // Computed export * from './computed'; diff --git a/packages/core/src/state/index.ts b/packages/core/src/state/index.ts index 2c8f1104..babcbee5 100644 --- a/packages/core/src/state/index.ts +++ b/packages/core/src/state/index.ts @@ -10,6 +10,7 @@ import { export * from './state'; // export * from './state.observer'; +// export * from './state.enhanced'; // export * from './state.persistent'; // export * from './state.runtime.job'; From 23ee44e755a641f01bc249cf7a7b42ba11ecc0e1 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Tue, 17 Aug 2021 06:54:34 +0200 Subject: [PATCH 40/93] added webpack analyzer to example --- .../react/develop/simple-counter/package.json | 6 +- .../develop/simple-counter/scripts/analyze.js | 20 ++++++ .../react/develop/simple-counter/yarn.lock | 63 ++++++++++++++++++- 3 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 examples/react/develop/simple-counter/scripts/analyze.js 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..3fbf0a66 --- /dev/null +++ b/examples/react/develop/simple-counter/scripts/analyze.js @@ -0,0 +1,20 @@ +// https://medium.com/@hamidihamza/optimize-react-web-apps-with-webpack-bundle-analyzer-6ecb9f162c76 + +process.env.NODE_ENV = 'production'; + +const webpack = require('webpack'); +const BundleAnalyzerPlugin = require('webpack-bundle-analyzer') + .BundleAnalyzerPlugin; +const webpackConfigProd = require('react-scripts/config/webpack.config')( + 'production' +); + +// Add Bundle Analyzer Plugin to React webpack config +webpackConfigProd.plugins.push(new BundleAnalyzerPlugin()); + +// Build project with webpack +webpack(webpackConfigProd, (err, stats) => { + if (err || stats.hasErrors()) { + console.error(err); + } +}); 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" From 414c033fc1911b68fc153b28d920dd4c1afed7b9 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Tue, 17 Aug 2021 21:06:39 +0200 Subject: [PATCH 41/93] fixed typos --- .../develop/simple-counter/scripts/analyze.js | 17 +++++++++++++++-- packages/core/src/agile.ts | 1 - packages/core/src/computed/index.ts | 2 +- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/examples/react/develop/simple-counter/scripts/analyze.js b/examples/react/develop/simple-counter/scripts/analyze.js index 3fbf0a66..9300d1a2 100644 --- a/examples/react/develop/simple-counter/scripts/analyze.js +++ b/examples/react/develop/simple-counter/scripts/analyze.js @@ -1,6 +1,15 @@ // 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) -process.env.NODE_ENV = 'production'; +// https://nodejs.org/docs/latest/api/process.html#process_process_argv +const isDev = process.argv.includes('--dev'); + +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') @@ -8,12 +17,16 @@ const BundleAnalyzerPlugin = require('webpack-bundle-analyzer') 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(webpackConfigProd, (err, stats) => { +webpack(isDev ? webpackConfigDev : webpackConfigProd, (err, stats) => { if (err || stats.hasErrors()) { console.error(err); } diff --git a/packages/core/src/agile.ts b/packages/core/src/agile.ts index 797107df..82fe3bab 100644 --- a/packages/core/src/agile.ts +++ b/packages/core/src/agile.ts @@ -42,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. diff --git a/packages/core/src/computed/index.ts b/packages/core/src/computed/index.ts index 35219792..185dec62 100644 --- a/packages/core/src/computed/index.ts +++ b/packages/core/src/computed/index.ts @@ -24,7 +24,7 @@ export * from './computed'; * 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. From d62cd8912d4948ee535fc8e2bb1501b48c0340fe Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Wed, 18 Aug 2021 21:21:51 +0200 Subject: [PATCH 42/93] added env config --- examples/react/develop/simple-counter/scripts/analyze.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/examples/react/develop/simple-counter/scripts/analyze.js b/examples/react/develop/simple-counter/scripts/analyze.js index 9300d1a2..ea2c0df2 100644 --- a/examples/react/develop/simple-counter/scripts/analyze.js +++ b/examples/react/develop/simple-counter/scripts/analyze.js @@ -2,8 +2,13 @@ // 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'); +const isDev = process.argv.includes('--dev') || process.env.DEV === 'true'; console.log( `Start bundling a '${isDev ? 'development' : 'production'}' build!` From 52d995e8034739b44ff529b70dc391c82ff4ebe8 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Thu, 19 Aug 2021 21:08:51 +0200 Subject: [PATCH 43/93] added warning when adding an existing storage manager --- packages/core/src/logCodeManager.ts | 2 ++ packages/core/src/storages/index.ts | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/core/src/logCodeManager.ts b/packages/core/src/logCodeManager.ts index bb5c9ac6..2e10408e 100644 --- a/packages/core/src/logCodeManager.ts +++ b/packages/core/src/logCodeManager.ts @@ -40,6 +40,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': diff --git a/packages/core/src/storages/index.ts b/packages/core/src/storages/index.ts index c5d9f797..b306d8f9 100644 --- a/packages/core/src/storages/index.ts +++ b/packages/core/src/storages/index.ts @@ -7,6 +7,7 @@ import { CreateAgileSubInstanceInterface, defineConfig, removeProperties, + LogCodeManager, } from '../internal'; export * from './storages'; @@ -53,7 +54,7 @@ export function getStorageManager(): Storages | null { export const registerStorageManager = (instance: Storages) => { if (storageManager != null) { - // TODO print warning + LogCodeManager.log('11:02:06', [], storageManager); } storageManager = instance; }; From 4e614ea43e80d9eac2ac2cd286faa3d99b0992a6 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Fri, 20 Aug 2021 19:47:07 +0200 Subject: [PATCH 44/93] fixed typo --- packages/core/src/storages/index.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/core/src/storages/index.ts b/packages/core/src/storages/index.ts index b306d8f9..3b4586cc 100644 --- a/packages/core/src/storages/index.ts +++ b/packages/core/src/storages/index.ts @@ -36,6 +36,13 @@ export function createStorage(config: CreateStorageConfigInterface): Storage { return new Storage(config); } +/** + * Returns a newly created Storage Manager. + * + * A Storage Manager manages the Storages. + * + * @param config - Configuration object + */ export function createStorageManager( config: CreateStorageManagerConfigInterfaceWithAgile = {} ): Storages { @@ -48,10 +55,18 @@ export function createStorageManager( ); } +/** + * Returns the current Storage Manager. + */ export function getStorageManager(): Storages | null { return storageManager; } +/** + * Registers a Storage Manager. + * + * @param instance - Storage Manager to be registered as the default Storage Manager. + */ export const registerStorageManager = (instance: Storages) => { if (storageManager != null) { LogCodeManager.log('11:02:06', [], storageManager); From 0226cab8d5eacb16e0fa8b7a6e3631d48d0cd94d Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sat, 21 Aug 2021 07:47:50 +0200 Subject: [PATCH 45/93] updated registerStorageManager name --- .../develop/functional-component-ts/src/core/index.ts | 11 +++++++---- packages/core/src/storages/index.ts | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) 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..89ce58f3 100644 --- a/examples/react/develop/functional-component-ts/src/core/index.ts +++ b/examples/react/develop/functional-component-ts/src/core/index.ts @@ -4,7 +4,9 @@ import Agile, { createComputed, createState, createStorage, + createStorageManager, Item, + registerSharedStorageManager, } from '@agile-ts/core'; import Event from '@agile-ts/event'; import { assignSharedAgileLoggerConfig, Logger } from '@agile-ts/logger'; @@ -13,13 +15,14 @@ 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); +const storageManager = createStorageManager({ localStorage: true }); +registerSharedStorageManager(storageManager); + // Register custom second Storage -App.registerStorage( +storageManager.register( createStorage({ key: 'myStorage', methods: { diff --git a/packages/core/src/storages/index.ts b/packages/core/src/storages/index.ts index 3b4586cc..86ea5528 100644 --- a/packages/core/src/storages/index.ts +++ b/packages/core/src/storages/index.ts @@ -67,7 +67,7 @@ export function getStorageManager(): Storages | null { * * @param instance - Storage Manager to be registered as the default Storage Manager. */ -export const registerStorageManager = (instance: Storages) => { +export const registerSharedStorageManager = (instance: Storages) => { if (storageManager != null) { LogCodeManager.log('11:02:06', [], storageManager); } From e6d1adc1c2ac592d4cb768a6c250b3ad3a173285 Mon Sep 17 00:00:00 2001 From: BennoDev Date: Sun, 22 Aug 2021 08:02:32 +0200 Subject: [PATCH 46/93] fixed typos --- .../develop/functional-component-ts/yarn.lock | 28 +++++++++---------- packages/core/src/storages/index.ts | 9 ++++-- yarn.lock | 14 +++++++++- 3 files changed, 33 insertions(+), 18 deletions(-) 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/packages/core/src/storages/index.ts b/packages/core/src/storages/index.ts index 3b4586cc..2da27474 100644 --- a/packages/core/src/storages/index.ts +++ b/packages/core/src/storages/index.ts @@ -39,7 +39,9 @@ export function createStorage(config: CreateStorageConfigInterface): Storage { /** * Returns a newly created Storage Manager. * - * A Storage Manager manages the Storages. + * A Storage Manager manages all external Storages for an AgileTs + * and provides an interface to easily store, + * load and remove values from multiple Storages at once. * * @param config - Configuration object */ @@ -56,14 +58,15 @@ export function createStorageManager( } /** - * Returns the current Storage Manager. + * Returns the current registered Storage Manager. */ export function getStorageManager(): Storages | null { return storageManager; } /** - * Registers a Storage Manager. + * Registers the specified Storage Manager + * as default Storage Manager for all Agile Instances. * * @param instance - Storage Manager to be registered as the default Storage Manager. */ 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" From 5d2d04ce76af084bf945cbda22e7f096d358a9da Mon Sep 17 00:00:00 2001 From: BennoDev Date: Mon, 23 Aug 2021 08:02:31 +0200 Subject: [PATCH 47/93] fixed typos --- packages/core/src/state/index.ts | 10 +- packages/core/tests/unit/agile.test.ts | 54 -- .../tests/unit/state/state.enhanced.test.ts | 811 ++++++++++++++++++ packages/core/tests/unit/state/state.test.ts | 699 +-------------- 4 files changed, 823 insertions(+), 751 deletions(-) create mode 100644 packages/core/tests/unit/state/state.enhanced.test.ts diff --git a/packages/core/src/state/index.ts b/packages/core/src/state/index.ts index babcbee5..84b1ece9 100644 --- a/packages/core/src/state/index.ts +++ b/packages/core/src/state/index.ts @@ -44,13 +44,17 @@ export function createLightState( } /** - * Returns a newly created State. + * Returns a newly created enhanced State. * - * A State manages a piece of Information + * A 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. * - * You can create as many global States as you need. + * 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) * diff --git a/packages/core/tests/unit/agile.test.ts b/packages/core/tests/unit/agile.test.ts index ee68d7ee..9d18f45b 100644 --- a/packages/core/tests/unit/agile.test.ts +++ b/packages/core/tests/unit/agile.test.ts @@ -93,10 +93,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 +102,6 @@ describe('Agile Tests', () => { const agile = new Agile({ waitForMount: false, bucket: false, - localStorage: true, bindGlobal: true, key: 'jeff', autoIntegrate: false, @@ -125,10 +120,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 +165,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 +172,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/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.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 */ From 0c0a10ab7066e844c19ae4f9c17bbc26451c0143 Mon Sep 17 00:00:00 2001 From: BennoDev Date: Mon, 23 Aug 2021 17:47:56 +0200 Subject: [PATCH 48/93] fixed typos --- .../collection/collection.persistent.test.ts | 20 +-- .../tests/unit/collection/collection.test.ts | 10 +- .../collection/group/group.observer.test.ts | 2 +- .../tests/unit/collection/group/group.test.ts | 2 +- .../core/tests/unit/collection/item.test.ts | 6 +- .../tests/unit/collection/selector.test.ts | 50 ++----- .../core/tests/unit/computed/computed.test.ts | 2 +- .../unit/computed/computed.tracker.test.ts | 2 +- .../unit/integrations/integrations.test.ts | 2 +- .../tests/unit/runtime/runtime.job.test.ts | 2 +- .../core/tests/unit/runtime/runtime.test.ts | 2 +- .../subscription/sub.controller.test.ts | 139 ++++++++++-------- .../tests/unit/state/state.observer.test.ts | 2 +- .../tests/unit/state/state.persistent.test.ts | 6 +- .../unit/state/state.runtime.job.test.ts | 2 +- .../tests/unit/storages/persistent.test.ts | 2 +- .../core/tests/unit/storages/storages.test.ts | 2 +- packages/core/tests/unit/utils.test.ts | 22 ++- 18 files changed, 128 insertions(+), 147 deletions(-) diff --git a/packages/core/tests/unit/collection/collection.persistent.test.ts b/packages/core/tests/unit/collection/collection.persistent.test.ts index 838587fd..a3cd2cef 100644 --- a/packages/core/tests/unit/collection/collection.persistent.test.ts +++ b/packages/core/tests/unit/collection/collection.persistent.test.ts @@ -22,7 +22,7 @@ describe('CollectionPersistent Tests', () => { beforeEach(() => { LogMock.mockLogs(); - dummyAgile = new Agile({ localStorage: false }); + dummyAgile = new Agile(); dummyCollection = new Collection(dummyAgile, { key: 'dummyCollectionKey', }); @@ -417,9 +417,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 @@ -568,9 +569,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( @@ -840,9 +842,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 } diff --git a/packages/core/tests/unit/collection/collection.test.ts b/packages/core/tests/unit/collection/collection.test.ts index 086079f4..89684604 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); 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..58788ede 100644 --- a/packages/core/tests/unit/collection/group/group.test.ts +++ b/packages/core/tests/unit/collection/group/group.test.ts @@ -23,7 +23,7 @@ describe('Group Tests', () => { beforeEach(() => { LogMock.mockLogs(); - dummyAgile = new Agile({ localStorage: false }); + dummyAgile = new Agile(); dummyCollection = new Collection(dummyAgile, { key: 'dummyCollection', }); diff --git a/packages/core/tests/unit/collection/item.test.ts b/packages/core/tests/unit/collection/item.test.ts index 38538da9..3e6b41df 100644 --- a/packages/core/tests/unit/collection/item.test.ts +++ b/packages/core/tests/unit/collection/item.test.ts @@ -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'); @@ -315,9 +315,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..d50326c9 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'); 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/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/state/state.observer.test.ts b/packages/core/tests/unit/state/state.observer.test.ts index 53ebd1dd..432e6c12 100644 --- a/packages/core/tests/unit/state/state.observer.test.ts +++ b/packages/core/tests/unit/state/state.observer.test.ts @@ -19,7 +19,7 @@ describe('StateObserver Tests', () => { beforeEach(() => { LogMock.mockLogs(); - dummyAgile = new Agile({ localStorage: false }); + dummyAgile = new Agile(); dummyState = new State(dummyAgile, 'dummyValue', { key: 'dummyState' }); jest.clearAllMocks(); diff --git a/packages/core/tests/unit/state/state.persistent.test.ts b/packages/core/tests/unit/state/state.persistent.test.ts index 65603523..d02103ea 100644 --- a/packages/core/tests/unit/state/state.persistent.test.ts +++ b/packages/core/tests/unit/state/state.persistent.test.ts @@ -14,7 +14,7 @@ describe('StatePersistent Tests', () => { beforeEach(() => { LogMock.mockLogs(); - dummyAgile = new Agile({ localStorage: false }); + dummyAgile = new Agile(); dummyState = new State(dummyAgile, 'dummyValue'); jest.spyOn(StatePersistent.prototype, 'instantiatePersistent'); @@ -308,9 +308,7 @@ describe('StatePersistent Tests', () => { () => { statePersistent.setupSideEffects(); - expect( - dummyState.addSideEffect - ).toHaveBeenCalledWith( + expect(dummyState.addSideEffect).toHaveBeenCalledWith( StatePersistent.storeValueSideEffectKey, expect.any(Function), { weight: 0 } diff --git a/packages/core/tests/unit/state/state.runtime.job.test.ts b/packages/core/tests/unit/state/state.runtime.job.test.ts index 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/storages/persistent.test.ts b/packages/core/tests/unit/storages/persistent.test.ts index a0dd3407..750d90d9 100644 --- a/packages/core/tests/unit/storages/persistent.test.ts +++ b/packages/core/tests/unit/storages/persistent.test.ts @@ -7,7 +7,7 @@ describe('Persistent Tests', () => { beforeEach(() => { LogMock.mockLogs(); - dummyAgile = new Agile({ localStorage: false }); + dummyAgile = new Agile(); jest.spyOn(Persistent.prototype, 'instantiatePersistent'); diff --git a/packages/core/tests/unit/storages/storages.test.ts b/packages/core/tests/unit/storages/storages.test.ts index 139a7e29..ac95a196 100644 --- a/packages/core/tests/unit/storages/storages.test.ts +++ b/packages/core/tests/unit/storages/storages.test.ts @@ -7,7 +7,7 @@ describe('Storages Tests', () => { beforeEach(() => { LogMock.mockLogs(); - dummyAgile = new Agile({ localStorage: false }); + dummyAgile = new Agile(); jest.spyOn(Storages.prototype, 'instantiateLocalStorage'); 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); From c36956206b68e8b1ad82c481e659c51bd4445dc5 Mon Sep 17 00:00:00 2001 From: BennoDev Date: Tue, 24 Aug 2021 18:16:52 +0200 Subject: [PATCH 49/93] fixed typos --- .../collection/collection.persistent.test.ts | 62 +++++++++++-------- .../tests/unit/collection/group/group.test.ts | 38 +++++++----- .../core/tests/unit/collection/item.test.ts | 44 +++++++------ .../core/tests/unit/computed/computed.test.ts | 12 ---- .../tests/unit/state/state.observer.test.ts | 8 ++- .../tests/unit/state/state.persistent.test.ts | 58 +++++++++-------- 6 files changed, 122 insertions(+), 100 deletions(-) diff --git a/packages/core/tests/unit/collection/collection.persistent.test.ts b/packages/core/tests/unit/collection/collection.persistent.test.ts index a3cd2cef..b4b1c4b1 100644 --- a/packages/core/tests/unit/collection/collection.persistent.test.ts +++ b/packages/core/tests/unit/collection/collection.persistent.test.ts @@ -7,8 +7,13 @@ import { StatePersistent, Group, Item, + registerSharedStorageManager, + createStorageManager, + getStorageManager, + Storages, } from '../../../src'; import { LogMock } from '../../helper/logMock'; +import waitForExpect from 'wait-for-expect'; describe('CollectionPersistent Tests', () => { interface ItemInterface { @@ -18,6 +23,7 @@ describe('CollectionPersistent Tests', () => { let dummyAgile: Agile; let dummyCollection: Collection; + let storageManager: Storages; beforeEach(() => { LogMock.mockLogs(); @@ -27,6 +33,10 @@ describe('CollectionPersistent Tests', () => { key: 'dummyCollectionKey', }); + // Register Storage Manager + registerSharedStorageManager(createStorageManager()); + storageManager = getStorageManager() as any; + jest.spyOn(CollectionPersistent.prototype, 'instantiatePersistent'); jest.spyOn(CollectionPersistent.prototype, 'initialLoading'); @@ -169,7 +179,7 @@ describe('CollectionPersistent Tests', () => { key: 'collectionPersistentKey', storageKeys: ['dummyStorage'], }); - dummyAgile.registerStorage( + storageManager.register( new Storage({ key: 'dummyStorage', methods: { @@ -226,8 +236,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 +278,7 @@ describe('CollectionPersistent Tests', () => { ); dummyCollection.assignItem = jest.fn(); - dummyAgile.storages.get = jest.fn(); + storageManager.get = jest.fn(); }); it( @@ -277,7 +289,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 +297,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 +350,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 +385,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 ); @@ -487,7 +499,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 () { @@ -508,7 +520,7 @@ describe('CollectionPersistent Tests', () => { ); expect(response).toBeTruthy(); - expect(dummyAgile.storages.get).toHaveBeenCalledWith( + expect(storageManager.get).toHaveBeenCalledWith( 'dummyKey', collectionPersistent.config.defaultStorageKey ); @@ -583,14 +595,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 ); @@ -614,7 +626,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(); @@ -631,7 +643,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); @@ -639,7 +651,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 ); @@ -684,7 +696,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 () => { @@ -693,7 +705,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 @@ -745,7 +757,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 @@ -791,7 +803,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(); @@ -810,7 +822,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(); @@ -919,7 +931,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 +940,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 +986,7 @@ describe('CollectionPersistent Tests', () => { ); expect(response).toBeTruthy(); - expect(dummyAgile.storages.remove).toHaveBeenCalledWith( + expect(storageManager.remove).toHaveBeenCalledWith( 'dummyKey', collectionPersistent.storageKeys ); @@ -1012,7 +1024,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 +1049,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/group/group.test.ts b/packages/core/tests/unit/collection/group/group.test.ts index 58788ede..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'; @@ -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/item.test.ts b/packages/core/tests/unit/collection/item.test.ts index 3e6b41df..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'; @@ -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, + } + ); }); }); diff --git a/packages/core/tests/unit/computed/computed.test.ts b/packages/core/tests/unit/computed/computed.test.ts index d50326c9..59b1d3ed 100644 --- a/packages/core/tests/unit/computed/computed.test.ts +++ b/packages/core/tests/unit/computed/computed.test.ts @@ -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/state/state.observer.test.ts b/packages/core/tests/unit/state/state.observer.test.ts index 432e6c12..8a40d53c 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, SubscriptionContainer, + EnhancedState, } from '../../../src'; import * as Utils from '@agile-ts/utils'; import { LogMock } from '../../helper/logMock'; @@ -14,13 +14,15 @@ import waitForExpect from 'wait-for-expect'; describe('StateObserver Tests', () => { let dummyAgile: Agile; - let dummyState: State; + let dummyState: EnhancedState; beforeEach(() => { LogMock.mockLogs(); dummyAgile = new Agile(); - dummyState = new State(dummyAgile, 'dummyValue', { key: 'dummyState' }); + dummyState = new EnhancedState(dummyAgile, 'dummyValue', { + key: 'dummyState', + }); jest.clearAllMocks(); }); diff --git a/packages/core/tests/unit/state/state.persistent.test.ts b/packages/core/tests/unit/state/state.persistent.test.ts index d02103ea..a5870b98 100644 --- a/packages/core/tests/unit/state/state.persistent.test.ts +++ b/packages/core/tests/unit/state/state.persistent.test.ts @@ -1,21 +1,31 @@ import { Agile, - State, StatePersistent, Storage, Persistent, + EnhancedState, + getStorageManager, + Storages, + registerSharedStorageManager, + 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(); - dummyState = new State(dummyAgile, 'dummyValue'); + dummyState = new EnhancedState(dummyAgile, 'dummyValue'); + + // Register Storage Manager + registerSharedStorageManager(createStorageManager()); + storageManager = getStorageManager() as any; jest.spyOn(StatePersistent.prototype, 'instantiatePersistent'); jest.spyOn(StatePersistent.prototype, 'initialLoading'); @@ -122,7 +132,7 @@ describe('StatePersistent Tests', () => { key: 'statePersistentKey', storageKeys: ['dummyStorage'], }); - dummyAgile.registerStorage( + storageManager.register( new Storage({ key: 'dummyStorage', methods: { @@ -142,8 +152,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 +170,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 +196,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 +215,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 +241,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(); } @@ -362,7 +370,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; }); @@ -376,7 +384,7 @@ describe('StatePersistent Tests', () => { expect(dummyState.removeSideEffect).toHaveBeenCalledWith( StatePersistent.storeValueSideEffectKey ); - expect(dummyAgile.storages.remove).toHaveBeenCalledWith( + expect(storageManager.remove).toHaveBeenCalledWith( statePersistent._key, statePersistent.storageKeys ); @@ -392,7 +400,7 @@ describe('StatePersistent Tests', () => { expect(dummyState.removeSideEffect).toHaveBeenCalledWith( StatePersistent.storeValueSideEffectKey ); - expect(dummyAgile.storages.remove).toHaveBeenCalledWith( + expect(storageManager.remove).toHaveBeenCalledWith( 'coolKey', statePersistent.storageKeys ); @@ -406,7 +414,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(); }); }); @@ -448,13 +456,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 @@ -466,7 +474,7 @@ describe('StatePersistent Tests', () => { storage: false, }); - expect(dummyAgile.storages.set).not.toHaveBeenCalled(); + expect(storageManager.set).not.toHaveBeenCalled(); }); }); }); From 4c322d011f1db97f574b359c22eb35f65f2b331d Mon Sep 17 00:00:00 2001 From: BennoDev Date: Wed, 25 Aug 2021 13:39:50 +0200 Subject: [PATCH 50/93] fixed typos --- .../functional-component-ts/src/core/index.ts | 2 +- examples/react/release/boxes/package.json | 6 ++-- examples/react/release/boxes/yarn.lock | 4 +-- packages/core/src/storages/index.ts | 9 ++++++ packages/core/src/storages/persistent.ts | 8 ++++- .../tests/unit/storages/persistent.test.ts | 32 ++++++++++++------- 6 files changed, 41 insertions(+), 20 deletions(-) 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 89ce58f3..207bce0d 100644 --- a/examples/react/develop/functional-component-ts/src/core/index.ts +++ b/examples/react/develop/functional-component-ts/src/core/index.ts @@ -18,7 +18,7 @@ assignSharedAgileLoggerConfig({ level: Logger.level.DEBUG }); export const App = new Agile(); assignSharedAgileInstance(App); -const storageManager = createStorageManager({ localStorage: true }); +export const storageManager = createStorageManager({ localStorage: true }); registerSharedStorageManager(storageManager); // Register custom second Storage diff --git a/examples/react/release/boxes/package.json b/examples/react/release/boxes/package.json index ebb40840..078a67a8 100644 --- a/examples/react/release/boxes/package.json +++ b/examples/react/release/boxes/package.json @@ -4,9 +4,9 @@ "private": true, "dependencies": { "@agile-ts/core": "file:.yalc/@agile-ts/core", - "@agile-ts/logger": "^0.0.7", - "@agile-ts/proxytree": "^0.0.5", - "@agile-ts/react": "^0.1.2", + "@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/yarn.lock b/examples/react/release/boxes/yarn.lock index 18e63613..a63eec3f 100644 --- a/examples/react/release/boxes/yarn.lock +++ b/examples/react/release/boxes/yarn.lock @@ -2,10 +2,8 @@ # yarn lockfile v1 -"@agile-ts/core@^0.1.2": +"@agile-ts/core@file:.yalc/@agile-ts/core": version "0.1.2" - resolved "https://registry.yarnpkg.com/@agile-ts/core/-/core-0.1.2.tgz#5a3974ba0c57a51a19bcdf81b2055e091c884f5e" - integrity sha512-9031MGUrPpg/ZL1ErpwUlHX751HKEtOfbc5Ae7W7x/POGH89Gka09hMAhqQlDrKF2+olVs3sf6PAsAHRv6paGw== dependencies: "@agile-ts/utils" "^0.0.7" diff --git a/packages/core/src/storages/index.ts b/packages/core/src/storages/index.ts index 1db40db5..8afb0ad0 100644 --- a/packages/core/src/storages/index.ts +++ b/packages/core/src/storages/index.ts @@ -8,6 +8,7 @@ import { defineConfig, removeProperties, LogCodeManager, + runsOnServer, } from '../internal'; export * from './storages'; @@ -61,6 +62,14 @@ export function createStorageManager( * Returns the current registered Storage Manager. */ export function getStorageManager(): Storages | null { + if (storageManager == null) { + const newStorageManager = createStorageManager({ + localStorage: !runsOnServer(), + }); + registerSharedStorageManager(newStorageManager); + return newStorageManager; + } + return storageManager; } diff --git a/packages/core/src/storages/persistent.ts b/packages/core/src/storages/persistent.ts index 8d869790..10d5967f 100644 --- a/packages/core/src/storages/persistent.ts +++ b/packages/core/src/storages/persistent.ts @@ -126,7 +126,13 @@ export class Persistent { ) { this._key = this.formatKey(config.key) ?? Persistent.placeHolderKey; this.assignStorageKeys(config.storageKeys, config.defaultStorageKey); - this.validatePersistent(); + const isValid = this.validatePersistent(); + + // Register Persistent to Storage Manager + const storageManager = getStorageManager(); + if (isValid && storageManager != null) { + storageManager.persistentInstances[this._key] = this; + } } /** diff --git a/packages/core/tests/unit/storages/persistent.test.ts b/packages/core/tests/unit/storages/persistent.test.ts index 750d90d9..9001741c 100644 --- a/packages/core/tests/unit/storages/persistent.test.ts +++ b/packages/core/tests/unit/storages/persistent.test.ts @@ -1,14 +1,28 @@ -import { Agile, Persistent, Storage, createStorage } from '../../../src'; +import { + Agile, + Persistent, + Storage, + createStorage, + Storages, + registerSharedStorageManager, + createStorageManager, + getStorageManager, +} from '../../../src'; import { LogMock } from '../../helper/logMock'; describe('Persistent Tests', () => { let dummyAgile: Agile; + let storageManager: Storages; beforeEach(() => { LogMock.mockLogs(); dummyAgile = new Agile(); + // Register Storage Manager + registerSharedStorageManager(createStorageManager()); + storageManager = getStorageManager() as any; + jest.spyOn(Persistent.prototype, 'instantiatePersistent'); jest.clearAllMocks(); @@ -28,9 +42,7 @@ describe('Persistent Tests', () => { key: undefined, defaultStorageKey: null, }); - expect( - dummyAgile.storages.persistentInstances.has(persistent) - ).toBeTruthy(); + expect(storageManager.persistentInstances.has(persistent)).toBeTruthy(); expect(persistent._key).toBe(Persistent.placeHolderKey); expect(persistent.ready).toBeFalsy(); @@ -58,9 +70,7 @@ describe('Persistent Tests', () => { key: 'persistentKey', defaultStorageKey: 'test1', }); - expect( - dummyAgile.storages.persistentInstances.has(persistent) - ).toBeTruthy(); + expect(storageManager.persistentInstances.has(persistent)).toBeTruthy(); expect(persistent._key).toBe(Persistent.placeHolderKey); expect(persistent.ready).toBeFalsy(); @@ -80,9 +90,7 @@ describe('Persistent Tests', () => { expect(persistent).toBeInstanceOf(Persistent); expect(persistent.instantiatePersistent).not.toHaveBeenCalled(); - expect( - dummyAgile.storages.persistentInstances.has(persistent) - ).toBeTruthy(); + expect(storageManager.persistentInstances.has(persistent)).toBeTruthy(); expect(persistent._key).toBe(Persistent.placeHolderKey); expect(persistent.ready).toBeFalsy(); @@ -262,7 +270,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 +345,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: { From ad1ad60b2c0bfb98960632c2167740978c3a8a66 Mon Sep 17 00:00:00 2001 From: BennoDev Date: Wed, 25 Aug 2021 15:05:38 +0200 Subject: [PATCH 51/93] fixed persistent tests --- .../tests/unit/storages/persistent.test.ts | 75 ++++++++++++++----- 1 file changed, 57 insertions(+), 18 deletions(-) diff --git a/packages/core/tests/unit/storages/persistent.test.ts b/packages/core/tests/unit/storages/persistent.test.ts index 9001741c..988a1579 100644 --- a/packages/core/tests/unit/storages/persistent.test.ts +++ b/packages/core/tests/unit/storages/persistent.test.ts @@ -42,7 +42,6 @@ describe('Persistent Tests', () => { key: undefined, defaultStorageKey: null, }); - expect(storageManager.persistentInstances.has(persistent)).toBeTruthy(); expect(persistent._key).toBe(Persistent.placeHolderKey); expect(persistent.ready).toBeFalsy(); @@ -70,7 +69,6 @@ describe('Persistent Tests', () => { key: 'persistentKey', defaultStorageKey: 'test1', }); - expect(storageManager.persistentInstances.has(persistent)).toBeTruthy(); expect(persistent._key).toBe(Persistent.placeHolderKey); expect(persistent.ready).toBeFalsy(); @@ -90,7 +88,6 @@ describe('Persistent Tests', () => { expect(persistent).toBeInstanceOf(Persistent); expect(persistent.instantiatePersistent).not.toHaveBeenCalled(); - expect(storageManager.persistentInstances.has(persistent)).toBeTruthy(); expect(persistent._key).toBe(Persistent.placeHolderKey); expect(persistent.ready).toBeFalsy(); @@ -196,25 +193,67 @@ 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 Storage Manager when Persistent is valid', + () => { + jest + .spyOn(persistent, 'validatePersistent') + .mockReturnValueOnce(true); + + 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 Storage Manager when Persistent isn't valid", + () => { + jest + .spyOn(persistent, 'validatePersistent') + .mockReturnValueOnce(false); + + 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).not.toHaveProperty( + 'persistentKey' + ); + } + ); }); describe('validatePersistent function tests', () => { From 6a8e069c8e9796698add25c38d5b31500804f7f5 Mon Sep 17 00:00:00 2001 From: BennoDev Date: Wed, 25 Aug 2021 15:35:45 +0200 Subject: [PATCH 52/93] fixed tests --- packages/core/src/storages/persistent.ts | 4 ++-- .../tests/unit/storages/persistent.test.ts | 18 +++++------------- .../core/tests/unit/storages/storages.test.ts | 15 +++++++++++---- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/packages/core/src/storages/persistent.ts b/packages/core/src/storages/persistent.ts index 10d5967f..11deec56 100644 --- a/packages/core/src/storages/persistent.ts +++ b/packages/core/src/storages/persistent.ts @@ -126,11 +126,11 @@ export class Persistent { ) { this._key = this.formatKey(config.key) ?? Persistent.placeHolderKey; this.assignStorageKeys(config.storageKeys, config.defaultStorageKey); - const isValid = this.validatePersistent(); + this.validatePersistent(); // Register Persistent to Storage Manager const storageManager = getStorageManager(); - if (isValid && storageManager != null) { + if (this._key !== Persistent.placeHolderKey && storageManager != null) { storageManager.persistentInstances[this._key] = this; } } diff --git a/packages/core/tests/unit/storages/persistent.test.ts b/packages/core/tests/unit/storages/persistent.test.ts index 988a1579..a6673cb0 100644 --- a/packages/core/tests/unit/storages/persistent.test.ts +++ b/packages/core/tests/unit/storages/persistent.test.ts @@ -196,16 +196,13 @@ describe('Persistent Tests', () => { beforeEach(() => { jest.spyOn(persistent, 'formatKey'); jest.spyOn(persistent, 'assignStorageKeys'); + jest.spyOn(persistent, 'validatePersistent'); }); it( 'should call formatKey, assignStorageKeys, validatePersistent ' + - 'and add Persistent to the Storage Manager when Persistent is valid', + 'and add Persistent to the Storage Manager when Persistent has a valid key', () => { - jest - .spyOn(persistent, 'validatePersistent') - .mockReturnValueOnce(true); - persistent.instantiatePersistent({ key: 'persistentKey', storageKeys: ['myName', 'is', 'jeff'], @@ -230,20 +227,15 @@ describe('Persistent Tests', () => { it( 'should call formatKey, assignStorageKeys, validatePersistent ' + - "and shouldn't add Persistent to the Storage Manager when Persistent isn't valid", + "and shouldn't add Persistent to the Storage Manager when Persistent has no valid key", () => { - jest - .spyOn(persistent, 'validatePersistent') - .mockReturnValueOnce(false); - persistent.instantiatePersistent({ - key: 'persistentKey', storageKeys: ['myName', 'is', 'jeff'], defaultStorageKey: 'jeff', }); - expect(persistent._key).toBe('persistentKey'); - expect(persistent.formatKey).toHaveBeenCalledWith('persistentKey'); + expect(persistent._key).toBe(Persistent.placeHolderKey); + expect(persistent.formatKey).toHaveBeenCalledWith(undefined); expect(persistent.assignStorageKeys).toHaveBeenCalledWith( ['myName', 'is', 'jeff'], 'jeff' diff --git a/packages/core/tests/unit/storages/storages.test.ts b/packages/core/tests/unit/storages/storages.test.ts index ac95a196..f19d3df1 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, + registerSharedStorageManager, +} from '../../../src'; import { LogMock } from '../../helper/logMock'; describe('Storages Tests', () => { @@ -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; + registerSharedStorageManager(storages); + dummyStorageMethods = { get: jest.fn(), set: jest.fn(), From fd0f56909e28ba023c2ca57781e1c5e92404a11e Mon Sep 17 00:00:00 2001 From: BennoDev Date: Wed, 25 Aug 2021 16:03:51 +0200 Subject: [PATCH 53/93] fixed typos --- packages/core/src/state/index.ts | 2 +- packages/core/src/state/state.enhanced.ts | 18 ++++++++++++++++++ packages/core/src/storages/index.ts | 8 ++++---- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/packages/core/src/state/index.ts b/packages/core/src/state/index.ts index 84b1ece9..a6152f54 100644 --- a/packages/core/src/state/index.ts +++ b/packages/core/src/state/index.ts @@ -46,7 +46,7 @@ export function createLightState( /** * Returns a newly created enhanced State. * - * A enhanced State manages, like a normal State, a piece of Information + * 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. * diff --git a/packages/core/src/state/state.enhanced.ts b/packages/core/src/state/state.enhanced.ts index 5dcde925..fe3a4c67 100644 --- a/packages/core/src/state/state.enhanced.ts +++ b/packages/core/src/state/state.enhanced.ts @@ -32,6 +32,24 @@ export class EnhancedState extends State { // 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, diff --git a/packages/core/src/storages/index.ts b/packages/core/src/storages/index.ts index 8afb0ad0..86992999 100644 --- a/packages/core/src/storages/index.ts +++ b/packages/core/src/storages/index.ts @@ -40,7 +40,7 @@ export function createStorage(config: CreateStorageConfigInterface): Storage { /** * Returns a newly created Storage Manager. * - * A Storage Manager manages all external Storages for an AgileTs + * A Storage Manager manages all external Storages for AgileTs * and provides an interface to easily store, * load and remove values from multiple Storages at once. * @@ -59,9 +59,10 @@ export function createStorageManager( } /** - * Returns the current registered Storage Manager. + * Returns the shared Storage Manager + * or creates a new one when no shared Storage Manager exists. */ -export function getStorageManager(): Storages | null { +export function getStorageManager(): Storages { if (storageManager == null) { const newStorageManager = createStorageManager({ localStorage: !runsOnServer(), @@ -69,7 +70,6 @@ export function getStorageManager(): Storages | null { registerSharedStorageManager(newStorageManager); return newStorageManager; } - return storageManager; } From ae48e86e12b5ac32c426d0c36a06dd424fd7fefa Mon Sep 17 00:00:00 2001 From: BennoDev Date: Wed, 25 Aug 2021 17:49:12 +0200 Subject: [PATCH 54/93] fixed typos --- packages/core/src/state/index.ts | 1 + packages/core/src/state/state.observer.ts | 2 +- packages/core/src/storages/index.ts | 2 +- packages/core/src/storages/storages.ts | 2 +- .../core/tests/unit/state/state.observer.test.ts | 13 ++++++------- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/core/src/state/index.ts b/packages/core/src/state/index.ts index a6152f54..7bc1d192 100644 --- a/packages/core/src/state/index.ts +++ b/packages/core/src/state/index.ts @@ -43,6 +43,7 @@ export function createLightState( ); } +// TODO 'createState' doesn't get entirely treeshaken away /** * Returns a newly created enhanced State. * diff --git a/packages/core/src/state/state.observer.ts b/packages/core/src/state/state.observer.ts index b7cd7327..f2ef181c 100644 --- a/packages/core/src/state/state.observer.ts +++ b/packages/core/src/state/state.observer.ts @@ -107,7 +107,7 @@ export class StateObserver extends Observer { } // Assign next State value to Observer and compute it if necessary (enhanced State) - this.nextStateValue = (state as EnhancedState).computeValueMethod + this.nextStateValue = (state as any).computeValueMethod ? copy((state as any).computeValueMethod(newStateValue)) : copy(newStateValue); diff --git a/packages/core/src/storages/index.ts b/packages/core/src/storages/index.ts index 86992999..020a2525 100644 --- a/packages/core/src/storages/index.ts +++ b/packages/core/src/storages/index.ts @@ -42,7 +42,7 @@ export function createStorage(config: CreateStorageConfigInterface): Storage { * * A Storage Manager manages all external Storages for AgileTs * and provides an interface to easily store, - * load and remove values from multiple Storages at once. + * load and remove values from multiple external Storages at once. * * @param config - Configuration object */ diff --git a/packages/core/src/storages/storages.ts b/packages/core/src/storages/storages.ts index dd0f9ea1..36a05903 100644 --- a/packages/core/src/storages/storages.ts +++ b/packages/core/src/storages/storages.ts @@ -97,7 +97,7 @@ export class Storages { this.storages[storage.key] = storage; if (config.default) this.config.defaultStorageKey = storage.key; - for (const persistentKey in this.persistentInstances) { + for (const persistentKey in Object.keys(this.persistentInstances)) { const persistent = this.persistentInstances[persistentKey]; // Revalidate Persistent, which contains key/name identifier of the newly registered Storage diff --git a/packages/core/tests/unit/state/state.observer.test.ts b/packages/core/tests/unit/state/state.observer.test.ts index 8a40d53c..e6e64367 100644 --- a/packages/core/tests/unit/state/state.observer.test.ts +++ b/packages/core/tests/unit/state/state.observer.test.ts @@ -4,9 +4,9 @@ import { StateRuntimeJob, Observer, StateObserver, - StatePersistent, - SubscriptionContainer, EnhancedState, + SubscriptionContainer, + State, } from '../../../src'; import * as Utils from '@agile-ts/utils'; import { LogMock } from '../../helper/logMock'; @@ -14,13 +14,13 @@ import waitForExpect from 'wait-for-expect'; describe('StateObserver Tests', () => { let dummyAgile: Agile; - let dummyState: EnhancedState; + let dummyState: State; beforeEach(() => { LogMock.mockLogs(); dummyAgile = new Agile(); - dummyState = new EnhancedState(dummyAgile, 'dummyValue', { + dummyState = new State(dummyAgile, 'dummyValue', { key: 'dummyState', }); @@ -319,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'); @@ -343,8 +344,6 @@ describe('StateObserver Tests', () => { dummyJob = new StateRuntimeJob(stateObserver, { key: 'dummyJob', }); - dummyState.persistent = new StatePersistent(dummyState); - dummyState.isPersisted = true; stateObserver.sideEffects = jest.fn(); }); From b490410f35c1ad5fd5a69b32ae4da6543ab10769 Mon Sep 17 00:00:00 2001 From: BennoDev Date: Thu, 26 Aug 2021 07:19:01 +0200 Subject: [PATCH 55/93] fixed typos --- .../react/develop/tree-shaking/package.json | 25 +++++++++++++++ .../react/develop/tree-shaking/src/App.jsx | 9 ++++++ .../react/develop/tree-shaking/src/core.js | 3 ++ .../react/develop/tree-shaking/src/index.js | 9 ++++++ .../develop/tree-shaking/webpack.config.js | 32 +++++++++++++++++++ .../boxes/src/core/entities/ui/ui.actions.ts | 1 - .../src/core/entities/ui/ui.controller.ts | 8 ++--- packages/core/src/storages/index.ts | 8 ++--- .../collection/collection.persistent.test.ts | 4 +-- .../tests/unit/state/state.persistent.test.ts | 4 +-- .../tests/unit/storages/persistent.test.ts | 8 ++--- .../core/tests/unit/storages/storages.test.ts | 4 +-- 12 files changed, 96 insertions(+), 19 deletions(-) create mode 100644 examples/react/develop/tree-shaking/package.json create mode 100644 examples/react/develop/tree-shaking/src/App.jsx create mode 100644 examples/react/develop/tree-shaking/src/core.js create mode 100644 examples/react/develop/tree-shaking/src/index.js create mode 100644 examples/react/develop/tree-shaking/webpack.config.js 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..370e6b55 --- /dev/null +++ b/examples/react/develop/tree-shaking/webpack.config.js @@ -0,0 +1,32 @@ +const path = require('path'); +const packageJson = require('./package.json'); +const dependencies = packageJson.dependencies || {}; + +module.exports = { + mode: 'production', + entry: './src/index.js', + externals: Object.keys(dependencies), + 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: { + providedExports: true, + usedExports: true, + }, +}; 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/packages/core/src/storages/index.ts b/packages/core/src/storages/index.ts index 020a2525..0342165e 100644 --- a/packages/core/src/storages/index.ts +++ b/packages/core/src/storages/index.ts @@ -67,19 +67,19 @@ export function getStorageManager(): Storages { const newStorageManager = createStorageManager({ localStorage: !runsOnServer(), }); - registerSharedStorageManager(newStorageManager); + assignSharedAgileStorageManager(newStorageManager); return newStorageManager; } return storageManager; } /** - * Registers the specified Storage Manager - * as default Storage Manager for all Agile Instances. + * 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 registerSharedStorageManager = (instance: Storages) => { +export const assignSharedAgileStorageManager = (instance: Storages) => { if (storageManager != null) { LogCodeManager.log('11:02:06', [], storageManager); } diff --git a/packages/core/tests/unit/collection/collection.persistent.test.ts b/packages/core/tests/unit/collection/collection.persistent.test.ts index b4b1c4b1..0c3844a1 100644 --- a/packages/core/tests/unit/collection/collection.persistent.test.ts +++ b/packages/core/tests/unit/collection/collection.persistent.test.ts @@ -7,7 +7,7 @@ import { StatePersistent, Group, Item, - registerSharedStorageManager, + assignSharedAgileStorageManager, createStorageManager, getStorageManager, Storages, @@ -34,7 +34,7 @@ describe('CollectionPersistent Tests', () => { }); // Register Storage Manager - registerSharedStorageManager(createStorageManager()); + assignSharedAgileStorageManager(createStorageManager()); storageManager = getStorageManager() as any; jest.spyOn(CollectionPersistent.prototype, 'instantiatePersistent'); diff --git a/packages/core/tests/unit/state/state.persistent.test.ts b/packages/core/tests/unit/state/state.persistent.test.ts index a5870b98..b226d5d8 100644 --- a/packages/core/tests/unit/state/state.persistent.test.ts +++ b/packages/core/tests/unit/state/state.persistent.test.ts @@ -6,7 +6,7 @@ import { EnhancedState, getStorageManager, Storages, - registerSharedStorageManager, + assignSharedAgileStorageManager, createStorageManager, } from '../../../src'; import { LogMock } from '../../helper/logMock'; @@ -24,7 +24,7 @@ describe('StatePersistent Tests', () => { dummyState = new EnhancedState(dummyAgile, 'dummyValue'); // Register Storage Manager - registerSharedStorageManager(createStorageManager()); + assignSharedAgileStorageManager(createStorageManager()); storageManager = getStorageManager() as any; jest.spyOn(StatePersistent.prototype, 'instantiatePersistent'); diff --git a/packages/core/tests/unit/storages/persistent.test.ts b/packages/core/tests/unit/storages/persistent.test.ts index a6673cb0..da35c44f 100644 --- a/packages/core/tests/unit/storages/persistent.test.ts +++ b/packages/core/tests/unit/storages/persistent.test.ts @@ -4,7 +4,7 @@ import { Storage, createStorage, Storages, - registerSharedStorageManager, + assignSharedAgileStorageManager, createStorageManager, getStorageManager, } from '../../../src'; @@ -20,7 +20,7 @@ describe('Persistent Tests', () => { dummyAgile = new Agile(); // Register Storage Manager - registerSharedStorageManager(createStorageManager()); + assignSharedAgileStorageManager(createStorageManager()); storageManager = getStorageManager() as any; jest.spyOn(Persistent.prototype, 'instantiatePersistent'); @@ -201,7 +201,7 @@ describe('Persistent Tests', () => { it( 'should call formatKey, assignStorageKeys, validatePersistent ' + - 'and add Persistent to the Storage Manager when Persistent has a valid key', + 'and add Persistent to the shared Storage Manager if Persistent has a valid key', () => { persistent.instantiatePersistent({ key: 'persistentKey', @@ -227,7 +227,7 @@ describe('Persistent Tests', () => { it( 'should call formatKey, assignStorageKeys, validatePersistent ' + - "and shouldn't add Persistent to the Storage Manager when Persistent has no valid key", + "and shouldn't add Persistent to the shared Storage Manager if Persistent has no valid key", () => { persistent.instantiatePersistent({ storageKeys: ['myName', 'is', 'jeff'], diff --git a/packages/core/tests/unit/storages/storages.test.ts b/packages/core/tests/unit/storages/storages.test.ts index f19d3df1..b54833b2 100644 --- a/packages/core/tests/unit/storages/storages.test.ts +++ b/packages/core/tests/unit/storages/storages.test.ts @@ -3,7 +3,7 @@ import { Agile, Storage, Persistent, - registerSharedStorageManager, + assignSharedAgileStorageManager, } from '../../../src'; import { LogMock } from '../../helper/logMock'; @@ -50,7 +50,7 @@ describe('Storages Tests', () => { beforeEach(() => { storages = new Storages(dummyAgile); - registerSharedStorageManager(storages); + assignSharedAgileStorageManager(storages); dummyStorageMethods = { get: jest.fn(), From 51b486ad1285ca20c7afc7882a60b8305854a1cf Mon Sep 17 00:00:00 2001 From: BennoDev Date: Thu, 26 Aug 2021 07:31:30 +0200 Subject: [PATCH 56/93] fixed typo --- examples/react/develop/tree-shaking/webpack.config.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/examples/react/develop/tree-shaking/webpack.config.js b/examples/react/develop/tree-shaking/webpack.config.js index 370e6b55..1b3fb79c 100644 --- a/examples/react/develop/tree-shaking/webpack.config.js +++ b/examples/react/develop/tree-shaking/webpack.config.js @@ -1,11 +1,8 @@ const path = require('path'); -const packageJson = require('./package.json'); -const dependencies = packageJson.dependencies || {}; module.exports = { - mode: 'production', + mode: 'development', entry: './src/index.js', - externals: Object.keys(dependencies), output: { path: path.resolve(__dirname, 'dist'), filename: 'app.js', @@ -26,7 +23,9 @@ module.exports = { ], }, optimization: { - providedExports: true, usedExports: true, + innerGraph: true, + sideEffects: true, }, + devtool: false, }; From d8ee8bf8ca66382f2047fe853dd168800b8aa45e Mon Sep 17 00:00:00 2001 From: BennoDev Date: Thu, 26 Aug 2021 09:40:13 +0200 Subject: [PATCH 57/93] fixed typo --- packages/core/src/storages/storages.ts | 3 ++- .../collection.persistent.integration.test.ts | 16 +++++++++++++--- packages/core/tests/unit/agile.test.ts | 1 - .../collection/collection.persistent.test.ts | 5 ++--- .../tests/unit/state/state.persistent.test.ts | 5 ++--- .../core/tests/unit/storages/persistent.test.ts | 5 ++--- .../core/tests/unit/storages/storages.test.ts | 2 +- 7 files changed, 22 insertions(+), 15 deletions(-) diff --git a/packages/core/src/storages/storages.ts b/packages/core/src/storages/storages.ts index 36a05903..99fcfa9b 100644 --- a/packages/core/src/storages/storages.ts +++ b/packages/core/src/storages/storages.ts @@ -97,8 +97,9 @@ export class Storages { this.storages[storage.key] = storage; if (config.default) this.config.defaultStorageKey = storage.key; - for (const persistentKey in Object.keys(this.persistentInstances)) { + 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)) { 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 9d18f45b..300c9be6 100644 --- a/packages/core/tests/unit/agile.test.ts +++ b/packages/core/tests/unit/agile.test.ts @@ -3,7 +3,6 @@ import { Runtime, SubController, Integrations, - Storage, Storages, } from '../../src'; import testIntegration from '../helper/test.integration'; diff --git a/packages/core/tests/unit/collection/collection.persistent.test.ts b/packages/core/tests/unit/collection/collection.persistent.test.ts index 0c3844a1..b4f64ef1 100644 --- a/packages/core/tests/unit/collection/collection.persistent.test.ts +++ b/packages/core/tests/unit/collection/collection.persistent.test.ts @@ -9,7 +9,6 @@ import { Item, assignSharedAgileStorageManager, createStorageManager, - getStorageManager, Storages, } from '../../../src'; import { LogMock } from '../../helper/logMock'; @@ -34,8 +33,8 @@ describe('CollectionPersistent Tests', () => { }); // Register Storage Manager - assignSharedAgileStorageManager(createStorageManager()); - storageManager = getStorageManager() as any; + storageManager = createStorageManager(); + assignSharedAgileStorageManager(storageManager); jest.spyOn(CollectionPersistent.prototype, 'instantiatePersistent'); jest.spyOn(CollectionPersistent.prototype, 'initialLoading'); diff --git a/packages/core/tests/unit/state/state.persistent.test.ts b/packages/core/tests/unit/state/state.persistent.test.ts index b226d5d8..1fa45381 100644 --- a/packages/core/tests/unit/state/state.persistent.test.ts +++ b/packages/core/tests/unit/state/state.persistent.test.ts @@ -4,7 +4,6 @@ import { Storage, Persistent, EnhancedState, - getStorageManager, Storages, assignSharedAgileStorageManager, createStorageManager, @@ -24,8 +23,8 @@ describe('StatePersistent Tests', () => { dummyState = new EnhancedState(dummyAgile, 'dummyValue'); // Register Storage Manager - assignSharedAgileStorageManager(createStorageManager()); - storageManager = getStorageManager() as any; + storageManager = createStorageManager(); + assignSharedAgileStorageManager(storageManager); jest.spyOn(StatePersistent.prototype, 'instantiatePersistent'); jest.spyOn(StatePersistent.prototype, 'initialLoading'); diff --git a/packages/core/tests/unit/storages/persistent.test.ts b/packages/core/tests/unit/storages/persistent.test.ts index da35c44f..44d09106 100644 --- a/packages/core/tests/unit/storages/persistent.test.ts +++ b/packages/core/tests/unit/storages/persistent.test.ts @@ -6,7 +6,6 @@ import { Storages, assignSharedAgileStorageManager, createStorageManager, - getStorageManager, } from '../../../src'; import { LogMock } from '../../helper/logMock'; @@ -20,8 +19,8 @@ describe('Persistent Tests', () => { dummyAgile = new Agile(); // Register Storage Manager - assignSharedAgileStorageManager(createStorageManager()); - storageManager = getStorageManager() as any; + storageManager = createStorageManager(); + assignSharedAgileStorageManager(storageManager); jest.spyOn(Persistent.prototype, 'instantiatePersistent'); diff --git a/packages/core/tests/unit/storages/storages.test.ts b/packages/core/tests/unit/storages/storages.test.ts index b54833b2..9f09d002 100644 --- a/packages/core/tests/unit/storages/storages.test.ts +++ b/packages/core/tests/unit/storages/storages.test.ts @@ -190,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', }); From aa20d8664ae53f1946a108c3fa69a41461e283d7 Mon Sep 17 00:00:00 2001 From: BennoDev Date: Thu, 26 Aug 2021 18:11:53 +0200 Subject: [PATCH 58/93] fixed typos --- packages/core/src/state/index.ts | 2 +- packages/core/src/storages/index.ts | 2 +- packages/core/tests/unit/agile.test.ts | 5 - packages/core/tests/unit/shared.test.ts | 72 -------- packages/core/tests/unit/state/index.test.ts | 100 +++++++++++ .../core/tests/unit/storages/index.test.ts | 164 ++++++++++++++++++ 6 files changed, 266 insertions(+), 79 deletions(-) create mode 100644 packages/core/tests/unit/state/index.test.ts create mode 100644 packages/core/tests/unit/storages/index.test.ts diff --git a/packages/core/src/state/index.ts b/packages/core/src/state/index.ts index 7bc1d192..6a208125 100644 --- a/packages/core/src/state/index.ts +++ b/packages/core/src/state/index.ts @@ -43,7 +43,7 @@ export function createLightState( ); } -// TODO 'createState' doesn't get entirely treeshaken away +// TODO 'createState' doesn't get entirely treeshaken away (React project) /** * Returns a newly created enhanced State. * diff --git a/packages/core/src/storages/index.ts b/packages/core/src/storages/index.ts index 0342165e..b373d0fc 100644 --- a/packages/core/src/storages/index.ts +++ b/packages/core/src/storages/index.ts @@ -79,7 +79,7 @@ export function getStorageManager(): Storages { * * @param instance - Storage Manager to be registered as the default Storage Manager. */ -export const assignSharedAgileStorageManager = (instance: Storages) => { +export const assignSharedAgileStorageManager = (instance: Storages | null) => { if (storageManager != null) { LogCodeManager.log('11:02:06', [], storageManager); } diff --git a/packages/core/tests/unit/agile.test.ts b/packages/core/tests/unit/agile.test.ts index 300c9be6..85521f63 100644 --- a/packages/core/tests/unit/agile.test.ts +++ b/packages/core/tests/unit/agile.test.ts @@ -24,11 +24,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 diff --git a/packages/core/tests/unit/shared.test.ts b/packages/core/tests/unit/shared.test.ts index e93d0d97..246215d1 100644 --- a/packages/core/tests/unit/shared.test.ts +++ b/packages/core/tests/unit/shared.test.ts @@ -3,27 +3,15 @@ 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; @@ -46,66 +34,6 @@ describe('Shared Tests', () => { }); }); - 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; 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..1b84c772 --- /dev/null +++ b/packages/core/tests/unit/state/index.test.ts @@ -0,0 +1,100 @@ +// 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 + >; + + 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; + + 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/storages/index.test.ts b/packages/core/tests/unit/storages/index.test.ts new file mode 100644 index 00000000..c26137e2 --- /dev/null +++ b/packages/core/tests/unit/storages/index.test.ts @@ -0,0 +1,164 @@ +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); + + 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); + } + ); + }); +}); From fe46dabd9c6b34916a3046dcb197e83f7352be6c Mon Sep 17 00:00:00 2001 From: BennoDev Date: Thu, 26 Aug 2021 18:20:13 +0200 Subject: [PATCH 59/93] outsourced collection and computed shared tests --- .../core/tests/unit/collection/index.test.ts | 62 ++++++++++ .../core/tests/unit/computed/index.test.ts | 86 ++++++++++++++ packages/core/tests/unit/shared.test.ts | 107 ------------------ 3 files changed, 148 insertions(+), 107 deletions(-) create mode 100644 packages/core/tests/unit/collection/index.test.ts create mode 100644 packages/core/tests/unit/computed/index.test.ts 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/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/shared.test.ts b/packages/core/tests/unit/shared.test.ts index 246215d1..b8d5e048 100644 --- a/packages/core/tests/unit/shared.test.ts +++ b/packages/core/tests/unit/shared.test.ts @@ -9,9 +9,6 @@ import { } from '../../src'; import { LogMock } from '../helper/logMock'; -jest.mock('../../src/collection/collection'); -jest.mock('../../src/computed/computed'); - describe('Shared Tests', () => { let sharedAgileInstance: Agile; @@ -33,108 +30,4 @@ describe('Shared Tests', () => { expect(shared).toBe(newAgileInstance); }); }); - - 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 - ); - }); - }); }); From 4ee147218d8d6a118f852123c38d61740130be3b Mon Sep 17 00:00:00 2001 From: BennoDev Date: Fri, 27 Aug 2021 07:18:30 +0200 Subject: [PATCH 60/93] fixed multieditor issue --- packages/core/tests/unit/agile.test.ts | 10 +--------- packages/core/tests/unit/state/index.test.ts | 8 ++++++++ packages/core/tests/unit/storages/index.test.ts | 1 + packages/multieditor/src/item.ts | 4 ++-- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/core/tests/unit/agile.test.ts b/packages/core/tests/unit/agile.test.ts index 85521f63..40682716 100644 --- a/packages/core/tests/unit/agile.test.ts +++ b/packages/core/tests/unit/agile.test.ts @@ -1,10 +1,4 @@ -import { - Agile, - Runtime, - SubController, - Integrations, - Storages, -} from '../../src'; +import { Agile, Runtime, SubController, Integrations } from '../../src'; import testIntegration from '../helper/test.integration'; import { LogMock } from '../helper/logMock'; @@ -49,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 >; @@ -60,7 +53,6 @@ describe('Agile Tests', () => { // Clear specified mocks RuntimeMock.mockClear(); SubControllerMock.mockClear(); - StoragesMock.mockClear(); IntegrationsMock.mockClear(); // Reset globalThis diff --git a/packages/core/tests/unit/state/index.test.ts b/packages/core/tests/unit/state/index.test.ts index 1b84c772..7899b192 100644 --- a/packages/core/tests/unit/state/index.test.ts +++ b/packages/core/tests/unit/state/index.test.ts @@ -39,6 +39,10 @@ describe('State Index', () => { typeof EnhancedState >; + beforeEach(() => { + EnhancedStateMock.mockClear(); + }); + it('should create enhanced State with the shared Agile Instance', () => { const state = createState('testValue', { key: 'myCoolState', @@ -72,6 +76,10 @@ describe('State Index', () => { 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', diff --git a/packages/core/tests/unit/storages/index.test.ts b/packages/core/tests/unit/storages/index.test.ts index c26137e2..dde743f3 100644 --- a/packages/core/tests/unit/storages/index.test.ts +++ b/packages/core/tests/unit/storages/index.test.ts @@ -18,6 +18,7 @@ describe('Storages Index', () => { sharedAgileInstance = new Agile(); assignSharedAgileInstance(sharedAgileInstance); + // Reset Storage Manager StorageIndex.assignSharedAgileStorageManager(null); jest.clearAllMocks(); 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; From d0e6435ca7009e19f5eb473aec2cf23260838256 Mon Sep 17 00:00:00 2001 From: BennoDev Date: Fri, 27 Aug 2021 07:53:43 +0200 Subject: [PATCH 61/93] fixed typo --- .../tests/unit/collection/collection.test.ts | 18 ++++++++++++++++++ packages/react/src/hooks/useWatcher.ts | 4 ++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/core/tests/unit/collection/collection.test.ts b/packages/core/tests/unit/collection/collection.test.ts index 89684604..86a019a1 100644 --- a/packages/core/tests/unit/collection/collection.test.ts +++ b/packages/core/tests/unit/collection/collection.test.ts @@ -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/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(() => { From 0e73f5c40e8ed9935915522e958e204eef962674 Mon Sep 17 00:00:00 2001 From: BennoDev Date: Fri, 27 Aug 2021 12:15:12 +0200 Subject: [PATCH 62/93] split useAgile hook --- .../runtime/subscription/sub.controller.ts | 2 +- packages/react/src/hooks/useAgile.ts | 207 +++--------------- packages/react/src/hooks/useBaseAgile.ts | 119 ++++++++++ packages/react/src/hooks/useProxy.ts | 98 ++++++++- packages/react/src/hooks/useSelector.ts | 72 ++++-- packages/react/src/hooks/useValue.ts | 7 +- 6 files changed, 302 insertions(+), 203 deletions(-) create mode 100644 packages/react/src/hooks/useBaseAgile.ts 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/react/src/hooks/useAgile.ts b/packages/react/src/hooks/useAgile.ts index 56d51419..dd1d2f4c 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,162 +55,38 @@ export function useAgile< ): AgileOutputHookArrayType | AgileOutputHookType { config = defineConfig(config, { key: generateId(), - proxyBased: false, agileInstance: null as any, componentId: undefined, observerType: undefined, deps: [], + handleReturn: (dep: Observer | undefined) => { + return dep != null ? dep.value : undefined; + }, }); const depsArray = extractRelevantObservers( 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; - }; - - // 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, + config.handleReturn as any, + 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; +export interface AgileHookConfigInterface extends BaseAgileHookConfigInterface { /** * 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, @@ -234,7 +98,7 @@ export interface AgileHookConfigInterface { * * @default false */ - proxyBased?: boolean; + // proxyBased?: boolean; /** * Equality comparison function * that allows you to customize the way the selected Agile Instance @@ -245,12 +109,8 @@ export interface AgileHookConfigInterface { * * @default undefined */ - selector?: SelectorMethodType; - /** - * Key/Name identifier of the UI-Component the Subscription Container is bound to. - * @default undefined - */ - componentId?: ComponentIdType; + // selector?: SelectorMethodType; + /** * What type of Observer to be bound to the UI-Component. * @@ -261,14 +121,9 @@ export interface AgileHookConfigInterface { */ 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 [] + * TODO */ - deps?: any[]; + handleReturn?: (dep: Observer | undefined) => 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..be25f4ed --- /dev/null +++ b/packages/react/src/hooks/useBaseAgile.ts @@ -0,0 +1,119 @@ +import React from 'react'; +import Agile, { + Collection, + ComponentIdType, + getAgileInstance, + LogCodeManager, + Observer, + State, + SubscriptionContainerKeyType, + RegisterSubscriptionConfigInterface, +} from '@agile-ts/core'; +import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'; + +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); + + const _agileInstance = extractAgileInstance(observers, agileInstance); + if (_agileInstance == null) 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); +}; + +export const extractAgileInstance = ( + observers: Observer[], + agileInstance?: Agile +): Agile | undefined => { + if (agileInstance != null) return agileInstance; + + // Try to extract Agile Instance from the specified Observers + agileInstance = getAgileInstance(observers[0]); + if (!agileInstance || !agileInstance.subController) { + LogCodeManager.getLogger()?.error( + 'Failed to subscribe to React Component because of missing valid Agile Instance.', + observers + ); + return undefined; + } + return agileInstance; +}; + +// Builds return value, +// depending on whether the deps were provided in array shape or not +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..50d85b21 100644 --- a/packages/react/src/hooks/useProxy.ts +++ b/packages/react/src/hooks/useProxy.ts @@ -1,11 +1,28 @@ 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'; + +// 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 +} export function useProxy>( deps: X | [], @@ -24,10 +41,75 @@ 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 { + console.error( + 'In order to use the Agile proxy functionality, ' + + `the installation of an additional package called '@agile-ts/proxytree' is required!` + ); + } + } + + 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..7664b84a 100644 --- a/packages/react/src/hooks/useSelector.ts +++ b/packages/react/src/hooks/useSelector.ts @@ -1,9 +1,17 @@ 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'; export function useSelector< @@ -13,13 +21,13 @@ export function useSelector< >( dep: X, selector: SelectorMethodType, - config?: AgileHookConfigInterface + config?: BaseAgileHookConfigInterface ): ReturnType; export function useSelector( dep: SubscribableAgileInstancesType, selector: SelectorMethodType, - config?: AgileHookConfigInterface + config?: BaseAgileHookConfigInterface ): ReturnType; export function useSelector< @@ -29,12 +37,50 @@ export function useSelector< >( dep: X, selector: SelectorMethodType, - config: AgileHookConfigInterface = {} + 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, + }); + 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 selector(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: [selector] }); + } + + 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..e4f71fa7 100644 --- a/packages/react/src/hooks/useValue.ts +++ b/packages/react/src/hooks/useValue.ts @@ -5,11 +5,8 @@ import { State, defineConfig, } from '@agile-ts/core'; -import { - AgileHookConfigInterface, - SubscribableAgileInstancesType, - useAgile, -} from './useAgile'; +import { AgileHookConfigInterface, useAgile } from './useAgile'; +import { SubscribableAgileInstancesType } from './useBaseAgile'; export function useValue>( deps: X | [], From dcaf6cd4e6393cf2c5e49e4485fbb1953280bf6d Mon Sep 17 00:00:00 2001 From: BennoDev Date: Fri, 27 Aug 2021 19:45:13 +0200 Subject: [PATCH 63/93] fixed typos --- .../event/tests/unit/event.observer.test.ts | 2 +- packages/event/tests/unit/event.test.ts | 2 +- packages/react/src/hooks/useAgile.ts | 41 ++----------- packages/react/src/hooks/useBaseAgile.ts | 57 +++++++++++-------- packages/react/src/hooks/useProxy.ts | 40 ++++++++++++- packages/react/src/hooks/useSelector.ts | 15 +++++ 6 files changed, 94 insertions(+), 63 deletions(-) diff --git a/packages/event/tests/unit/event.observer.test.ts b/packages/event/tests/unit/event.observer.test.ts index b700a72f..2732e219 100644 --- a/packages/event/tests/unit/event.observer.test.ts +++ b/packages/event/tests/unit/event.observer.test.ts @@ -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.test.ts index bd369952..5885471b 100644 --- a/packages/event/tests/unit/event.test.ts +++ b/packages/event/tests/unit/event.test.ts @@ -9,7 +9,7 @@ describe('Event Tests', () => { beforeEach(() => { LogMock.mockLogs(); - dummyAgile = new Agile({ localStorage: false }); + dummyAgile = new Agile(); jest.clearAllMocks(); }); diff --git a/packages/react/src/hooks/useAgile.ts b/packages/react/src/hooks/useAgile.ts index dd1d2f4c..15d33408 100644 --- a/packages/react/src/hooks/useAgile.ts +++ b/packages/react/src/hooks/useAgile.ts @@ -59,15 +59,16 @@ export function useAgile< componentId: undefined, observerType: undefined, deps: [], - handleReturn: (dep: Observer | undefined) => { - return dep != null ? dep.value : undefined; - }, }); const depsArray = extractRelevantObservers( normalizeArray(deps), config.observerType ); + const handleReturn = (dep: Observer | undefined) => { + return dep != null ? dep.value : undefined; + }; + useBaseAgile( depsArray, () => ({ @@ -79,38 +80,10 @@ export function useAgile< config.agileInstance ); - return getReturnValue( - depsArray, - config.handleReturn as any, - Array.isArray(deps) - ); + return getReturnValue(depsArray, handleReturn, Array.isArray(deps)); } export interface AgileHookConfigInterface extends BaseAgileHookConfigInterface { - /** - * 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; - /** * What type of Observer to be bound to the UI-Component. * @@ -120,10 +93,6 @@ export interface AgileHookConfigInterface extends BaseAgileHookConfigInterface { * @default undefined */ observerType?: string; - /** - * TODO - */ - handleReturn?: (dep: Observer | undefined) => any; } // Array Type diff --git a/packages/react/src/hooks/useBaseAgile.ts b/packages/react/src/hooks/useBaseAgile.ts index be25f4ed..f6b872a2 100644 --- a/packages/react/src/hooks/useBaseAgile.ts +++ b/packages/react/src/hooks/useBaseAgile.ts @@ -11,6 +11,19 @@ import Agile, { } from '@agile-ts/core'; import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'; +/** + * 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: ( @@ -30,11 +43,18 @@ export const useBaseAgile = ( const subContainerConfig = getSubContainerConfig(observers); - const _agileInstance = extractAgileInstance(observers, agileInstance); - if (_agileInstance == null) return; + // Try to extract Agile Instance from the specified Instance/s + if (agileInstance == null) agileInstance = getAgileInstance(observers[0]); + if (agileInstance == null || agileInstance.subController == null) { + LogCodeManager.getLogger()?.error( + 'Failed to subscribe Component with deps because of missing valid Agile Instance.', + deps + ); + return; + } // Create Callback based Subscription - const subscriptionContainer = _agileInstance.subController.subscribe( + const subscriptionContainer = agileInstance.subController.subscribe( () => { forceRender(); }, @@ -44,31 +64,20 @@ export const useBaseAgile = ( // Unsubscribe Callback based Subscription on unmount return () => { - _agileInstance.subController.unsubscribe(subscriptionContainer); + agileInstance?.subController.unsubscribe(subscriptionContainer); }; }, deps); }; -export const extractAgileInstance = ( - observers: Observer[], - agileInstance?: Agile -): Agile | undefined => { - if (agileInstance != null) return agileInstance; - - // Try to extract Agile Instance from the specified Observers - agileInstance = getAgileInstance(observers[0]); - if (!agileInstance || !agileInstance.subController) { - LogCodeManager.getLogger()?.error( - 'Failed to subscribe to React Component because of missing valid Agile Instance.', - observers - ); - return undefined; - } - return agileInstance; -}; - -// Builds return value, -// depending on whether the deps were provided in array shape or not +/** + * 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, diff --git a/packages/react/src/hooks/useProxy.ts b/packages/react/src/hooks/useProxy.ts index 50d85b21..76356f28 100644 --- a/packages/react/src/hooks/useProxy.ts +++ b/packages/react/src/hooks/useProxy.ts @@ -24,11 +24,48 @@ try { // 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 @@ -63,6 +100,7 @@ export function useProxy< proxyTreeWeakMap.set(dep, proxyTree); return proxyTree.proxy; } else { + // TODO add LogCode Manager logcode console.error( 'In order to use the Agile proxy functionality, ' + `the installation of an additional package called '@agile-ts/proxytree' is required!` diff --git a/packages/react/src/hooks/useSelector.ts b/packages/react/src/hooks/useSelector.ts index 7664b84a..75a9cd34 100644 --- a/packages/react/src/hooks/useSelector.ts +++ b/packages/react/src/hooks/useSelector.ts @@ -24,6 +24,21 @@ export function useSelector< 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 selector - 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, From fc9266b24dd2cb45ca436e2f79bda5c15fcaa610 Mon Sep 17 00:00:00 2001 From: BennoDev Date: Fri, 27 Aug 2021 19:53:34 +0200 Subject: [PATCH 64/93] fixed example --- .../react/develop/functional-component-ts/src/core/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 207bce0d..06de957a 100644 --- a/examples/react/develop/functional-component-ts/src/core/index.ts +++ b/examples/react/develop/functional-component-ts/src/core/index.ts @@ -6,7 +6,7 @@ import Agile, { createStorage, createStorageManager, Item, - registerSharedStorageManager, + assignSharedAgileStorageManager, } from '@agile-ts/core'; import Event from '@agile-ts/event'; import { assignSharedAgileLoggerConfig, Logger } from '@agile-ts/logger'; @@ -19,7 +19,7 @@ export const App = new Agile(); assignSharedAgileInstance(App); export const storageManager = createStorageManager({ localStorage: true }); -registerSharedStorageManager(storageManager); +assignSharedAgileStorageManager(storageManager); // Register custom second Storage storageManager.register( From 075735c16cf41a1616a87c1a9c3363f251dc2cd0 Mon Sep 17 00:00:00 2001 From: BennoDev Date: Sat, 28 Aug 2021 07:59:40 +0200 Subject: [PATCH 65/93] fixed typos --- .../develop/AwesomeTSProject/core/index.ts | 20 ++++++-------- .../class-component-ts/src/core/index.ts | 26 +++++++++---------- .../functional-component-ts/src/App.tsx | 18 +++++++++---- .../functional-component-ts/src/core/index.ts | 4 +-- .../develop/multieditor-ts/src/core/agile.ts | 6 +---- examples/react/release/boxes/yarn.lock | 16 ++++-------- .../stopwatch-query-url/src/core/index.ts | 11 +++++--- examples/vue/develop/my-project/src/core.js | 24 ++++++++--------- packages/react/src/hooks/useValue.ts | 13 ++++++---- 9 files changed, 70 insertions(+), 68 deletions(-) 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..13aa298d 100644 --- a/examples/react/develop/functional-component-ts/src/App.tsx +++ b/examples/react/develop/functional-component-ts/src/App.tsx @@ -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 06de957a..5d74ad72 100644 --- a/examples/react/develop/functional-component-ts/src/core/index.ts +++ b/examples/react/develop/functional-component-ts/src/core/index.ts @@ -8,7 +8,7 @@ import Agile, { 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'; @@ -114,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/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/release/boxes/yarn.lock b/examples/react/release/boxes/yarn.lock index a63eec3f..07d40055 100644 --- a/examples/react/release/boxes/yarn.lock +++ b/examples/react/release/boxes/yarn.lock @@ -3,26 +3,20 @@ "@agile-ts/core@file:.yalc/@agile-ts/core": - version "0.1.2" + 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/react/src/hooks/useValue.ts b/packages/react/src/hooks/useValue.ts index e4f71fa7..46670532 100644 --- a/packages/react/src/hooks/useValue.ts +++ b/packages/react/src/hooks/useValue.ts @@ -5,17 +5,20 @@ import { State, defineConfig, } from '@agile-ts/core'; -import { AgileHookConfigInterface, useAgile } from './useAgile'; -import { SubscribableAgileInstancesType } from './useBaseAgile'; +import { useAgile } from './useAgile'; +import { + BaseAgileHookConfigInterface, + SubscribableAgileInstancesType, +} 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< @@ -23,7 +26,7 @@ export function useValue< Y extends SubscribableAgileInstancesType >( deps: X | Y, - config: AgileHookConfigInterface = {} + config: BaseAgileHookConfigInterface = {} ): AgileValueHookArrayType | AgileValueHookType { return useAgile( deps as any, From 59bd613cbd09254c577c9b38debf04cf9b35c85f Mon Sep 17 00:00:00 2001 From: BennoDev Date: Sat, 28 Aug 2021 08:15:32 +0200 Subject: [PATCH 66/93] updated event structure --- .../develop/functional-component-ts/src/App.tsx | 2 +- packages/event/src/{ => event}/event.observer.ts | 2 +- .../{event.job.ts => event/event.runtime.job.ts} | 2 +- packages/event/src/{ => event}/event.ts | 8 ++++---- packages/event/src/{shared.ts => event/index.ts} | 16 ++++++++-------- packages/event/src/index.ts | 1 - packages/event/src/internal.ts | 7 ++----- packages/event/src/{ => react}/hooks/useEvent.ts | 2 +- .../hooks/useIsomorphicLayoutEffect.ts | 0 packages/event/src/react/index.ts | 1 + .../tests/unit/{ => event}/event.job.test.ts | 11 +++++++---- .../unit/{ => event}/event.observer.test.ts | 4 ++-- .../event/tests/unit/{ => event}/event.test.ts | 4 ++-- .../unit/{shared.test.ts => event/index.test.ts} | 6 +++--- 14 files changed, 33 insertions(+), 33 deletions(-) rename packages/event/src/{ => event}/event.observer.ts (97%) rename packages/event/src/{event.job.ts => event/event.runtime.job.ts} (90%) rename packages/event/src/{ => event}/event.ts (97%) rename packages/event/src/{shared.ts => event/index.ts} (67%) rename packages/event/src/{ => react}/hooks/useEvent.ts (95%) rename packages/event/src/{ => react}/hooks/useIsomorphicLayoutEffect.ts (100%) create mode 100644 packages/event/src/react/index.ts rename packages/event/tests/unit/{ => event}/event.job.test.ts (74%) rename packages/event/tests/unit/{ => event}/event.observer.test.ts (94%) rename packages/event/tests/unit/{ => event}/event.test.ts (99%) rename packages/event/tests/unit/{shared.test.ts => event/index.test.ts} (88%) diff --git a/examples/react/develop/functional-component-ts/src/App.tsx b/examples/react/develop/functional-component-ts/src/App.tsx index 13aa298d..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, 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 94% rename from packages/event/tests/unit/event.observer.test.ts rename to packages/event/tests/unit/event/event.observer.test.ts index 2732e219..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; diff --git a/packages/event/tests/unit/event.test.ts b/packages/event/tests/unit/event/event.test.ts similarity index 99% rename from packages/event/tests/unit/event.test.ts rename to packages/event/tests/unit/event/event.test.ts index 5885471b..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; 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; From 5ea27e28dabdda9f48ad31cb160a22b10dc5cad9 Mon Sep 17 00:00:00 2001 From: BennoDev Date: Sat, 28 Aug 2021 10:05:18 +0200 Subject: [PATCH 67/93] fixed typos --- packages/multieditor/src/multieditor/index.ts | 22 ++ .../src/{ => multieditor}/multieditor.ts | 2 +- packages/utils/src/index.ts | 189 ++++++++---------- 3 files changed, 104 insertions(+), 109 deletions(-) create mode 100644 packages/multieditor/src/multieditor/index.ts rename packages/multieditor/src/{ => multieditor}/multieditor.ts (99%) 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/utils/src/index.ts b/packages/utils/src/index.ts index 6161059c..c06a7505 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: number = 5, + characters: string = '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, From dcf742fda1d7179d7df7d980152dab21ed8ef576 Mon Sep 17 00:00:00 2001 From: BennoDev Date: Sun, 29 Aug 2021 17:06:56 +0200 Subject: [PATCH 68/93] added log code manager to react package --- packages/core/src/logCodeManager.ts | 76 ++++++++++++++---------- packages/logger/src/index.ts | 18 +++--- packages/react/src/hooks/useBaseAgile.ts | 7 +-- packages/react/src/hooks/useProxy.ts | 7 +-- packages/react/src/logCodeManager.ts | 26 ++++++++ 5 files changed, 85 insertions(+), 49 deletions(-) create mode 100644 packages/react/src/logCodeManager.ts diff --git a/packages/core/src/logCodeManager.ts b/packages/core/src/logCodeManager.ts index 2e10408e..79d36451 100644 --- a/packages/core/src/logCodeManager.ts +++ b/packages/core/src/logCodeManager.ts @@ -23,7 +23,7 @@ const logCodeTypes = { // --- // 00:00:|00| third digits are based on the Log Message (ascending counted) -const niceLogCodeMessages = { +const logCodeMessages = { // Agile '10:00:00': 'Created new AgileInstance.', '10:02:00': @@ -165,13 +165,6 @@ const niceLogCodeMessages = { '00:03:01': "'${0}' has to be of the type ${1}!", }; -// Note: Not outsource the 'production' env check, -// because then webpack can't treeshake based on the current env -const logCodeMessages: typeof niceLogCodeMessages = - typeof process === 'object' && process.env.NODE_ENV !== 'production' - ? niceLogCodeMessages - : ({} as any); - /** * Returns the log message according to the specified log code. * @@ -257,20 +250,22 @@ 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. - * - * @internal - */ -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 { + logCodeManager.logCodeMessages = { + ...LogCodeManager.logCodeMessages, + ...additionalLogs, + } as any; + return logCodeManager as any; +} + +let tempLogCodeManager: LogCodeManagerInterface; if (typeof process === 'object' && process.env.NODE_ENV !== 'production') { let loggerPackage: any = null; try { @@ -284,11 +279,7 @@ if (typeof process === 'object' && process.env.NODE_ENV !== 'production') { log, logCodeLogTypes: logCodeTypes, logCodeMessages: logCodeMessages, - // Not doing 'logger: loggerPackage?.sharedAgileLogger' - // because only by calling a function (now 'getLogger()') the 'sharedLogger' is refetched - getLogger: () => { - return loggerPackage?.sharedAgileLogger ?? null; - }, + getLogger: loggerPackage.getLogger, logIfTags, }; } else { @@ -297,20 +288,43 @@ 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 */ }, }; } + +/** + * 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; +} 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/react/src/hooks/useBaseAgile.ts b/packages/react/src/hooks/useBaseAgile.ts index f6b872a2..b566b756 100644 --- a/packages/react/src/hooks/useBaseAgile.ts +++ b/packages/react/src/hooks/useBaseAgile.ts @@ -3,13 +3,13 @@ import Agile, { Collection, ComponentIdType, getAgileInstance, - LogCodeManager, Observer, State, SubscriptionContainerKeyType, RegisterSubscriptionConfigInterface, } from '@agile-ts/core'; import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'; +import { LogCodeManager } from '../logCodeManager'; /** * An internal used React Hook @@ -46,10 +46,7 @@ export const useBaseAgile = ( // Try to extract Agile Instance from the specified Instance/s if (agileInstance == null) agileInstance = getAgileInstance(observers[0]); if (agileInstance == null || agileInstance.subController == null) { - LogCodeManager.getLogger()?.error( - 'Failed to subscribe Component with deps because of missing valid Agile Instance.', - deps - ); + LogCodeManager.log('30:03:00', deps); return; } diff --git a/packages/react/src/hooks/useProxy.ts b/packages/react/src/hooks/useProxy.ts index 76356f28..02b100ef 100644 --- a/packages/react/src/hooks/useProxy.ts +++ b/packages/react/src/hooks/useProxy.ts @@ -15,6 +15,7 @@ import { AgileOutputHookArrayType, AgileOutputHookType, } from './useAgile'; +import { LogCodeManager } from '../logCodeManager'; // TODO https://stackoverflow.com/questions/68148235/require-module-inside-a-function-doesnt-work let proxyPackage: any = null; @@ -100,11 +101,7 @@ export function useProxy< proxyTreeWeakMap.set(dep, proxyTree); return proxyTree.proxy; } else { - // TODO add LogCode Manager logcode - console.error( - 'In order to use the Agile proxy functionality, ' + - `the installation of an additional package called '@agile-ts/proxytree' is required!` - ); + LogCodeManager.log('31:03:00'); } } diff --git a/packages/react/src/logCodeManager.ts b/packages/react/src/logCodeManager.ts new file mode 100644 index 00000000..0ddfd542 --- /dev/null +++ b/packages/react/src/logCodeManager.ts @@ -0,0 +1,26 @@ +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!", +}; + +/** + * 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); From 321a8aeea573a4e7cc71e73baa6bf026241ba0e7 Mon Sep 17 00:00:00 2001 From: BennoDev Date: Sun, 29 Aug 2021 17:30:50 +0200 Subject: [PATCH 69/93] fixed typo --- packages/core/src/logCodeManager.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/core/src/logCodeManager.ts b/packages/core/src/logCodeManager.ts index 79d36451..7c92dbca 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 @@ -251,6 +253,13 @@ function logIfTags>( logger.if.tag(tags)[logType](getLog(logCode, replacers), ...data); } +/** + * Creates an extension of the specified LogCodeManager + * and assigns the provided additional log messages to it. + * + * @param additionalLogs - Log messages to be added to the LogCodeManager. + * @param logCodeManager - LogCodeManager to create an extension from. + */ export function assignAdditionalLogs< NewLogCodeMessages, OldLogCodeMessages = typeof logCodeMessages @@ -258,11 +267,12 @@ export function assignAdditionalLogs< additionalLogs: { [key: string]: string }, logCodeManager: LogCodeManagerInterface ): LogCodeManagerInterface { - logCodeManager.logCodeMessages = { - ...LogCodeManager.logCodeMessages, + const copiedLogCodeManager = copy(logCodeManager); + copiedLogCodeManager.logCodeMessages = { + ...copiedLogCodeManager.logCodeMessages, ...additionalLogs, } as any; - return logCodeManager as any; + return copiedLogCodeManager as any; } let tempLogCodeManager: LogCodeManagerInterface; From 6600cd5741576a1a62e1d99e6f14604f677c585d Mon Sep 17 00:00:00 2001 From: BennoDev Date: Sun, 29 Aug 2021 17:50:23 +0200 Subject: [PATCH 70/93] fixed linter --- packages/react/src/hocs/AgileHOC.ts | 19 +++++++------------ packages/react/src/logCodeManager.ts | 1 + packages/utils/src/index.ts | 4 ++-- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/packages/react/src/hocs/AgileHOC.ts b/packages/react/src/hocs/AgileHOC.ts index 99e161d2..7bfcd4a6 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 type { Collection } from '@agile-ts/core'; +import { LogCodeManager } from '../logCodeManager'; // Only import Collection and Group type for better Treeshaking /** * 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/logCodeManager.ts b/packages/react/src/logCodeManager.ts index 0ddfd542..b3601ba2 100644 --- a/packages/react/src/logCodeManager.ts +++ b/packages/react/src/logCodeManager.ts @@ -8,6 +8,7 @@ const additionalLogs = { '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', }; /** diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index c06a7505..ffef55ed 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -237,8 +237,8 @@ export function notEqual(value1: any, value2: any): boolean { * @param characters - Characters to generate the id from (default = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'). */ export function generateId( - length: number = 5, - characters: string = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + length = 5, + characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' ): string { const charactersLength = characters.length; let result = ''; From 71648dda7c1d8a3a3ef4875af45a50eb1b9567ef Mon Sep 17 00:00:00 2001 From: BennoDev Date: Mon, 30 Aug 2021 19:51:36 +0200 Subject: [PATCH 71/93] fixed typo --- .../plainjs/develop/tree-shaking/package.json | 2 +- packages/core/src/logCodeManager.ts | 9 +++++- packages/react/src/hocs/AgileHOC.ts | 4 +-- packages/react/src/hooks/useSelector.ts | 29 ++++++++++++++----- 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/examples/plainjs/develop/tree-shaking/package.json b/examples/plainjs/develop/tree-shaking/package.json index 9f6bb167..4af19172 100644 --- a/examples/plainjs/develop/tree-shaking/package.json +++ b/examples/plainjs/develop/tree-shaking/package.json @@ -13,7 +13,7 @@ "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" diff --git a/packages/core/src/logCodeManager.ts b/packages/core/src/logCodeManager.ts index 7c92dbca..d0fb84c9 100644 --- a/packages/core/src/logCodeManager.ts +++ b/packages/core/src/logCodeManager.ts @@ -25,7 +25,7 @@ const logCodeTypes = { // --- // 00:00:|00| third digits are based on the Log Message (ascending counted) -const logCodeMessages = { +const niceLogCodeMessages = { // Agile '10:00:00': 'Created new AgileInstance.', '10:02:00': @@ -167,6 +167,13 @@ const logCodeMessages = { '00:03:01': "'${0}' has to be of the type ${1}!", }; +// Note: Not outsource the 'production' env check, +// because then webpack can't treeshake based on the current env +const logCodeMessages: typeof niceLogCodeMessages = + typeof process === 'object' && process.env.NODE_ENV !== 'production' + ? niceLogCodeMessages + : ({} as any); + /** * Returns the log message according to the specified log code. * diff --git a/packages/react/src/hocs/AgileHOC.ts b/packages/react/src/hocs/AgileHOC.ts index 7bfcd4a6..01b787ed 100644 --- a/packages/react/src/hocs/AgileHOC.ts +++ b/packages/react/src/hocs/AgileHOC.ts @@ -9,8 +9,8 @@ import Agile, { extractRelevantObservers, normalizeArray, } from '@agile-ts/core'; -import type { Collection } from '@agile-ts/core'; -import { LogCodeManager } from '../logCodeManager'; // Only import Collection and Group type for better Treeshaking +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 diff --git a/packages/react/src/hooks/useSelector.ts b/packages/react/src/hooks/useSelector.ts index 75a9cd34..043df0c2 100644 --- a/packages/react/src/hooks/useSelector.ts +++ b/packages/react/src/hooks/useSelector.ts @@ -14,16 +14,30 @@ import { } 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, + 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) @@ -34,14 +48,14 @@ export function useSelector< * * @public * @param dep - Agile Sub Instance to be bound to the Functional Component. - * @param selector - Equality comparison function + * @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, + selectorMethod: SelectorMethodType, config?: BaseAgileHookConfigInterface ): ReturnType; @@ -51,13 +65,14 @@ export function useSelector< ReturnType = any >( dep: X, - selector: SelectorMethodType, + selectorMethod: SelectorMethodType, config: BaseAgileHookConfigInterface = {} ): ReturnType { config = defineConfig(config, { key: generateId(), agileInstance: null as any, componentId: undefined, + deps: [], }); const depsArray = extractRelevantObservers([dep]); @@ -70,7 +85,7 @@ export function useSelector< // (Destroys the type of the useAgile hook, // however the type can be adjusted in the useSelector hook) if (isValidObject(value, true)) { - return selector(value); + return selectorMethod(value); } return value; @@ -83,7 +98,7 @@ export function useSelector< let selectorWeakMap: SelectorWeakMapType | undefined = undefined; selectorWeakMap = new WeakMap(); for (const observer of observers) { - selectorWeakMap.set(observer, { methods: [selector] }); + selectorWeakMap.set(observer, { methods: [selectorMethod] }); } return { From 5907794cb3046482ef1136d312c0c82e417836df Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Tue, 31 Aug 2021 18:01:19 +0200 Subject: [PATCH 72/93] added feature to completely block the agile logcodemanager --- .../1000fields/bench/agilets/collection.tsx | 5 ++--- .../1000fields/bench/agilets/nestedState.tsx | 5 ++--- .../react/1000fields/bench/agilets/state.tsx | 5 ++--- .../computed/bench/agilets/autoTracking.tsx | 10 +++++++--- .../react/computed/bench/agilets/hardCoded.tsx | 10 +++++++--- .../benchmarks/react/counter/bench/agilets.tsx | 5 ++--- packages/core/src/logCodeManager.ts | 18 ++++++++++++++++-- 7 files changed, 38 insertions(+), 20 deletions(-) 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/packages/core/src/logCodeManager.ts b/packages/core/src/logCodeManager.ts index d0fb84c9..70aa95e3 100644 --- a/packages/core/src/logCodeManager.ts +++ b/packages/core/src/logCodeManager.ts @@ -25,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.', @@ -174,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. * @@ -213,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; @@ -246,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; @@ -298,6 +309,7 @@ if (typeof process === 'object' && process.env.NODE_ENV !== 'production') { logCodeMessages: logCodeMessages, getLogger: loggerPackage.getLogger, logIfTags, + setAllowLogging, }; } else { tempLogCodeManager = { @@ -312,6 +324,7 @@ if (typeof process === 'object' && process.env.NODE_ENV !== 'production') { logIfTags: (tags, logCode, replacers) => { /* empty because logs with tags can't be that important */ }, + setAllowLogging, }; } @@ -344,4 +357,5 @@ export interface LogCodeManagerInterface { replacers?: any[], ...data: any[] ) => void; + setAllowLogging: (logging: boolean) => void; } From fe98f658e3a9696cc98b304395747356484543d6 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Wed, 1 Sep 2021 19:55:50 +0200 Subject: [PATCH 73/93] fixed typos --- .../react/develop/simple-counter/package.json | 22 +++++---- .../react/develop/simple-counter/src/index.js | 2 + .../src/state-manager/Hookstate.js | 48 +++++++++++++++++++ .../react/develop/simple-counter/yarn.lock | 34 ++++++++----- 4 files changed, 84 insertions(+), 22 deletions(-) create mode 100644 examples/react/develop/simple-counter/src/state-manager/Hookstate.js diff --git a/examples/react/develop/simple-counter/package.json b/examples/react/develop/simple-counter/package.json index 058eac66..0a89113d 100644 --- a/examples/react/develop/simple-counter/package.json +++ b/examples/react/develop/simple-counter/package.json @@ -2,11 +2,23 @@ "name": "counter", "version": "0.1.0", "private": true, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "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" + }, "dependencies": { "@agile-ts/core": "file:.yalc/@agile-ts/core", "@agile-ts/logger": "file:.yalc/@agile-ts/logger", "@agile-ts/react": "file:.yalc/@agile-ts/react", + "@hookstate/core": "^3.0.8", "@reduxjs/toolkit": "^1.6.1", + "dotenv": "^10.0.0", "jotai": "^1.2.2", "nanostores": "^0.4.1", "react": "17.0.2", @@ -19,16 +31,6 @@ "source-map-explorer": "^2.5.2", "webpack-bundle-analyzer": "^4.4.2" }, - "scripts": { - "start": "react-scripts start", - "build": "react-scripts build", - "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" - }, "browserslist": { "production": [ ">0.2%", diff --git a/examples/react/develop/simple-counter/src/index.js b/examples/react/develop/simple-counter/src/index.js index ae2db727..6e17f3fd 100644 --- a/examples/react/develop/simple-counter/src/index.js +++ b/examples/react/develop/simple-counter/src/index.js @@ -6,6 +6,7 @@ import * as Jotai from './state-manager/Jotai'; import * as NanoStores from './state-manager/NanoStores'; import * as Recoil from './state-manager/Recoil'; import * as ReduxToolkit from './state-manager/ReduxToolkit'; +import * as Hookstate from './state-manager/Hookstate'; ReactDOM.render( @@ -14,6 +15,7 @@ ReactDOM.render( + , document.getElementById('root') ); diff --git a/examples/react/develop/simple-counter/src/state-manager/Hookstate.js b/examples/react/develop/simple-counter/src/state-manager/Hookstate.js new file mode 100644 index 00000000..0d9b86d4 --- /dev/null +++ b/examples/react/develop/simple-counter/src/state-manager/Hookstate.js @@ -0,0 +1,48 @@ +import React from 'react'; +import { createState, useState } from '@hookstate/core'; + +const COUNTER_A = createState(1); +const COUNTER_B = createState(2); +const COUNTER_C = createState(3); + +const CounterA = () => { + const count = useState(COUNTER_A); + return ( +

+ A: {count.get()}{' '} + +
+ ); +}; + +const CounterB = () => { + const count = useState(COUNTER_B); + return ( +
+ B: {count.get()}{' '} + +
+ ); +}; + +const CounterC = () => { + const count = useState(COUNTER_C); + return ( +
+ C: {count.get()}{' '} + +
+ ); +}; + +export const App = () => ( +
+

Hookstate

+ + + + + + +
+); diff --git a/examples/react/develop/simple-counter/yarn.lock b/examples/react/develop/simple-counter/yarn.lock index 23f11909..2505ba28 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.4" + version "0.2.0-alpha.3" dependencies: "@agile-ts/utils" "^0.0.7" @@ -1783,6 +1783,11 @@ dependencies: "@hapi/hoek" "^8.3.0" +"@hookstate/core@^3.0.8": + version "3.0.8" + resolved "https://registry.yarnpkg.com/@hookstate/core/-/core-3.0.8.tgz#d6838153d6d43c2f35cfca475c31248192564e62" + integrity sha512-blQagGIVIbNoUiNCRrvaXqFmUe7WGMY35ok/LENfl2pcIsLBjkreYIZiaSFi83tkycwq7ZOmcQz/R1nvLKhH8w== + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -2011,10 +2016,10 @@ 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== +"@polka/url@^1.0.0-next.20": + version "1.0.0-next.20" + resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.20.tgz#111b5db0f501aa89b05076fa31f0ea0e0c292cd3" + integrity sha512-88p7+M0QGxKpmnkfXjS4V26AnoC/eiqZutE8GLdaI5X12NY75bXSdTY9NkmYb2Xyk1O+MmkuO6Frmsj84V6I8Q== "@reduxjs/toolkit@^1.6.1": version "1.6.1" @@ -4789,6 +4794,11 @@ dotenv@8.2.0: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== +dotenv@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81" + integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== + duplexer@^0.1.1, duplexer@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" @@ -10527,11 +10537,11 @@ simple-swizzle@^0.2.2: 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== + version "1.0.17" + resolved "https://registry.yarnpkg.com/sirv/-/sirv-1.0.17.tgz#86e2c63c612da5a1dace1c16c46f524aaa26ac45" + integrity sha512-qx9go5yraB7ekT7bCMqUHJ5jEaOC/GXBxUWv+jeWnb7WzHUFdcQPGWk7YmAwFBaQBrogpuSqd/azbC2lZRqqmw== dependencies: - "@polka/url" "^1.0.0-next.17" + "@polka/url" "^1.0.0-next.20" mime "^2.3.1" totalist "^1.0.0" @@ -12117,9 +12127,9 @@ ws@^7.2.3: 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== + version "7.5.4" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.4.tgz#56bfa20b167427e138a7795de68d134fe92e21f9" + integrity sha512-zP9z6GXm6zC27YtspwH99T3qTG7bBFv2VIkeHstMLrLlDJuzA7tQ5ls3OJ1hOGGCzTQPniNJoHXIAOS0Jljohg== xml-name-validator@^3.0.0: version "3.0.0" From 5c6145c1a0e4a918c991a7dc691f94d75ed09825 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Thu, 2 Sep 2021 17:56:37 +0200 Subject: [PATCH 74/93] fixed esm issue --- examples/react/develop/simple-counter/scripts/analyze.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/react/develop/simple-counter/scripts/analyze.js b/examples/react/develop/simple-counter/scripts/analyze.js index ea2c0df2..59a7bb13 100644 --- a/examples/react/develop/simple-counter/scripts/analyze.js +++ b/examples/react/develop/simple-counter/scripts/analyze.js @@ -2,7 +2,7 @@ // 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'; +const dotenv = require('dotenv'); // Loads environment variables from the '.env' file dotenv.config(); From 8368a0079e0021d75af77a35b27c32155f3fb0be Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Fri, 3 Sep 2021 09:56:50 +0200 Subject: [PATCH 75/93] fixed typo --- examples/plainjs/develop/tree-shaking/package.json | 7 ++++--- packages/core/src/logCodeManager.ts | 6 ++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/examples/plainjs/develop/tree-shaking/package.json b/examples/plainjs/develop/tree-shaking/package.json index 4af19172..2bd5d547 100644 --- a/examples/plainjs/develop/tree-shaking/package.json +++ b/examples/plainjs/develop/tree-shaking/package.json @@ -6,8 +6,8 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "webpack", - "install:dev:agile": "yalc add @agile-ts/core & yarn install", - "install:prod:agile": "yarn add @agile-ts/core & yarn install" + "install:dev:agile": "yalc add @agile-ts/core @agile-ts/logger & yarn install", + "install:prod:agile": "yarn add @agile-ts/core @agile-ts/logger & yarn install" }, "author": "", "license": "ISC", @@ -16,6 +16,7 @@ "webpack-cli": "^4.8.0" }, "dependencies": { - "@agile-ts/core": "file:.yalc/@agile-ts/core" + "@agile-ts/core": "file:.yalc/@agile-ts/core", + "@agile-ts/logger": "file:.yalc/@agile-ts/logger" } } diff --git a/packages/core/src/logCodeManager.ts b/packages/core/src/logCodeManager.ts index 70aa95e3..fb5aaffc 100644 --- a/packages/core/src/logCodeManager.ts +++ b/packages/core/src/logCodeManager.ts @@ -171,9 +171,7 @@ const niceLogCodeMessages = { // Note: Not outsource the 'production' env check, // because then webpack can't treeshake based on the current env const logCodeMessages: typeof niceLogCodeMessages = - typeof process === 'object' && process.env.NODE_ENV !== 'production' - ? niceLogCodeMessages - : ({} as any); + process.env.NODE_ENV !== 'production' ? niceLogCodeMessages : ({} as any); /** * Specifies whether the LogCodeManager is allowed to print any logs. @@ -294,7 +292,7 @@ export function assignAdditionalLogs< } let tempLogCodeManager: LogCodeManagerInterface; -if (typeof process === 'object' && process.env.NODE_ENV !== 'production') { +if (process.env.NODE_ENV !== 'production') { let loggerPackage: any = null; try { loggerPackage = require('@agile-ts/logger'); From 22a047eb0813ffa9aecfc5cf750ce343f075dac8 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Fri, 3 Sep 2021 10:12:19 +0200 Subject: [PATCH 76/93] bumped version for manual release --- packages/core/package.json | 2 +- packages/react/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 5e8cb90b..88d0e1e0 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@agile-ts/core", - "version": "0.2.0-alpha.4", + "version": "0.2.0-alpha.5", "author": "BennoDev", "license": "MIT", "homepage": "https://agile-ts.org/", diff --git a/packages/react/package.json b/packages/react/package.json index 2e03a774..2706a9e5 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@agile-ts/react", - "version": "0.2.0-alpha.1", + "version": "0.2.0-alpha.2", "author": "BennoDev", "license": "MIT", "homepage": "https://agile-ts.org/", From 0c2e76a50ebba0ab34dc8e532296ddc8ffada4f3 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sat, 4 Sep 2021 19:39:39 +0200 Subject: [PATCH 77/93] fixed typos --- .../benchmarks/react/1000fields/index.ts | 16 ++-- packages/core/src/collection/collection.ts | 42 ++++++---- packages/core/src/collection/group/index.ts | 80 ++++++++++++++++++- 3 files changed, 111 insertions(+), 27 deletions(-) diff --git a/benchmark/benchmarks/react/1000fields/index.ts b/benchmark/benchmarks/react/1000fields/index.ts index 9c1385be..9b318872 100644 --- a/benchmark/benchmarks/react/1000fields/index.ts +++ b/benchmark/benchmarks/react/1000fields/index.ts @@ -80,16 +80,16 @@ suite .add('Agile Collection', configTest(agileCollection)) .add('Agile State', configTest(agileState)) .add('Agile nested State', configTest(agileNestedState)) - .add('Pulse Collection', configTest(pulseCollection)) + // .add('Pulse Collection', configTest(pulseCollection)) // .add('Pulse State', configTest(pulseState)) // .add('Pulse nested State', configTest(pulseNestedState)) - .add('Hookstate', configTest(hookstate)) - .add('Jotai', configTest(jotai)) - .add('Mobx', configTest(mobx)) - .add('Nano Stores', configTest(nanostores)) - .add('Recoil', configTest(recoil)) - .add('Redux', configTest(redux)) - .add('Valtio', configTest(valtio)) + // .add('Hookstate', configTest(hookstate)) + // .add('Jotai', configTest(jotai)) + // .add('Mobx', configTest(mobx)) + // .add('Nano Stores', configTest(nanostores)) + // .add('Recoil', configTest(recoil)) + // .add('Redux', configTest(redux)) + // .add('Valtio', configTest(valtio)) // Add Listener .on('start', function (this: any) { diff --git a/packages/core/src/collection/collection.ts b/packages/core/src/collection/collection.ts index 31b8a107..2e541dad 100644 --- a/packages/core/src/collection/collection.ts +++ b/packages/core/src/collection/collection.ts @@ -1,26 +1,27 @@ import { Agile, - Item, - Group, - GroupKey, - Selector, - SelectorKey, - StorageKey, - GroupConfigInterface, - isValidObject, - normalizeArray, - copy, CollectionPersistent, - GroupAddConfigInterface, ComputedTracker, + copy, + defineConfig, generateId, - SideEffectConfigInterface, - SelectorConfigInterface, - removeProperties, + Group, + GroupAddConfigInterface, + GroupConfigInterface, + GroupKey, isFunction, + isValidObject, + Item, LogCodeManager, + normalizeArray, PatchOptionConfigInterface, - defineConfig, + removeProperties, + Selector, + SelectorConfigInterface, + SelectorKey, + SideEffectConfigInterface, + StorageKey, + TrackedChangeMethod, } from '../internal'; export class Collection< @@ -1473,12 +1474,19 @@ export class Collection< // Rebuild Groups that include itemKey for (const groupKey in this.groups) { const group = this.getGroup(groupKey); - if (group?.has(itemKey)) { + if (group != null && group.has(itemKey)) { // Not necessary because a sideEffect of ingesting the Group // into the runtime is to rebuilt itself // group.rebuild(); - group?.rebuild({ + // TODO + group.trackChange({ + key: itemKey, + index: group.nextStateValue.findIndex((ik) => itemKey === ik), + method: TrackedChangeMethod.UPDATE, + }); + + group.rebuild({ background: config?.background, sideEffects: config?.sideEffects, storage: false, diff --git a/packages/core/src/collection/group/index.ts b/packages/core/src/collection/group/index.ts index dc339159..f3bdca9a 100644 --- a/packages/core/src/collection/group/index.ts +++ b/packages/core/src/collection/group/index.ts @@ -39,6 +39,13 @@ export class Group< // Keeps track of all Item identifiers for Items that couldn't be found in the Collection notFoundItemKeys: Array = []; + // Keeps track of all changes made between rebuilds (add, remove, update) + // Why not rebuilding the Group directly in the add(), remove() method? + // Because rebuilding the Group is a side effect of the Group. + // A rebuild should always happen whenever the Group mutates. + // (-> Simplicity and keeping the current structure to not rewrite all tests) + trackedChanges: TrackedChangeInterface[] = []; + /** * An extension of the State Class that categorizes and preserves the ordering of structured data. * It allows us to cluster together data from a Collection as an array of Item keys. @@ -130,12 +137,15 @@ export class Group< */ public remove( itemKeys: ItemKey | ItemKey[], - config: StateIngestConfigInterface = {} + config: GroupRemoveConfigInterface = {} ): this { const _itemKeys = normalizeArray(itemKeys); const notExistingItemKeysInCollection: Array = []; const notExistingItemKeys: Array = []; let newGroupValue = copy(this.nextStateValue); + defineConfig(config, { + softRebuild: true, + }); // Remove itemKeys from Group _itemKeys.forEach((itemKey) => { @@ -146,6 +156,14 @@ export class Group< return; } + if (config.softRebuild) { + this.trackChange({ + index: newGroupValue.findIndex((ik) => ik === itemKey), + method: TrackedChangeMethod.REMOVE, + key: itemKey, + }); + } + // Check if itemKey exists in Collection if (!this.collection().getItem(itemKey)) notExistingItemKeysInCollection.push(itemKey); @@ -187,17 +205,33 @@ export class Group< defineConfig(config, { method: 'push', overwrite: false, + softRebuild: true, }); // Add itemKeys to Group _itemKeys.forEach((itemKey) => { + const exists = newGroupValue.includes(itemKey); + // Check if itemKey exists in Collection if (!this.collection().getItem(itemKey)) notExistingItemKeysInCollection.push(itemKey); + // Track changes to soft rebuild the Group when rebuilding the Group + if (config.softRebuild) { + this.trackChange({ + method: exists ? TrackedChangeMethod.UPDATE : TrackedChangeMethod.ADD, + key: itemKey, + index: exists + ? newGroupValue.findIndex((ik) => ik === itemKey) + : config.method === 'push' + ? newGroupValue.length - 1 + : 0, + }); + } + // Remove itemKey temporary from newGroupValue // if it should be overwritten and already exists in the newGroupValue - if (newGroupValue.includes(itemKey)) { + if (exists) { if (config.overwrite) { newGroupValue = newGroupValue.filter((key) => key !== itemKey); } else { @@ -373,6 +407,14 @@ export class Group< return this; } + + /** + * TODO + * @param change + */ + public trackChange(change: TrackedChangeInterface) { + this.trackedChanges.push(change); + } } export type GroupKey = string | number; @@ -400,6 +442,19 @@ export interface GroupAddConfigInterface extends StateIngestConfigInterface { * @default false */ overwrite?: boolean; + /** + * TODO + * @default true + */ + softRebuild?: boolean; +} + +export interface GroupRemoveConfigInterface extends StateIngestConfigInterface { + /** + * TODO + * @default true + */ + softRebuild?: boolean; } export interface GroupConfigInterface { @@ -425,3 +480,24 @@ export interface GroupPersistConfigInterface */ followCollectionPersistKeyPattern?: boolean; } + +export enum TrackedChangeMethod { + ADD, + REMOVE, + UPDATE, +} + +export interface TrackedChangeInterface { + /** + * TODO + */ + method: TrackedChangeMethod; + /** + * TODO + */ + key: ItemKey; + /** + * TODO + */ + index: number; +} From c894d24e5dd5755c4a1ef671ec63b7604c0e2cde Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Mon, 6 Sep 2021 20:33:30 +0200 Subject: [PATCH 78/93] added basic soft rebuild --- packages/core/src/collection/group/index.ts | 29 +++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/core/src/collection/group/index.ts b/packages/core/src/collection/group/index.ts index f3bdca9a..f5b458b7 100644 --- a/packages/core/src/collection/group/index.ts +++ b/packages/core/src/collection/group/index.ts @@ -384,6 +384,35 @@ export class Group< // contains the Items which are essential for a proper rebuild) if (!this.collection().isInstantiated) return this; + // Soft rebuild the Collection (-> rebuild only parts of the Collection) + if (this.trackedChanges.length > 0) { + this.trackedChanges.forEach((change) => { + const item = this.collection().getItem(change.key); + switch (change.method) { + case TrackedChangeMethod.ADD: + if (item != null) { + this._value.splice(change.index, 0, change.key); + this._output.splice(change.index, 0, copy(item._value)); + } + break; + case TrackedChangeMethod.UPDATE: + if (item != null) { + this._output.splice(change.index, 0, copy(item._value)); + } + break; + case TrackedChangeMethod.REMOVE: + this._value.splice(change.index, 1); + this._output.splice(change.index, 1); + break; + default: + } + }); + this.trackedChanges = []; + return this; + } + + // Rebuild the whole Collection + // Fetch Items from Collection this._value.forEach((itemKey) => { const item = this.collection().getItem(itemKey); From 91221f4948cc075f28e42e53089698abe2f21b5c Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Tue, 7 Sep 2021 20:29:59 +0200 Subject: [PATCH 79/93] fixed typos --- packages/core/src/collection/collection.ts | 5 ----- .../src/collection/group/group.observer.ts | 16 ++++++-------- packages/core/src/collection/group/index.ts | 21 ++++++++++++++----- .../collection/group/group.observer.test.ts | 10 ++++----- .../tests/unit/collection/group/group.test.ts | 8 +++---- 5 files changed, 31 insertions(+), 29 deletions(-) diff --git a/packages/core/src/collection/collection.ts b/packages/core/src/collection/collection.ts index 2e541dad..bf713291 100644 --- a/packages/core/src/collection/collection.ts +++ b/packages/core/src/collection/collection.ts @@ -1475,11 +1475,6 @@ export class Collection< for (const groupKey in this.groups) { const group = this.getGroup(groupKey); if (group != null && group.has(itemKey)) { - // Not necessary because a sideEffect of ingesting the Group - // into the runtime is to rebuilt itself - // group.rebuild(); - - // TODO group.trackChange({ key: itemKey, index: group.nextStateValue.findIndex((ik) => itemKey === ik), diff --git a/packages/core/src/collection/group/group.observer.ts b/packages/core/src/collection/group/group.observer.ts index 4c0e860c..872bfcf0 100644 --- a/packages/core/src/collection/group/group.observer.ts +++ b/packages/core/src/collection/group/group.observer.ts @@ -45,14 +45,14 @@ export class GroupObserver extends Observer { * into the runtime wrapped into a Runtime-Job * where it is executed accordingly. * - * During the execution the runtime applies the rebuilt `nextGroupOutput` to the Group, + * During the execution the runtime applies the current Group `output` to the Group, * updates its dependents and re-renders the UI-Components it is subscribed to. * * @internal * @param config - Configuration object */ public ingest(config: GroupIngestConfigInterface = {}): void { - this.group().rebuild(config); + this.ingestOutput(this.group()._output, config); } /** @@ -63,11 +63,11 @@ export class GroupObserver extends Observer { * updates its dependents and re-renders the UI-Components it is subscribed to. * * @internal - * @param newGroupItems - New Group Items to be applied to the Group. + * @param newGroupOutput - New Group Output to be applied to the Group. * @param config - Configuration object. */ - public ingestItems( - newGroupItems: Item[], + public ingestOutput( + newGroupOutput: DataType[], config: GroupIngestConfigInterface = {} ): void { const group = this.group(); @@ -89,11 +89,7 @@ export class GroupObserver extends Observer { } // Assign next Group output to Observer - this.nextGroupOutput = copy( - newGroupItems.map((item) => { - return item._value; - }) - ); + this.nextGroupOutput = copy(newGroupOutput); // Check if current Group output and to assign Group output are equal if (equal(group._output, this.nextGroupOutput) && !config.force) return; diff --git a/packages/core/src/collection/group/index.ts b/packages/core/src/collection/group/index.ts index f5b458b7..19623438 100644 --- a/packages/core/src/collection/group/index.ts +++ b/packages/core/src/collection/group/index.ts @@ -376,9 +376,6 @@ export class Group< * @param config - Configuration object */ public rebuild(config: StateIngestConfigInterface = {}): this { - const notFoundItemKeys: Array = []; // Item keys that couldn't be found in the Collection - const groupItems: Array> = []; - // Don't rebuild Group if Collection isn't correctly instantiated yet // (because only after a successful instantiation the Collection // contains the Items which are essential for a proper rebuild) @@ -386,13 +383,16 @@ export class Group< // Soft rebuild the Collection (-> rebuild only parts of the Collection) if (this.trackedChanges.length > 0) { + let ingestGroupValue = false; this.trackedChanges.forEach((change) => { const item = this.collection().getItem(change.key); + switch (change.method) { case TrackedChangeMethod.ADD: if (item != null) { this._value.splice(change.index, 0, change.key); this._output.splice(change.index, 0, copy(item._value)); + ingestGroupValue = true; } break; case TrackedChangeMethod.UPDATE: @@ -403,15 +403,21 @@ export class Group< case TrackedChangeMethod.REMOVE: this._value.splice(change.index, 1); this._output.splice(change.index, 1); + ingestGroupValue = true; break; default: } }); this.trackedChanges = []; + this.observers['output'].ingest(config); + if (ingestGroupValue) this.observers['value'].ingest(config); return this; } - // Rebuild the whole Collection + // Hard rebuild the whole Collection + + const notFoundItemKeys: Array = []; // Item keys that couldn't be found in the Collection + const groupItems: Array> = []; // Fetch Items from Collection this._value.forEach((itemKey) => { @@ -432,7 +438,12 @@ export class Group< this.notFoundItemKeys = notFoundItemKeys; // Ingest rebuilt Group output into the Runtime - this.observers['output'].ingestItems(groupItems, config); + this.observers['output'].ingestOutput( + groupItems.map((item) => { + return item._value; + }), + config + ); return this; } diff --git a/packages/core/tests/unit/collection/group/group.observer.test.ts b/packages/core/tests/unit/collection/group/group.observer.test.ts index 488147b1..d5b6a783 100644 --- a/packages/core/tests/unit/collection/group/group.observer.test.ts +++ b/packages/core/tests/unit/collection/group/group.observer.test.ts @@ -158,7 +158,7 @@ describe('GroupObserver Tests', () => { }); }); - groupObserver.ingestItems([dummyItem1, dummyItem2]); + groupObserver.ingestOutput([dummyItem1, dummyItem2]); expect(groupObserver.nextGroupOutput).toStrictEqual([ dummyItem1._value, @@ -190,7 +190,7 @@ describe('GroupObserver Tests', () => { }); }); - groupObserver.ingestItems([dummyItem1, dummyItem2], { + groupObserver.ingestOutput([dummyItem1, dummyItem2], { perform: false, force: true, sideEffects: { @@ -219,7 +219,7 @@ describe('GroupObserver Tests', () => { () => { dummyGroup._output = [dummyItem1._value, dummyItem2._value]; - groupObserver.ingestItems([dummyItem1, dummyItem2]); + groupObserver.ingestOutput([dummyItem1, dummyItem2]); expect(groupObserver.nextGroupOutput).toStrictEqual([ dummyItem1._value, @@ -249,7 +249,7 @@ describe('GroupObserver Tests', () => { }); }); - groupObserver.ingestItems([dummyItem1, dummyItem2], { force: true }); + groupObserver.ingestOutput([dummyItem1, dummyItem2], { force: true }); expect(groupObserver.nextGroupOutput).toStrictEqual([ dummyItem1._value, @@ -281,7 +281,7 @@ describe('GroupObserver Tests', () => { }); dummyGroup.isPlaceholder = true; - groupObserver.ingestItems([dummyItem1, dummyItem2]); + groupObserver.ingestOutput([dummyItem1, dummyItem2]); expect(groupObserver.nextGroupOutput).toStrictEqual([ dummyItem1._value, diff --git a/packages/core/tests/unit/collection/group/group.test.ts b/packages/core/tests/unit/collection/group/group.test.ts index 17574a02..45ac8d30 100644 --- a/packages/core/tests/unit/collection/group/group.test.ts +++ b/packages/core/tests/unit/collection/group/group.test.ts @@ -512,7 +512,7 @@ describe('Group Tests', () => { describe('rebuild function tests', () => { beforeEach(() => { group._value = ['dummyItem1Key', 'dummyItem3Key', 'dummyItem2Key']; - group.observers['output'].ingestItems = jest.fn(); + group.observers['output'].ingestOutput = jest.fn(); }); it('should ingest the built Group output and set notFoundItemKeys to the not found Item Keys (default config)', () => { @@ -520,7 +520,7 @@ describe('Group Tests', () => { expect(group.notFoundItemKeys).toStrictEqual(['dummyItem3Key']); expect(group._output).toStrictEqual([]); // because of mocking 'ingestValue' - expect(group.observers['output'].ingestItems).toHaveBeenCalledWith( + expect(group.observers['output'].ingestOutput).toHaveBeenCalledWith( [dummyItem1, dummyItem2], {} ); @@ -537,7 +537,7 @@ describe('Group Tests', () => { expect(group.notFoundItemKeys).toStrictEqual(['dummyItem3Key']); expect(group._output).toStrictEqual([]); // because of mocking 'ingestValue' - expect(group.observers['output'].ingestItems).toHaveBeenCalledWith( + expect(group.observers['output'].ingestOutput).toHaveBeenCalledWith( [dummyItem1, dummyItem2], { storage: true, overwrite: true, background: false } ); @@ -556,7 +556,7 @@ describe('Group Tests', () => { expect(group.notFoundItemKeys).toStrictEqual([]); expect(group._output).toStrictEqual([]); - expect(group.observers['output'].ingestItems).not.toHaveBeenCalled(); + expect(group.observers['output'].ingestOutput).not.toHaveBeenCalled(); LogMock.hasNotLogged('warn'); }); }); From 5aad7379591fb908a812f91a1a11d4e6d7ccedaa Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Wed, 8 Sep 2021 08:35:10 +0200 Subject: [PATCH 80/93] fixed typos --- benchmark/package.json | 1 + .../functional-component-ts/src/App.tsx | 2 +- .../src/collection/group/group.observer.ts | 5 ++-- packages/core/src/collection/group/index.ts | 28 +++++++++---------- packages/core/src/logCodeManager.ts | 4 ++- packages/core/src/state/state.observer.ts | 1 - 6 files changed, 21 insertions(+), 20 deletions(-) diff --git a/benchmark/package.json b/benchmark/package.json index 52dcd5bf..137ac46f 100644 --- a/benchmark/package.json +++ b/benchmark/package.json @@ -21,6 +21,7 @@ }, "dependencies": { "@agile-ts/core": "file:.yalc/@agile-ts/core", + "@agile-ts/logger": "file:.yalc/@agile-ts/logger", "@agile-ts/react": "file:.yalc/@agile-ts/react", "@hookstate/core": "^3.0.8", "@pulsejs/core": "^4.0.0-beta.3", diff --git a/examples/react/develop/functional-component-ts/src/App.tsx b/examples/react/develop/functional-component-ts/src/App.tsx index 7c0849b3..2321584a 100644 --- a/examples/react/develop/functional-component-ts/src/App.tsx +++ b/examples/react/develop/functional-component-ts/src/App.tsx @@ -1,7 +1,6 @@ import React, { useEffect } from 'react'; import './App.css'; import { useAgile, useWatcher, useProxy, useSelector } from '@agile-ts/react'; -import { useEvent } from '@agile-ts/event/dist/react'; import { COUNTUP, externalCreatedItem, @@ -15,6 +14,7 @@ import { } from './core'; import { generateId } from '@agile-ts/utils'; import { globalBind } from '@agile-ts/core'; +import { useEvent } from '@agile-ts/event'; let rerenderCount = 0; let rerenderCountInCountupView = 0; diff --git a/packages/core/src/collection/group/group.observer.ts b/packages/core/src/collection/group/group.observer.ts index 872bfcf0..32fa71bd 100644 --- a/packages/core/src/collection/group/group.observer.ts +++ b/packages/core/src/collection/group/group.observer.ts @@ -45,14 +45,14 @@ export class GroupObserver extends Observer { * into the runtime wrapped into a Runtime-Job * where it is executed accordingly. * - * During the execution the runtime applies the current Group `output` to the Group, + * During the execution the runtime applies the `nextGroupOutput` to the Group, * updates its dependents and re-renders the UI-Components it is subscribed to. * * @internal * @param config - Configuration object */ public ingest(config: GroupIngestConfigInterface = {}): void { - this.ingestOutput(this.group()._output, config); + this.ingestOutput(this.group().nextGroupOutput, config); } /** @@ -126,6 +126,7 @@ export class GroupObserver extends Observer { // Assign new Group output group._output = copy(observer.nextGroupOutput); + group.nextGroupOutput = copy(observer.nextGroupOutput); // Assign new public output to the Observer (output used by the Integrations) job.observer.previousValue = copy(job.observer.value); diff --git a/packages/core/src/collection/group/index.ts b/packages/core/src/collection/group/index.ts index 19623438..f39eaef9 100644 --- a/packages/core/src/collection/group/index.ts +++ b/packages/core/src/collection/group/index.ts @@ -30,21 +30,23 @@ export class Group< static rebuildGroupSideEffectKey = 'rebuildGroup'; // Item values represented by the Group - _output: Array = []; + public _output: Array = []; + // Next output of the Group (which can be used for dynamic Group updates) + public nextGroupOutput: Array = []; // Manages dependencies to other States and subscriptions of UI-Components. // It also serves as an interface to the runtime. public observers: GroupObservers = {} as any; // Keeps track of all Item identifiers for Items that couldn't be found in the Collection - notFoundItemKeys: Array = []; + public notFoundItemKeys: Array = []; // Keeps track of all changes made between rebuilds (add, remove, update) // Why not rebuilding the Group directly in the add(), remove() method? // Because rebuilding the Group is a side effect of the Group. // A rebuild should always happen whenever the Group mutates. // (-> Simplicity and keeping the current structure to not rewrite all tests) - trackedChanges: TrackedChangeInterface[] = []; + public trackedChanges: TrackedChangeInterface[] = []; /** * An extension of the State Class that categorizes and preserves the ordering of structured data. @@ -143,7 +145,7 @@ export class Group< const notExistingItemKeysInCollection: Array = []; const notExistingItemKeys: Array = []; let newGroupValue = copy(this.nextStateValue); - defineConfig(config, { + config = defineConfig(config, { softRebuild: true, }); @@ -202,7 +204,7 @@ export class Group< const notExistingItemKeysInCollection: Array = []; const existingItemKeys: Array = []; let newGroupValue = copy(this.nextStateValue); - defineConfig(config, { + config = defineConfig(config, { method: 'push', overwrite: false, softRebuild: true, @@ -217,7 +219,7 @@ export class Group< notExistingItemKeysInCollection.push(itemKey); // Track changes to soft rebuild the Group when rebuilding the Group - if (config.softRebuild) { + if (config.softRebuild && (!exists || (exists && config.overwrite))) { this.trackChange({ method: exists ? TrackedChangeMethod.UPDATE : TrackedChangeMethod.ADD, key: itemKey, @@ -383,34 +385,30 @@ export class Group< // Soft rebuild the Collection (-> rebuild only parts of the Collection) if (this.trackedChanges.length > 0) { - let ingestGroupValue = false; this.trackedChanges.forEach((change) => { const item = this.collection().getItem(change.key); switch (change.method) { case TrackedChangeMethod.ADD: if (item != null) { - this._value.splice(change.index, 0, change.key); - this._output.splice(change.index, 0, copy(item._value)); - ingestGroupValue = true; + // this._value.splice(change.index, 0, change.key); // Already updated in 'add' method + this.nextGroupOutput.splice(change.index, 0, copy(item._value)); } break; case TrackedChangeMethod.UPDATE: if (item != null) { - this._output.splice(change.index, 0, copy(item._value)); + this.nextGroupOutput.splice(change.index, 0, copy(item._value)); } break; case TrackedChangeMethod.REMOVE: - this._value.splice(change.index, 1); - this._output.splice(change.index, 1); - ingestGroupValue = true; + // this._value.splice(change.index, 1); // Already updated in 'remove' method + this.nextGroupOutput.splice(change.index, 1); break; default: } }); this.trackedChanges = []; this.observers['output'].ingest(config); - if (ingestGroupValue) this.observers['value'].ingest(config); return this; } diff --git a/packages/core/src/logCodeManager.ts b/packages/core/src/logCodeManager.ts index fb5aaffc..5c1afbcf 100644 --- a/packages/core/src/logCodeManager.ts +++ b/packages/core/src/logCodeManager.ts @@ -305,7 +305,9 @@ if (process.env.NODE_ENV !== 'production') { log, logCodeLogTypes: logCodeTypes, logCodeMessages: logCodeMessages, - getLogger: loggerPackage.getLogger, + getLogger: () => { + return loggerPackage?.getLogger() ?? null; + }, logIfTags, setAllowLogging, }; diff --git a/packages/core/src/state/state.observer.ts b/packages/core/src/state/state.observer.ts index f2ef181c..6994237c 100644 --- a/packages/core/src/state/state.observer.ts +++ b/packages/core/src/state/state.observer.ts @@ -15,7 +15,6 @@ import { ObserverKey, defineConfig, } from '../internal'; -import type { EnhancedState } from '../internal'; export class StateObserver extends Observer { // State the Observer belongs to From 1211f0bb400c46fae941cfada88d05696b546efe Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Wed, 8 Sep 2021 08:58:48 +0200 Subject: [PATCH 81/93] fixed typos --- packages/core/src/collection/group/group.observer.ts | 1 - packages/core/src/collection/group/index.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/core/src/collection/group/group.observer.ts b/packages/core/src/collection/group/group.observer.ts index 32fa71bd..0029ecd1 100644 --- a/packages/core/src/collection/group/group.observer.ts +++ b/packages/core/src/collection/group/group.observer.ts @@ -6,7 +6,6 @@ import { equal, generateId, RuntimeJob, - Item, IngestConfigInterface, CreateRuntimeJobConfigInterface, defineConfig, diff --git a/packages/core/src/collection/group/index.ts b/packages/core/src/collection/group/index.ts index f39eaef9..ece977e4 100644 --- a/packages/core/src/collection/group/index.ts +++ b/packages/core/src/collection/group/index.ts @@ -397,7 +397,7 @@ export class Group< break; case TrackedChangeMethod.UPDATE: if (item != null) { - this.nextGroupOutput.splice(change.index, 0, copy(item._value)); + this.nextGroupOutput[change.index] = copy(item._value); } break; case TrackedChangeMethod.REMOVE: From 1da3b28689d0df5ee2c071f8f62dcbe2aeadfc7e Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Thu, 9 Sep 2021 07:18:29 +0200 Subject: [PATCH 82/93] added fields example --- examples/react/develop/fields/.env | 1 + examples/react/develop/fields/.gitignore | 23 ++++++ examples/react/develop/fields/README.md | 70 ++++++++++++++++++ examples/react/develop/fields/package.json | 38 ++++++++++ .../react/develop/fields/public/favicon.ico | Bin 0 -> 3870 bytes .../react/develop/fields/public/index.html | 43 +++++++++++ .../react/develop/fields/public/logo192.png | Bin 0 -> 5347 bytes .../react/develop/fields/public/logo512.png | Bin 0 -> 9664 bytes .../react/develop/fields/public/manifest.json | 25 +++++++ .../react/develop/fields/public/robots.txt | 3 + examples/react/develop/fields/src/App.js | 60 +++++++++++++++ examples/react/develop/fields/src/index.js | 10 +++ 12 files changed, 273 insertions(+) create mode 100644 examples/react/develop/fields/.env create mode 100644 examples/react/develop/fields/.gitignore create mode 100644 examples/react/develop/fields/README.md create mode 100644 examples/react/develop/fields/package.json create mode 100644 examples/react/develop/fields/public/favicon.ico create mode 100644 examples/react/develop/fields/public/index.html create mode 100644 examples/react/develop/fields/public/logo192.png create mode 100644 examples/react/develop/fields/public/logo512.png create mode 100644 examples/react/develop/fields/public/manifest.json create mode 100644 examples/react/develop/fields/public/robots.txt create mode 100644 examples/react/develop/fields/src/App.js create mode 100644 examples/react/develop/fields/src/index.js diff --git a/examples/react/develop/fields/.env b/examples/react/develop/fields/.env new file mode 100644 index 00000000..6f809cc2 --- /dev/null +++ b/examples/react/develop/fields/.env @@ -0,0 +1 @@ +SKIP_PREFLIGHT_CHECK=true diff --git a/examples/react/develop/fields/.gitignore b/examples/react/develop/fields/.gitignore new file mode 100644 index 00000000..4d29575d --- /dev/null +++ b/examples/react/develop/fields/.gitignore @@ -0,0 +1,23 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/examples/react/develop/fields/README.md b/examples/react/develop/fields/README.md new file mode 100644 index 00000000..02aac3f6 --- /dev/null +++ b/examples/react/develop/fields/README.md @@ -0,0 +1,70 @@ +# Getting Started with Create React App + +This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). + +## Available Scripts + +In the project directory, you can run: + +### `yarn start` + +Runs the app in the development mode.\ +Open [http://localhost:3000](http://localhost:3000) to view it in the browser. + +The page will reload if you make edits.\ +You will also see any lint errors in the console. + +### `yarn test` + +Launches the test runner in the interactive watch mode.\ +See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. + +### `yarn build` + +Builds the app for production to the `build` folder.\ +It correctly bundles React in production mode and optimizes the build for the best performance. + +The build is minified and the filenames include the hashes.\ +Your app is ready to be deployed! + +See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. + +### `yarn eject` + +**Note: this is a one-way operation. Once you `eject`, you can’t go back!** + +If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. + +Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. + +You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. + +## Learn More + +You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). + +To learn React, check out the [React documentation](https://reactjs.org/). + +### Code Splitting + +This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) + +### Analyzing the Bundle Size + +This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) + +### Making a Progressive Web App + +This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) + +### Advanced Configuration + +This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) + +### Deployment + +This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) + +### `yarn build` fails to minify + +This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) diff --git a/examples/react/develop/fields/package.json b/examples/react/develop/fields/package.json new file mode 100644 index 00000000..ad32f1c1 --- /dev/null +++ b/examples/react/develop/fields/package.json @@ -0,0 +1,38 @@ +{ + "name": "fields", + "version": "0.1.0", + "private": true, + "dependencies": { + "@agile-ts/core": "file:.yalc/@agile-ts/core", + "@agile-ts/react": "file:.yalc/@agile-ts/react", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-scripts": "4.0.3" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject", + "install:dev:agile": "yalc add @agile-ts/core @agile-ts/react & yarn install", + "install:prod:agile": "yarn add @agile-ts/core @agile-ts/react & yarn install" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/examples/react/develop/fields/public/favicon.ico b/examples/react/develop/fields/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a11777cc471a4344702741ab1c8a588998b1311a GIT binary patch literal 3870 zcma);c{J4h9>;%nil|2-o+rCuEF-(I%-F}ijC~o(k~HKAkr0)!FCj~d>`RtpD?8b; zXOC1OD!V*IsqUwzbMF1)-gEDD=A573Z-&G7^LoAC9|WO7Xc0Cx1g^Zu0u_SjAPB3vGa^W|sj)80f#V0@M_CAZTIO(t--xg= z!sii`1giyH7EKL_+Wi0ab<)&E_0KD!3Rp2^HNB*K2@PHCs4PWSA32*-^7d{9nH2_E zmC{C*N*)(vEF1_aMamw2A{ZH5aIDqiabnFdJ|y0%aS|64E$`s2ccV~3lR!u<){eS` z#^Mx6o(iP1Ix%4dv`t@!&Za-K@mTm#vadc{0aWDV*_%EiGK7qMC_(`exc>-$Gb9~W!w_^{*pYRm~G zBN{nA;cm^w$VWg1O^^<6vY`1XCD|s_zv*g*5&V#wv&s#h$xlUilPe4U@I&UXZbL z0)%9Uj&@yd03n;!7do+bfixH^FeZ-Ema}s;DQX2gY+7g0s(9;`8GyvPY1*vxiF&|w z>!vA~GA<~JUqH}d;DfBSi^IT*#lrzXl$fNpq0_T1tA+`A$1?(gLb?e#0>UELvljtQ zK+*74m0jn&)5yk8mLBv;=@}c{t0ztT<v;Avck$S6D`Z)^c0(jiwKhQsn|LDRY&w(Fmi91I7H6S;b0XM{e zXp0~(T@k_r-!jkLwd1_Vre^v$G4|kh4}=Gi?$AaJ)3I+^m|Zyj#*?Kp@w(lQdJZf4 z#|IJW5z+S^e9@(6hW6N~{pj8|NO*>1)E=%?nNUAkmv~OY&ZV;m-%?pQ_11)hAr0oAwILrlsGawpxx4D43J&K=n+p3WLnlDsQ$b(9+4 z?mO^hmV^F8MV{4Lx>(Q=aHhQ1){0d*(e&s%G=i5rq3;t{JC zmgbn5Nkl)t@fPH$v;af26lyhH!k+#}_&aBK4baYPbZy$5aFx4}ka&qxl z$=Rh$W;U)>-=S-0=?7FH9dUAd2(q#4TCAHky!$^~;Dz^j|8_wuKc*YzfdAht@Q&ror?91Dm!N03=4=O!a)I*0q~p0g$Fm$pmr$ zb;wD;STDIi$@M%y1>p&_>%?UP($15gou_ue1u0!4(%81;qcIW8NyxFEvXpiJ|H4wz z*mFT(qVx1FKufG11hByuX%lPk4t#WZ{>8ka2efjY`~;AL6vWyQKpJun2nRiZYDij$ zP>4jQXPaP$UC$yIVgGa)jDV;F0l^n(V=HMRB5)20V7&r$jmk{UUIe zVjKroK}JAbD>B`2cwNQ&GDLx8{pg`7hbA~grk|W6LgiZ`8y`{Iq0i>t!3p2}MS6S+ zO_ruKyAElt)rdS>CtF7j{&6rP-#c=7evGMt7B6`7HG|-(WL`bDUAjyn+k$mx$CH;q2Dz4x;cPP$hW=`pFfLO)!jaCL@V2+F)So3}vg|%O*^T1j>C2lx zsURO-zIJC$^$g2byVbRIo^w>UxK}74^TqUiRR#7s_X$e)$6iYG1(PcW7un-va-S&u zHk9-6Zn&>T==A)lM^D~bk{&rFzCi35>UR!ZjQkdSiNX*-;l4z9j*7|q`TBl~Au`5& z+c)*8?#-tgUR$Zd%Q3bs96w6k7q@#tUn`5rj+r@_sAVVLqco|6O{ILX&U-&-cbVa3 zY?ngHR@%l{;`ri%H*0EhBWrGjv!LE4db?HEWb5mu*t@{kv|XwK8?npOshmzf=vZA@ zVSN9sL~!sn?r(AK)Q7Jk2(|M67Uy3I{eRy z_l&Y@A>;vjkWN5I2xvFFTLX0i+`{qz7C_@bo`ZUzDugfq4+>a3?1v%)O+YTd6@Ul7 zAfLfm=nhZ`)P~&v90$&UcF+yXm9sq!qCx3^9gzIcO|Y(js^Fj)Rvq>nQAHI92ap=P z10A4@prk+AGWCb`2)dQYFuR$|H6iDE8p}9a?#nV2}LBCoCf(Xi2@szia7#gY>b|l!-U`c}@ zLdhvQjc!BdLJvYvzzzngnw51yRYCqh4}$oRCy-z|v3Hc*d|?^Wj=l~18*E~*cR_kU z{XsxM1i{V*4GujHQ3DBpl2w4FgFR48Nma@HPgnyKoIEY-MqmMeY=I<%oG~l!f<+FN z1ZY^;10j4M4#HYXP zw5eJpA_y(>uLQ~OucgxDLuf}fVs272FaMxhn4xnDGIyLXnw>Xsd^J8XhcWIwIoQ9} z%FoSJTAGW(SRGwJwb=@pY7r$uQRK3Zd~XbxU)ts!4XsJrCycrWSI?e!IqwqIR8+Jh zlRjZ`UO1I!BtJR_2~7AbkbSm%XQqxEPkz6BTGWx8e}nQ=w7bZ|eVP4?*Tb!$(R)iC z9)&%bS*u(lXqzitAN)Oo=&Ytn>%Hzjc<5liuPi>zC_nw;Z0AE3Y$Jao_Q90R-gl~5 z_xAb2J%eArrC1CN4G$}-zVvCqF1;H;abAu6G*+PDHSYFx@Tdbfox*uEd3}BUyYY-l zTfEsOqsi#f9^FoLO;ChK<554qkri&Av~SIM*{fEYRE?vH7pTAOmu2pz3X?Wn*!ROX ztd54huAk&mFBemMooL33RV-*1f0Q3_(7hl$<#*|WF9P!;r;4_+X~k~uKEqdzZ$5Al zV63XN@)j$FN#cCD;ek1R#l zv%pGrhB~KWgoCj%GT?%{@@o(AJGt*PG#l3i>lhmb_twKH^EYvacVY-6bsCl5*^~L0 zonm@lk2UvvTKr2RS%}T>^~EYqdL1q4nD%0n&Xqr^cK^`J5W;lRRB^R-O8b&HENO||mo0xaD+S=I8RTlIfVgqN@SXDr2&-)we--K7w= zJVU8?Z+7k9dy;s;^gDkQa`0nz6N{T?(A&Iz)2!DEecLyRa&FI!id#5Z7B*O2=PsR0 zEvc|8{NS^)!d)MDX(97Xw}m&kEO@5jqRaDZ!+%`wYOI<23q|&js`&o4xvjP7D_xv@ z5hEwpsp{HezI9!~6O{~)lLR@oF7?J7i>1|5a~UuoN=q&6N}EJPV_GD`&M*v8Y`^2j zKII*d_@Fi$+i*YEW+Hbzn{iQk~yP z>7N{S4)r*!NwQ`(qcN#8SRQsNK6>{)X12nbF`*7#ecO7I)Q$uZsV+xS4E7aUn+U(K baj7?x%VD!5Cxk2YbYLNVeiXvvpMCWYo=by@ literal 0 HcmV?d00001 diff --git a/examples/react/develop/fields/public/index.html b/examples/react/develop/fields/public/index.html new file mode 100644 index 00000000..aa069f27 --- /dev/null +++ b/examples/react/develop/fields/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
+ + + diff --git a/examples/react/develop/fields/public/logo192.png b/examples/react/develop/fields/public/logo192.png new file mode 100644 index 0000000000000000000000000000000000000000..fc44b0a3796c0e0a64c3d858ca038bd4570465d9 GIT binary patch literal 5347 zcmZWtbyO6NvR-oO24RV%BvuJ&=?+<7=`LvyB&A_#M7mSDYw1v6DJkiYl9XjT!%$dLEBTQ8R9|wd3008in6lFF3GV-6mLi?MoP_y~}QUnaDCHI#t z7w^m$@6DI)|C8_jrT?q=f8D?0AM?L)Z}xAo^e^W>t$*Y0KlT5=@bBjT9kxb%-KNdk zeOS1tKO#ChhG7%{ApNBzE2ZVNcxbrin#E1TiAw#BlUhXllzhN$qWez5l;h+t^q#Eav8PhR2|T}y5kkflaK`ba-eoE+Z2q@o6P$)=&` z+(8}+-McnNO>e#$Rr{32ngsZIAX>GH??tqgwUuUz6kjns|LjsB37zUEWd|(&O!)DY zQLrq%Y>)Y8G`yYbYCx&aVHi@-vZ3|ebG!f$sTQqMgi0hWRJ^Wc+Ibv!udh_r%2|U) zPi|E^PK?UE!>_4`f`1k4hqqj_$+d!EB_#IYt;f9)fBOumGNyglU(ofY`yHq4Y?B%- zp&G!MRY<~ajTgIHErMe(Z8JG*;D-PJhd@RX@QatggM7+G(Lz8eZ;73)72Hfx5KDOE zkT(m}i2;@X2AT5fW?qVp?@WgN$aT+f_6eo?IsLh;jscNRp|8H}Z9p_UBO^SJXpZew zEK8fz|0Th%(Wr|KZBGTM4yxkA5CFdAj8=QSrT$fKW#tweUFqr0TZ9D~a5lF{)%-tTGMK^2tz(y2v$i%V8XAxIywrZCp=)83p(zIk6@S5AWl|Oa2hF`~~^W zI;KeOSkw1O#TiQ8;U7OPXjZM|KrnN}9arP)m0v$c|L)lF`j_rpG(zW1Qjv$=^|p*f z>)Na{D&>n`jOWMwB^TM}slgTEcjxTlUby89j1)|6ydRfWERn3|7Zd2&e7?!K&5G$x z`5U3uFtn4~SZq|LjFVrz$3iln-+ucY4q$BC{CSm7Xe5c1J<=%Oagztj{ifpaZk_bQ z9Sb-LaQMKp-qJA*bP6DzgE3`}*i1o3GKmo2pn@dj0;He}F=BgINo};6gQF8!n0ULZ zL>kC0nPSFzlcB7p41doao2F7%6IUTi_+!L`MM4o*#Y#0v~WiO8uSeAUNp=vA2KaR&=jNR2iVwG>7t%sG2x_~yXzY)7K& zk3p+O0AFZ1eu^T3s};B%6TpJ6h-Y%B^*zT&SN7C=N;g|#dGIVMSOru3iv^SvO>h4M=t-N1GSLLDqVTcgurco6)3&XpU!FP6Hlrmj}f$ zp95;b)>M~`kxuZF3r~a!rMf4|&1=uMG$;h^g=Kl;H&Np-(pFT9FF@++MMEx3RBsK?AU0fPk-#mdR)Wdkj)`>ZMl#^<80kM87VvsI3r_c@_vX=fdQ`_9-d(xiI z4K;1y1TiPj_RPh*SpDI7U~^QQ?%0&!$Sh#?x_@;ag)P}ZkAik{_WPB4rHyW#%>|Gs zdbhyt=qQPA7`?h2_8T;-E6HI#im9K>au*(j4;kzwMSLgo6u*}-K`$_Gzgu&XE)udQ zmQ72^eZd|vzI)~!20JV-v-T|<4@7ruqrj|o4=JJPlybwMg;M$Ud7>h6g()CT@wXm` zbq=A(t;RJ^{Xxi*Ff~!|3!-l_PS{AyNAU~t{h;(N(PXMEf^R(B+ZVX3 z8y0;0A8hJYp@g+c*`>eTA|3Tgv9U8#BDTO9@a@gVMDxr(fVaEqL1tl?md{v^j8aUv zm&%PX4^|rX|?E4^CkplWWNv*OKM>DxPa z!RJ)U^0-WJMi)Ksc!^ixOtw^egoAZZ2Cg;X7(5xZG7yL_;UJ#yp*ZD-;I^Z9qkP`} zwCTs0*%rIVF1sgLervtnUo&brwz?6?PXRuOCS*JI-WL6GKy7-~yi0giTEMmDs_-UX zo=+nFrW_EfTg>oY72_4Z0*uG>MnXP=c0VpT&*|rvv1iStW;*^={rP1y?Hv+6R6bxFMkxpWkJ>m7Ba{>zc_q zEefC3jsXdyS5??Mz7IET$Kft|EMNJIv7Ny8ZOcKnzf`K5Cd)&`-fTY#W&jnV0l2vt z?Gqhic}l}mCv1yUEy$%DP}4AN;36$=7aNI^*AzV(eYGeJ(Px-j<^gSDp5dBAv2#?; zcMXv#aj>%;MiG^q^$0MSg-(uTl!xm49dH!{X0){Ew7ThWV~Gtj7h%ZD zVN-R-^7Cf0VH!8O)uUHPL2mO2tmE*cecwQv_5CzWeh)ykX8r5Hi`ehYo)d{Jnh&3p z9ndXT$OW51#H5cFKa76c<%nNkP~FU93b5h-|Cb}ScHs@4Q#|}byWg;KDMJ#|l zE=MKD*F@HDBcX@~QJH%56eh~jfPO-uKm}~t7VkHxHT;)4sd+?Wc4* z>CyR*{w@4(gnYRdFq=^(#-ytb^5ESD?x<0Skhb%Pt?npNW1m+Nv`tr9+qN<3H1f<% zZvNEqyK5FgPsQ`QIu9P0x_}wJR~^CotL|n zk?dn;tLRw9jJTur4uWoX6iMm914f0AJfB@C74a;_qRrAP4E7l890P&{v<}>_&GLrW z)klculcg`?zJO~4;BBAa=POU%aN|pmZJn2{hA!d!*lwO%YSIzv8bTJ}=nhC^n}g(ld^rn#kq9Z3)z`k9lvV>y#!F4e{5c$tnr9M{V)0m(Z< z#88vX6-AW7T2UUwW`g<;8I$Jb!R%z@rCcGT)-2k7&x9kZZT66}Ztid~6t0jKb&9mm zpa}LCb`bz`{MzpZR#E*QuBiZXI#<`5qxx=&LMr-UUf~@dRk}YI2hbMsAMWOmDzYtm zjof16D=mc`^B$+_bCG$$@R0t;e?~UkF?7<(vkb70*EQB1rfUWXh$j)R2)+dNAH5%R zEBs^?N;UMdy}V};59Gu#0$q53$}|+q7CIGg_w_WlvE}AdqoS<7DY1LWS9?TrfmcvT zaypmplwn=P4;a8-%l^e?f`OpGb}%(_mFsL&GywhyN(-VROj`4~V~9bGv%UhcA|YW% zs{;nh@aDX11y^HOFXB$a7#Sr3cEtNd4eLm@Y#fc&j)TGvbbMwze zXtekX_wJqxe4NhuW$r}cNy|L{V=t#$%SuWEW)YZTH|!iT79k#?632OFse{+BT_gau zJwQcbH{b}dzKO?^dV&3nTILYlGw{27UJ72ZN){BILd_HV_s$WfI2DC<9LIHFmtyw? zQ;?MuK7g%Ym+4e^W#5}WDLpko%jPOC=aN)3!=8)s#Rnercak&b3ESRX3z{xfKBF8L z5%CGkFmGO@x?_mPGlpEej!3!AMddChabyf~nJNZxx!D&{@xEb!TDyvqSj%Y5@A{}9 zRzoBn0?x}=krh{ok3Nn%e)#~uh;6jpezhA)ySb^b#E>73e*frBFu6IZ^D7Ii&rsiU z%jzygxT-n*joJpY4o&8UXr2s%j^Q{?e-voloX`4DQyEK+DmrZh8A$)iWL#NO9+Y@!sO2f@rI!@jN@>HOA< z?q2l{^%mY*PNx2FoX+A7X3N}(RV$B`g&N=e0uvAvEN1W^{*W?zT1i#fxuw10%~))J zjx#gxoVlXREWZf4hRkgdHx5V_S*;p-y%JtGgQ4}lnA~MBz-AFdxUxU1RIT$`sal|X zPB6sEVRjGbXIP0U+?rT|y5+ev&OMX*5C$n2SBPZr`jqzrmpVrNciR0e*Wm?fK6DY& zl(XQZ60yWXV-|Ps!A{EF;=_z(YAF=T(-MkJXUoX zI{UMQDAV2}Ya?EisdEW;@pE6dt;j0fg5oT2dxCi{wqWJ<)|SR6fxX~5CzblPGr8cb zUBVJ2CQd~3L?7yfTpLNbt)He1D>*KXI^GK%<`bq^cUq$Q@uJifG>p3LU(!H=C)aEL zenk7pVg}0{dKU}&l)Y2Y2eFMdS(JS0}oZUuVaf2+K*YFNGHB`^YGcIpnBlMhO7d4@vV zv(@N}(k#REdul8~fP+^F@ky*wt@~&|(&&meNO>rKDEnB{ykAZ}k>e@lad7to>Ao$B zz<1(L=#J*u4_LB=8w+*{KFK^u00NAmeNN7pr+Pf+N*Zl^dO{LM-hMHyP6N!~`24jd zXYP|Ze;dRXKdF2iJG$U{k=S86l@pytLx}$JFFs8e)*Vi?aVBtGJ3JZUj!~c{(rw5>vuRF$`^p!P8w1B=O!skwkO5yd4_XuG^QVF z`-r5K7(IPSiKQ2|U9+`@Js!g6sfJwAHVd|s?|mnC*q zp|B|z)(8+mxXyxQ{8Pg3F4|tdpgZZSoU4P&9I8)nHo1@)9_9u&NcT^FI)6|hsAZFk zZ+arl&@*>RXBf-OZxhZerOr&dN5LW9@gV=oGFbK*J+m#R-|e6(Loz(;g@T^*oO)0R zN`N=X46b{7yk5FZGr#5&n1!-@j@g02g|X>MOpF3#IjZ_4wg{dX+G9eqS+Es9@6nC7 zD9$NuVJI}6ZlwtUm5cCAiYv0(Yi{%eH+}t)!E^>^KxB5^L~a`4%1~5q6h>d;paC9c zTj0wTCKrhWf+F#5>EgX`sl%POl?oyCq0(w0xoL?L%)|Q7d|Hl92rUYAU#lc**I&^6p=4lNQPa0 znQ|A~i0ip@`B=FW-Q;zh?-wF;Wl5!+q3GXDu-x&}$gUO)NoO7^$BeEIrd~1Dh{Tr` z8s<(Bn@gZ(mkIGnmYh_ehXnq78QL$pNDi)|QcT*|GtS%nz1uKE+E{7jdEBp%h0}%r zD2|KmYGiPa4;md-t_m5YDz#c*oV_FqXd85d@eub?9N61QuYcb3CnVWpM(D-^|CmkL z(F}L&N7qhL2PCq)fRh}XO@U`Yn<?TNGR4L(mF7#4u29{i~@k;pLsgl({YW5`Mo+p=zZn3L*4{JU;++dG9 X@eDJUQo;Ye2mwlRs?y0|+_a0zY+Zo%Dkae}+MySoIppb75o?vUW_?)>@g{U2`ERQIXV zeY$JrWnMZ$QC<=ii4X|@0H8`si75jB(ElJb00HAB%>SlLR{!zO|C9P3zxw_U8?1d8uRZ=({Ga4shyN}3 zAK}WA(ds|``G4jA)9}Bt2Hy0+f3rV1E6b|@?hpGA=PI&r8)ah|)I2s(P5Ic*Ndhn^ z*T&j@gbCTv7+8rpYbR^Ty}1AY)YH;p!m948r#%7x^Z@_-w{pDl|1S4`EM3n_PaXvK z1JF)E3qy$qTj5Xs{jU9k=y%SQ0>8E$;x?p9ayU0bZZeo{5Z@&FKX>}s!0+^>C^D#z z>xsCPvxD3Z=dP}TTOSJhNTPyVt14VCQ9MQFN`rn!c&_p?&4<5_PGm4a;WS&1(!qKE z_H$;dDdiPQ!F_gsN`2>`X}$I=B;={R8%L~`>RyKcS$72ai$!2>d(YkciA^J0@X%G4 z4cu!%Ps~2JuJ8ex`&;Fa0NQOq_nDZ&X;^A=oc1&f#3P1(!5il>6?uK4QpEG8z0Rhu zvBJ+A9RV?z%v?!$=(vcH?*;vRs*+PPbOQ3cdPr5=tOcLqmfx@#hOqX0iN)wTTO21jH<>jpmwRIAGw7`a|sl?9y9zRBh>(_%| zF?h|P7}~RKj?HR+q|4U`CjRmV-$mLW>MScKnNXiv{vD3&2@*u)-6P@h0A`eeZ7}71 zK(w%@R<4lLt`O7fs1E)$5iGb~fPfJ?WxhY7c3Q>T-w#wT&zW522pH-B%r5v#5y^CF zcC30Se|`D2mY$hAlIULL%-PNXgbbpRHgn<&X3N9W!@BUk@9g*P5mz-YnZBb*-$zMM z7Qq}ic0mR8n{^L|=+diODdV}Q!gwr?y+2m=3HWwMq4z)DqYVg0J~^}-%7rMR@S1;9 z7GFj6K}i32X;3*$SmzB&HW{PJ55kT+EI#SsZf}bD7nW^Haf}_gXciYKX{QBxIPSx2Ma? zHQqgzZq!_{&zg{yxqv3xq8YV+`S}F6A>Gtl39_m;K4dA{pP$BW0oIXJ>jEQ!2V3A2 zdpoTxG&V=(?^q?ZTj2ZUpDUdMb)T?E$}CI>r@}PFPWD9@*%V6;4Ag>D#h>!s)=$0R zRXvdkZ%|c}ubej`jl?cS$onl9Tw52rBKT)kgyw~Xy%z62Lr%V6Y=f?2)J|bZJ5(Wx zmji`O;_B+*X@qe-#~`HFP<{8$w@z4@&`q^Q-Zk8JG3>WalhnW1cvnoVw>*R@c&|o8 zZ%w!{Z+MHeZ*OE4v*otkZqz11*s!#s^Gq>+o`8Z5 z^i-qzJLJh9!W-;SmFkR8HEZJWiXk$40i6)7 zZpr=k2lp}SasbM*Nbn3j$sn0;rUI;%EDbi7T1ZI4qL6PNNM2Y%6{LMIKW+FY_yF3) zSKQ2QSujzNMSL2r&bYs`|i2Dnn z=>}c0>a}>|uT!IiMOA~pVT~R@bGlm}Edf}Kq0?*Af6#mW9f9!}RjW7om0c9Qlp;yK z)=XQs(|6GCadQbWIhYF=rf{Y)sj%^Id-ARO0=O^Ad;Ph+ z0?$eE1xhH?{T$QI>0JP75`r)U_$#%K1^BQ8z#uciKf(C701&RyLQWBUp*Q7eyn76} z6JHpC9}R$J#(R0cDCkXoFSp;j6{x{b&0yE@P7{;pCEpKjS(+1RQy38`=&Yxo%F=3y zCPeefABp34U-s?WmU#JJw23dcC{sPPFc2#J$ZgEN%zod}J~8dLm*fx9f6SpO zn^Ww3bt9-r0XaT2a@Wpw;C23XM}7_14#%QpubrIw5aZtP+CqIFmsG4`Cm6rfxl9n5 z7=r2C-+lM2AB9X0T_`?EW&Byv&K?HS4QLoylJ|OAF z`8atBNTzJ&AQ!>sOo$?^0xj~D(;kS$`9zbEGd>f6r`NC3X`tX)sWgWUUOQ7w=$TO&*j;=u%25ay-%>3@81tGe^_z*C7pb9y*Ed^H3t$BIKH2o+olp#$q;)_ zfpjCb_^VFg5fU~K)nf*d*r@BCC>UZ!0&b?AGk_jTPXaSnCuW110wjHPPe^9R^;jo3 zwvzTl)C`Zl5}O2}3lec=hZ*$JnkW#7enKKc)(pM${_$9Hc=Sr_A9Biwe*Y=T?~1CK z6eZ9uPICjy-sMGbZl$yQmpB&`ouS8v{58__t0$JP%i3R&%QR3ianbZqDs<2#5FdN@n5bCn^ZtH992~5k(eA|8|@G9u`wdn7bnpg|@{m z^d6Y`*$Zf2Xr&|g%sai#5}Syvv(>Jnx&EM7-|Jr7!M~zdAyjt*xl;OLhvW-a%H1m0 z*x5*nb=R5u><7lyVpNAR?q@1U59 zO+)QWwL8t zyip?u_nI+K$uh{y)~}qj?(w0&=SE^8`_WMM zTybjG=999h38Yes7}-4*LJ7H)UE8{mE(6;8voE+TYY%33A>S6`G_95^5QHNTo_;Ao ztIQIZ_}49%{8|=O;isBZ?=7kfdF8_@azfoTd+hEJKWE!)$)N%HIe2cplaK`ry#=pV z0q{9w-`i0h@!R8K3GC{ivt{70IWG`EP|(1g7i_Q<>aEAT{5(yD z=!O?kq61VegV+st@XCw475j6vS)_z@efuqQgHQR1T4;|-#OLZNQJPV4k$AX1Uk8Lm z{N*b*ia=I+MB}kWpupJ~>!C@xEN#Wa7V+7{m4j8c?)ChV=D?o~sjT?0C_AQ7B-vxqX30s0I_`2$in86#`mAsT-w?j{&AL@B3$;P z31G4(lV|b}uSDCIrjk+M1R!X7s4Aabn<)zpgT}#gE|mIvV38^ODy@<&yflpCwS#fRf9ZX3lPV_?8@C5)A;T zqmouFLFk;qIs4rA=hh=GL~sCFsXHsqO6_y~*AFt939UYVBSx1s(=Kb&5;j7cSowdE;7()CC2|-i9Zz+_BIw8#ll~-tyH?F3{%`QCsYa*b#s*9iCc`1P1oC26?`g<9))EJ3%xz+O!B3 zZ7$j~To)C@PquR>a1+Dh>-a%IvH_Y7^ys|4o?E%3`I&ADXfC8++hAdZfzIT#%C+Jz z1lU~K_vAm0m8Qk}K$F>|>RPK%<1SI0(G+8q~H zAsjezyP+u!Se4q3GW)`h`NPSRlMoBjCzNPesWJwVTY!o@G8=(6I%4XHGaSiS3MEBK zhgGFv6Jc>L$4jVE!I?TQuwvz_%CyO!bLh94nqK11C2W$*aa2ueGopG8DnBICVUORP zgytv#)49fVXDaR$SukloYC3u7#5H)}1K21=?DKj^U)8G;MS)&Op)g^zR2($<>C*zW z;X7`hLxiIO#J`ANdyAOJle4V%ppa*(+0i3w;8i*BA_;u8gOO6)MY`ueq7stBMJTB; z-a0R>hT*}>z|Gg}@^zDL1MrH+2hsR8 zHc}*9IvuQC^Ju)^#Y{fOr(96rQNPNhxc;mH@W*m206>Lo<*SaaH?~8zg&f&%YiOEG zGiz?*CP>Bci}!WiS=zj#K5I}>DtpregpP_tfZtPa(N<%vo^#WCQ5BTv0vr%Z{)0q+ z)RbfHktUm|lg&U3YM%lMUM(fu}i#kjX9h>GYctkx9Mt_8{@s%!K_EI zScgwy6%_fR?CGJQtmgNAj^h9B#zmaMDWgH55pGuY1Gv7D z;8Psm(vEPiwn#MgJYu4Ty9D|h!?Rj0ddE|&L3S{IP%H4^N!m`60ZwZw^;eg4sk6K{ ziA^`Sbl_4~f&Oo%n;8Ye(tiAdlZKI!Z=|j$5hS|D$bDJ}p{gh$KN&JZYLUjv4h{NY zBJ>X9z!xfDGY z+oh_Z&_e#Q(-}>ssZfm=j$D&4W4FNy&-kAO1~#3Im;F)Nwe{(*75(p=P^VI?X0GFakfh+X-px4a%Uw@fSbmp9hM1_~R>?Z8+ ziy|e9>8V*`OP}4x5JjdWp}7eX;lVxp5qS}0YZek;SNmm7tEeSF*-dI)6U-A%m6YvCgM(}_=k#a6o^%-K4{`B1+}O4x zztDT%hVb;v#?j`lTvlFQ3aV#zkX=7;YFLS$uIzb0E3lozs5`Xy zi~vF+%{z9uLjKvKPhP%x5f~7-Gj+%5N`%^=yk*Qn{`> z;xj&ROY6g`iy2a@{O)V(jk&8#hHACVDXey5a+KDod_Z&}kHM}xt7}Md@pil{2x7E~ zL$k^d2@Ec2XskjrN+IILw;#7((abu;OJii&v3?60x>d_Ma(onIPtcVnX@ELF0aL?T zSmWiL3(dOFkt!x=1O!_0n(cAzZW+3nHJ{2S>tgSK?~cFha^y(l@-Mr2W$%MN{#af8J;V*>hdq!gx=d0h$T7l}>91Wh07)9CTX zh2_ZdQCyFOQ)l(}gft0UZG`Sh2`x-w`5vC2UD}lZs*5 zG76$akzn}Xi))L3oGJ75#pcN=cX3!=57$Ha=hQ2^lwdyU#a}4JJOz6ddR%zae%#4& za)bFj)z=YQela(F#Y|Q#dp}PJghITwXouVaMq$BM?K%cXn9^Y@g43$=O)F&ZlOUom zJiad#dea;-eywBA@e&D6Pdso1?2^(pXiN91?jvcaUyYoKUmvl5G9e$W!okWe*@a<^ z8cQQ6cNSf+UPDx%?_G4aIiybZHHagF{;IcD(dPO!#=u zWfqLcPc^+7Uu#l(Bpxft{*4lv#*u7X9AOzDO z1D9?^jIo}?%iz(_dwLa{ex#T}76ZfN_Z-hwpus9y+4xaUu9cX}&P{XrZVWE{1^0yw zO;YhLEW!pJcbCt3L8~a7>jsaN{V3>tz6_7`&pi%GxZ=V3?3K^U+*ryLSb)8^IblJ0 zSRLNDvIxt)S}g30?s_3NX>F?NKIGrG_zB9@Z>uSW3k2es_H2kU;Rnn%j5qP)!XHKE zPB2mHP~tLCg4K_vH$xv`HbRsJwbZMUV(t=ez;Ec(vyHH)FbfLg`c61I$W_uBB>i^r z&{_P;369-&>23R%qNIULe=1~T$(DA`ev*EWZ6j(B$(te}x1WvmIll21zvygkS%vwG zzkR6Z#RKA2!z!C%M!O>!=Gr0(J0FP=-MN=5t-Ir)of50y10W}j`GtRCsXBakrKtG& zazmITDJMA0C51&BnLY)SY9r)NVTMs);1<=oosS9g31l{4ztjD3#+2H7u_|66b|_*O z;Qk6nalpqdHOjx|K&vUS_6ITgGll;TdaN*ta=M_YtyC)I9Tmr~VaPrH2qb6sd~=AcIxV+%z{E&0@y=DPArw zdV7z(G1hBx7hd{>(cr43^WF%4Y@PXZ?wPpj{OQ#tvc$pABJbvPGvdR`cAtHn)cSEV zrpu}1tJwQ3y!mSmH*uz*x0o|CS<^w%&KJzsj~DU0cLQUxk5B!hWE>aBkjJle8z~;s z-!A=($+}Jq_BTK5^B!`R>!MulZN)F=iXXeUd0w5lUsE5VP*H*oCy(;?S$p*TVvTxwAeWFB$jHyb0593)$zqalVlDX=GcCN1gU0 zlgU)I$LcXZ8Oyc2TZYTPu@-;7<4YYB-``Qa;IDcvydIA$%kHhJKV^m*-zxcvU4viy&Kr5GVM{IT>WRywKQ9;>SEiQD*NqplK-KK4YR`p0@JW)n_{TU3bt0 zim%;(m1=#v2}zTps=?fU5w^(*y)xT%1vtQH&}50ZF!9YxW=&7*W($2kgKyz1mUgfs zfV<*XVVIFnohW=|j+@Kfo!#liQR^x>2yQdrG;2o8WZR+XzU_nG=Ed2rK?ntA;K5B{ z>M8+*A4!Jm^Bg}aW?R?6;@QG@uQ8&oJ{hFixcfEnJ4QH?A4>P=q29oDGW;L;= z9-a0;g%c`C+Ai!UmK$NC*4#;Jp<1=TioL=t^YM)<<%u#hnnfSS`nq63QKGO1L8RzX z@MFDqs1z ztYmxDl@LU)5acvHk)~Z`RW7=aJ_nGD!mOSYD>5Odjn@TK#LY{jf?+piB5AM-CAoT_ z?S-*q7}wyLJzK>N%eMPuFgN)Q_otKP;aqy=D5f!7<=n(lNkYRXVpkB{TAYLYg{|(jtRqYmg$xH zjmq?B(RE4 zQx^~Pt}gxC2~l=K$$-sYy_r$CO(d=+b3H1MB*y_5g6WLaWTXn+TKQ|hNY^>Mp6k*$ zwkovomhu776vQATqT4blf~g;TY(MWCrf^^yfWJvSAB$p5l;jm@o#=!lqw+Lqfq>X= z$6~kxfm7`3q4zUEB;u4qa#BdJxO!;xGm)wwuisj{0y2x{R(IGMrsIzDY9LW>m!Y`= z04sx3IjnYvL<4JqxQ8f7qYd0s2Ig%`ytYPEMKI)s(LD}D@EY>x`VFtqvnADNBdeao zC96X+MxnwKmjpg{U&gP3HE}1=s!lv&D{6(g_lzyF3A`7Jn*&d_kL<;dAFx!UZ>hB8 z5A*%LsAn;VLp>3${0>M?PSQ)9s3}|h2e?TG4_F{}{Cs>#3Q*t$(CUc}M)I}8cPF6% z=+h(Kh^8)}gj(0}#e7O^FQ6`~fd1#8#!}LMuo3A0bN`o}PYsm!Y}sdOz$+Tegc=qT z8x`PH$7lvnhJp{kHWb22l;@7B7|4yL4UOOVM0MP_>P%S1Lnid)+k9{+3D+JFa#Pyf zhVc#&df87APl4W9X)F3pGS>@etfl=_E5tBcVoOfrD4hmVeTY-cj((pkn%n@EgN{0f zwb_^Rk0I#iZuHK!l*lN`ceJn(sI{$Fq6nN& zE<-=0_2WN}m+*ivmIOxB@#~Q-cZ>l136w{#TIJe478`KE7@=a{>SzPHsKLzYAyBQO zAtuuF$-JSDy_S@6GW0MOE~R)b;+0f%_NMrW(+V#c_d&U8Z9+ec4=HmOHw?gdjF(Lu zzra83M_BoO-1b3;9`%&DHfuUY)6YDV21P$C!Rc?mv&{lx#f8oc6?0?x zK08{WP65?#>(vPfA-c=MCY|%*1_<3D4NX zeVTi-JGl2uP_2@0F{G({pxQOXt_d{g_CV6b?jNpfUG9;8yle-^4KHRvZs-_2siata zt+d_T@U$&t*xaD22(fH(W1r$Mo?3dc%Tncm=C6{V9y{v&VT#^1L04vDrLM9qBoZ4@ z6DBN#m57hX7$C(=#$Y5$bJmwA$T8jKD8+6A!-IJwA{WOfs%s}yxUw^?MRZjF$n_KN z6`_bGXcmE#5e4Ym)aQJ)xg3Pg0@k`iGuHe?f(5LtuzSq=nS^5z>vqU0EuZ&75V%Z{ zYyhRLN^)$c6Ds{f7*FBpE;n5iglx5PkHfWrj3`x^j^t z7ntuV`g!9Xg#^3!x)l*}IW=(Tz3>Y5l4uGaB&lz{GDjm2D5S$CExLT`I1#n^lBH7Y zDgpMag@`iETKAI=p<5E#LTkwzVR@=yY|uBVI1HG|8h+d;G-qfuj}-ZR6fN>EfCCW z9~wRQoAPEa#aO?3h?x{YvV*d+NtPkf&4V0k4|L=uj!U{L+oLa(z#&iuhJr3-PjO3R z5s?=nn_5^*^Rawr>>Nr@K(jwkB#JK-=+HqwfdO<+P5byeim)wvqGlP-P|~Nse8=XF zz`?RYB|D6SwS}C+YQv+;}k6$-%D(@+t14BL@vM z2q%q?f6D-A5s$_WY3{^G0F131bbh|g!}#BKw=HQ7mx;Dzg4Z*bTLQSfo{ed{4}NZW zfrRm^Ca$rlE{Ue~uYv>R9{3smwATcdM_6+yWIO z*ZRH~uXE@#p$XTbCt5j7j2=86e{9>HIB6xDzV+vAo&B?KUiMP|ttOElepnl%|DPqL b{|{}U^kRn2wo}j7|0ATu<;8xA7zX}7|B6mN literal 0 HcmV?d00001 diff --git a/examples/react/develop/fields/public/manifest.json b/examples/react/develop/fields/public/manifest.json new file mode 100644 index 00000000..080d6c77 --- /dev/null +++ b/examples/react/develop/fields/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/examples/react/develop/fields/public/robots.txt b/examples/react/develop/fields/public/robots.txt new file mode 100644 index 00000000..e9e57dc4 --- /dev/null +++ b/examples/react/develop/fields/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/examples/react/develop/fields/src/App.js b/examples/react/develop/fields/src/App.js new file mode 100644 index 00000000..5796839d --- /dev/null +++ b/examples/react/develop/fields/src/App.js @@ -0,0 +1,60 @@ +import React from 'react'; +import { createCollection, LogCodeManager, shared } from '@agile-ts/core'; +import reactIntegration, { useAgile, useValue } from '@agile-ts/react'; + +LogCodeManager.setAllowLogging(false); +shared.integrate(reactIntegration); + +const FIELDS = createCollection({ + initialData: Array.from(Array(5000).keys()).map((i) => ({ + id: i, + name: `Field #${i + 1}`, + })), +}); + +let renderFieldsCount = 0; + +function Field({ index }) { + const ITEM = FIELDS.getItem(index); + const item = useAgile(ITEM); + + console.log(`Rerender Fields at '${index}':`, ++renderFieldsCount); + + return ( +
+ Last {``} render at: {new Date().toISOString()} +   + { + ITEM?.patch({ name: e.target.value }); + }} + /> +
+ ); +} + +export default function App() { + const fieldKeys = useValue(FIELDS); + + return ( +
+
+ Last {``} render at: {new Date().toISOString()} +
+
+ {fieldKeys.map((key, i) => ( + + ))} + +
+ ); +} diff --git a/examples/react/develop/fields/src/index.js b/examples/react/develop/fields/src/index.js new file mode 100644 index 00000000..c1f31c5f --- /dev/null +++ b/examples/react/develop/fields/src/index.js @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import App from './App'; + +ReactDOM.render( + + + , + document.getElementById('root') +); From e0d33b433b41cdcfc1834f23d44f847228990126 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Thu, 9 Sep 2021 11:37:10 +0200 Subject: [PATCH 83/93] fixed typos --- .../benchmarks/react/1000fields/index.ts | 6 +- examples/react/develop/simple-todo-list/.env | 1 + .../react/develop/simple-todo-list/.gitignore | 23 ++++++ .../react/develop/simple-todo-list/README.md | 70 ++++++++++++++++++ .../develop/simple-todo-list/package.json | 39 ++++++++++ .../simple-todo-list/public/favicon.ico | Bin 0 -> 3870 bytes .../simple-todo-list/public/index.html | 43 +++++++++++ .../simple-todo-list/public/logo192.png | Bin 0 -> 5347 bytes .../simple-todo-list/public/logo512.png | Bin 0 -> 9664 bytes .../simple-todo-list/public/manifest.json | 25 +++++++ .../simple-todo-list/public/robots.txt | 3 + .../react/develop/simple-todo-list/src/App.js | 53 +++++++++++++ .../develop/simple-todo-list/src/core.js | 9 +++ .../develop/simple-todo-list/src/index.js | 10 +++ .../src/collection/collection.persistent.ts | 19 ++--- packages/core/src/collection/collection.ts | 49 ++++-------- packages/core/src/collection/group/index.ts | 11 ++- packages/core/src/state/state.enhanced.ts | 2 +- packages/core/src/state/state.persistent.ts | 4 +- packages/core/src/storages/persistent.ts | 6 +- .../collection/collection.persistent.test.ts | 20 ++--- .../tests/unit/state/state.persistent.test.ts | 6 +- .../tests/unit/storages/persistent.test.ts | 2 +- 23 files changed, 330 insertions(+), 71 deletions(-) create mode 100644 examples/react/develop/simple-todo-list/.env create mode 100644 examples/react/develop/simple-todo-list/.gitignore create mode 100644 examples/react/develop/simple-todo-list/README.md create mode 100644 examples/react/develop/simple-todo-list/package.json create mode 100644 examples/react/develop/simple-todo-list/public/favicon.ico create mode 100644 examples/react/develop/simple-todo-list/public/index.html create mode 100644 examples/react/develop/simple-todo-list/public/logo192.png create mode 100644 examples/react/develop/simple-todo-list/public/logo512.png create mode 100644 examples/react/develop/simple-todo-list/public/manifest.json create mode 100644 examples/react/develop/simple-todo-list/public/robots.txt create mode 100644 examples/react/develop/simple-todo-list/src/App.js create mode 100644 examples/react/develop/simple-todo-list/src/core.js create mode 100644 examples/react/develop/simple-todo-list/src/index.js diff --git a/benchmark/benchmarks/react/1000fields/index.ts b/benchmark/benchmarks/react/1000fields/index.ts index 9b318872..1c2b070e 100644 --- a/benchmark/benchmarks/react/1000fields/index.ts +++ b/benchmark/benchmarks/react/1000fields/index.ts @@ -78,9 +78,9 @@ const results: CycleResultInterface[] = []; // Add Tests to the Benchmark Test Suite suite .add('Agile Collection', configTest(agileCollection)) - .add('Agile State', configTest(agileState)) - .add('Agile nested State', configTest(agileNestedState)) - // .add('Pulse Collection', configTest(pulseCollection)) + // .add('Agile State', configTest(agileState)) + // .add('Agile nested State', configTest(agileNestedState)) + .add('Pulse Collection', configTest(pulseCollection)) // .add('Pulse State', configTest(pulseState)) // .add('Pulse nested State', configTest(pulseNestedState)) // .add('Hookstate', configTest(hookstate)) diff --git a/examples/react/develop/simple-todo-list/.env b/examples/react/develop/simple-todo-list/.env new file mode 100644 index 00000000..6f809cc2 --- /dev/null +++ b/examples/react/develop/simple-todo-list/.env @@ -0,0 +1 @@ +SKIP_PREFLIGHT_CHECK=true diff --git a/examples/react/develop/simple-todo-list/.gitignore b/examples/react/develop/simple-todo-list/.gitignore new file mode 100644 index 00000000..4d29575d --- /dev/null +++ b/examples/react/develop/simple-todo-list/.gitignore @@ -0,0 +1,23 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/examples/react/develop/simple-todo-list/README.md b/examples/react/develop/simple-todo-list/README.md new file mode 100644 index 00000000..02aac3f6 --- /dev/null +++ b/examples/react/develop/simple-todo-list/README.md @@ -0,0 +1,70 @@ +# Getting Started with Create React App + +This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). + +## Available Scripts + +In the project directory, you can run: + +### `yarn start` + +Runs the app in the development mode.\ +Open [http://localhost:3000](http://localhost:3000) to view it in the browser. + +The page will reload if you make edits.\ +You will also see any lint errors in the console. + +### `yarn test` + +Launches the test runner in the interactive watch mode.\ +See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. + +### `yarn build` + +Builds the app for production to the `build` folder.\ +It correctly bundles React in production mode and optimizes the build for the best performance. + +The build is minified and the filenames include the hashes.\ +Your app is ready to be deployed! + +See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. + +### `yarn eject` + +**Note: this is a one-way operation. Once you `eject`, you can’t go back!** + +If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. + +Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. + +You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. + +## Learn More + +You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). + +To learn React, check out the [React documentation](https://reactjs.org/). + +### Code Splitting + +This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) + +### Analyzing the Bundle Size + +This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) + +### Making a Progressive Web App + +This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) + +### Advanced Configuration + +This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) + +### Deployment + +This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) + +### `yarn build` fails to minify + +This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) diff --git a/examples/react/develop/simple-todo-list/package.json b/examples/react/develop/simple-todo-list/package.json new file mode 100644 index 00000000..c034b229 --- /dev/null +++ b/examples/react/develop/simple-todo-list/package.json @@ -0,0 +1,39 @@ +{ + "name": "simple-todo-list", + "version": "0.1.0", + "private": true, + "dependencies": { + "@agile-ts/core": "file:.yalc/@agile-ts/core", + "@agile-ts/logger": "file:.yalc/@agile-ts/logger", + "@agile-ts/react": "file:.yalc/@agile-ts/react", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-scripts": "4.0.3" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject", + "install:dev:agile": "yalc add @agile-ts/core @agile-ts/react @agile-ts/logger & yarn install", + "install:prod:agile": "yarn add @agile-ts/core @agile-ts/react @agile-ts/logger & yarn install" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/examples/react/develop/simple-todo-list/public/favicon.ico b/examples/react/develop/simple-todo-list/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a11777cc471a4344702741ab1c8a588998b1311a GIT binary patch literal 3870 zcma);c{J4h9>;%nil|2-o+rCuEF-(I%-F}ijC~o(k~HKAkr0)!FCj~d>`RtpD?8b; zXOC1OD!V*IsqUwzbMF1)-gEDD=A573Z-&G7^LoAC9|WO7Xc0Cx1g^Zu0u_SjAPB
3vGa^W|sj)80f#V0@M_CAZTIO(t--xg= z!sii`1giyH7EKL_+Wi0ab<)&E_0KD!3Rp2^HNB*K2@PHCs4PWSA32*-^7d{9nH2_E zmC{C*N*)(vEF1_aMamw2A{ZH5aIDqiabnFdJ|y0%aS|64E$`s2ccV~3lR!u<){eS` z#^Mx6o(iP1Ix%4dv`t@!&Za-K@mTm#vadc{0aWDV*_%EiGK7qMC_(`exc>-$Gb9~W!w_^{*pYRm~G zBN{nA;cm^w$VWg1O^^<6vY`1XCD|s_zv*g*5&V#wv&s#h$xlUilPe4U@I&UXZbL z0)%9Uj&@yd03n;!7do+bfixH^FeZ-Ema}s;DQX2gY+7g0s(9;`8GyvPY1*vxiF&|w z>!vA~GA<~JUqH}d;DfBSi^IT*#lrzXl$fNpq0_T1tA+`A$1?(gLb?e#0>UELvljtQ zK+*74m0jn&)5yk8mLBv;=@}c{t0ztT<v;Avck$S6D`Z)^c0(jiwKhQsn|LDRY&w(Fmi91I7H6S;b0XM{e zXp0~(T@k_r-!jkLwd1_Vre^v$G4|kh4}=Gi?$AaJ)3I+^m|Zyj#*?Kp@w(lQdJZf4 z#|IJW5z+S^e9@(6hW6N~{pj8|NO*>1)E=%?nNUAkmv~OY&ZV;m-%?pQ_11)hAr0oAwILrlsGawpxx4D43J&K=n+p3WLnlDsQ$b(9+4 z?mO^hmV^F8MV{4Lx>(Q=aHhQ1){0d*(e&s%G=i5rq3;t{JC zmgbn5Nkl)t@fPH$v;af26lyhH!k+#}_&aBK4baYPbZy$5aFx4}ka&qxl z$=Rh$W;U)>-=S-0=?7FH9dUAd2(q#4TCAHky!$^~;Dz^j|8_wuKc*YzfdAht@Q&ror?91Dm!N03=4=O!a)I*0q~p0g$Fm$pmr$ zb;wD;STDIi$@M%y1>p&_>%?UP($15gou_ue1u0!4(%81;qcIW8NyxFEvXpiJ|H4wz z*mFT(qVx1FKufG11hByuX%lPk4t#WZ{>8ka2efjY`~;AL6vWyQKpJun2nRiZYDij$ zP>4jQXPaP$UC$yIVgGa)jDV;F0l^n(V=HMRB5)20V7&r$jmk{UUIe zVjKroK}JAbD>B`2cwNQ&GDLx8{pg`7hbA~grk|W6LgiZ`8y`{Iq0i>t!3p2}MS6S+ zO_ruKyAElt)rdS>CtF7j{&6rP-#c=7evGMt7B6`7HG|-(WL`bDUAjyn+k$mx$CH;q2Dz4x;cPP$hW=`pFfLO)!jaCL@V2+F)So3}vg|%O*^T1j>C2lx zsURO-zIJC$^$g2byVbRIo^w>UxK}74^TqUiRR#7s_X$e)$6iYG1(PcW7un-va-S&u zHk9-6Zn&>T==A)lM^D~bk{&rFzCi35>UR!ZjQkdSiNX*-;l4z9j*7|q`TBl~Au`5& z+c)*8?#-tgUR$Zd%Q3bs96w6k7q@#tUn`5rj+r@_sAVVLqco|6O{ILX&U-&-cbVa3 zY?ngHR@%l{;`ri%H*0EhBWrGjv!LE4db?HEWb5mu*t@{kv|XwK8?npOshmzf=vZA@ zVSN9sL~!sn?r(AK)Q7Jk2(|M67Uy3I{eRy z_l&Y@A>;vjkWN5I2xvFFTLX0i+`{qz7C_@bo`ZUzDugfq4+>a3?1v%)O+YTd6@Ul7 zAfLfm=nhZ`)P~&v90$&UcF+yXm9sq!qCx3^9gzIcO|Y(js^Fj)Rvq>nQAHI92ap=P z10A4@prk+AGWCb`2)dQYFuR$|H6iDE8p}9a?#nV2}LBCoCf(Xi2@szia7#gY>b|l!-U`c}@ zLdhvQjc!BdLJvYvzzzngnw51yRYCqh4}$oRCy-z|v3Hc*d|?^Wj=l~18*E~*cR_kU z{XsxM1i{V*4GujHQ3DBpl2w4FgFR48Nma@HPgnyKoIEY-MqmMeY=I<%oG~l!f<+FN z1ZY^;10j4M4#HYXP zw5eJpA_y(>uLQ~OucgxDLuf}fVs272FaMxhn4xnDGIyLXnw>Xsd^J8XhcWIwIoQ9} z%FoSJTAGW(SRGwJwb=@pY7r$uQRK3Zd~XbxU)ts!4XsJrCycrWSI?e!IqwqIR8+Jh zlRjZ`UO1I!BtJR_2~7AbkbSm%XQqxEPkz6BTGWx8e}nQ=w7bZ|eVP4?*Tb!$(R)iC z9)&%bS*u(lXqzitAN)Oo=&Ytn>%Hzjc<5liuPi>zC_nw;Z0AE3Y$Jao_Q90R-gl~5 z_xAb2J%eArrC1CN4G$}-zVvCqF1;H;abAu6G*+PDHSYFx@Tdbfox*uEd3}BUyYY-l zTfEsOqsi#f9^FoLO;ChK<554qkri&Av~SIM*{fEYRE?vH7pTAOmu2pz3X?Wn*!ROX ztd54huAk&mFBemMooL33RV-*1f0Q3_(7hl$<#*|WF9P!;r;4_+X~k~uKEqdzZ$5Al zV63XN@)j$FN#cCD;ek1R#l zv%pGrhB~KWgoCj%GT?%{@@o(AJGt*PG#l3i>lhmb_twKH^EYvacVY-6bsCl5*^~L0 zonm@lk2UvvTKr2RS%}T>^~EYqdL1q4nD%0n&Xqr^cK^`J5W;lRRB^R-O8b&HENO||mo0xaD+S=I8RTlIfVgqN@SXDr2&-)we--K7w= zJVU8?Z+7k9dy;s;^gDkQa`0nz6N{T?(A&Iz)2!DEecLyRa&FI!id#5Z7B*O2=PsR0 zEvc|8{NS^)!d)MDX(97Xw}m&kEO@5jqRaDZ!+%`wYOI<23q|&js`&o4xvjP7D_xv@ z5hEwpsp{HezI9!~6O{~)lLR@oF7?J7i>1|5a~UuoN=q&6N}EJPV_GD`&M*v8Y`^2j zKII*d_@Fi$+i*YEW+Hbzn{iQk~yP z>7N{S4)r*!NwQ`(qcN#8SRQsNK6>{)X12nbF`*7#ecO7I)Q$uZsV+xS4E7aUn+U(K baj7?x%VD!5Cxk2YbYLNVeiXvvpMCWYo=by@ literal 0 HcmV?d00001 diff --git a/examples/react/develop/simple-todo-list/public/index.html b/examples/react/develop/simple-todo-list/public/index.html new file mode 100644 index 00000000..aa069f27 --- /dev/null +++ b/examples/react/develop/simple-todo-list/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
+ + + diff --git a/examples/react/develop/simple-todo-list/public/logo192.png b/examples/react/develop/simple-todo-list/public/logo192.png new file mode 100644 index 0000000000000000000000000000000000000000..fc44b0a3796c0e0a64c3d858ca038bd4570465d9 GIT binary patch literal 5347 zcmZWtbyO6NvR-oO24RV%BvuJ&=?+<7=`LvyB&A_#M7mSDYw1v6DJkiYl9XjT!%$dLEBTQ8R9|wd3008in6lFF3GV-6mLi?MoP_y~}QUnaDCHI#t z7w^m$@6DI)|C8_jrT?q=f8D?0AM?L)Z}xAo^e^W>t$*Y0KlT5=@bBjT9kxb%-KNdk zeOS1tKO#ChhG7%{ApNBzE2ZVNcxbrin#E1TiAw#BlUhXllzhN$qWez5l;h+t^q#Eav8PhR2|T}y5kkflaK`ba-eoE+Z2q@o6P$)=&` z+(8}+-McnNO>e#$Rr{32ngsZIAX>GH??tqgwUuUz6kjns|LjsB37zUEWd|(&O!)DY zQLrq%Y>)Y8G`yYbYCx&aVHi@-vZ3|ebG!f$sTQqMgi0hWRJ^Wc+Ibv!udh_r%2|U) zPi|E^PK?UE!>_4`f`1k4hqqj_$+d!EB_#IYt;f9)fBOumGNyglU(ofY`yHq4Y?B%- zp&G!MRY<~ajTgIHErMe(Z8JG*;D-PJhd@RX@QatggM7+G(Lz8eZ;73)72Hfx5KDOE zkT(m}i2;@X2AT5fW?qVp?@WgN$aT+f_6eo?IsLh;jscNRp|8H}Z9p_UBO^SJXpZew zEK8fz|0Th%(Wr|KZBGTM4yxkA5CFdAj8=QSrT$fKW#tweUFqr0TZ9D~a5lF{)%-tTGMK^2tz(y2v$i%V8XAxIywrZCp=)83p(zIk6@S5AWl|Oa2hF`~~^W zI;KeOSkw1O#TiQ8;U7OPXjZM|KrnN}9arP)m0v$c|L)lF`j_rpG(zW1Qjv$=^|p*f z>)Na{D&>n`jOWMwB^TM}slgTEcjxTlUby89j1)|6ydRfWERn3|7Zd2&e7?!K&5G$x z`5U3uFtn4~SZq|LjFVrz$3iln-+ucY4q$BC{CSm7Xe5c1J<=%Oagztj{ifpaZk_bQ z9Sb-LaQMKp-qJA*bP6DzgE3`}*i1o3GKmo2pn@dj0;He}F=BgINo};6gQF8!n0ULZ zL>kC0nPSFzlcB7p41doao2F7%6IUTi_+!L`MM4o*#Y#0v~WiO8uSeAUNp=vA2KaR&=jNR2iVwG>7t%sG2x_~yXzY)7K& zk3p+O0AFZ1eu^T3s};B%6TpJ6h-Y%B^*zT&SN7C=N;g|#dGIVMSOru3iv^SvO>h4M=t-N1GSLLDqVTcgurco6)3&XpU!FP6Hlrmj}f$ zp95;b)>M~`kxuZF3r~a!rMf4|&1=uMG$;h^g=Kl;H&Np-(pFT9FF@++MMEx3RBsK?AU0fPk-#mdR)Wdkj)`>ZMl#^<80kM87VvsI3r_c@_vX=fdQ`_9-d(xiI z4K;1y1TiPj_RPh*SpDI7U~^QQ?%0&!$Sh#?x_@;ag)P}ZkAik{_WPB4rHyW#%>|Gs zdbhyt=qQPA7`?h2_8T;-E6HI#im9K>au*(j4;kzwMSLgo6u*}-K`$_Gzgu&XE)udQ zmQ72^eZd|vzI)~!20JV-v-T|<4@7ruqrj|o4=JJPlybwMg;M$Ud7>h6g()CT@wXm` zbq=A(t;RJ^{Xxi*Ff~!|3!-l_PS{AyNAU~t{h;(N(PXMEf^R(B+ZVX3 z8y0;0A8hJYp@g+c*`>eTA|3Tgv9U8#BDTO9@a@gVMDxr(fVaEqL1tl?md{v^j8aUv zm&%PX4^|rX|?E4^CkplWWNv*OKM>DxPa z!RJ)U^0-WJMi)Ksc!^ixOtw^egoAZZ2Cg;X7(5xZG7yL_;UJ#yp*ZD-;I^Z9qkP`} zwCTs0*%rIVF1sgLervtnUo&brwz?6?PXRuOCS*JI-WL6GKy7-~yi0giTEMmDs_-UX zo=+nFrW_EfTg>oY72_4Z0*uG>MnXP=c0VpT&*|rvv1iStW;*^={rP1y?Hv+6R6bxFMkxpWkJ>m7Ba{>zc_q zEefC3jsXdyS5??Mz7IET$Kft|EMNJIv7Ny8ZOcKnzf`K5Cd)&`-fTY#W&jnV0l2vt z?Gqhic}l}mCv1yUEy$%DP}4AN;36$=7aNI^*AzV(eYGeJ(Px-j<^gSDp5dBAv2#?; zcMXv#aj>%;MiG^q^$0MSg-(uTl!xm49dH!{X0){Ew7ThWV~Gtj7h%ZD zVN-R-^7Cf0VH!8O)uUHPL2mO2tmE*cecwQv_5CzWeh)ykX8r5Hi`ehYo)d{Jnh&3p z9ndXT$OW51#H5cFKa76c<%nNkP~FU93b5h-|Cb}ScHs@4Q#|}byWg;KDMJ#|l zE=MKD*F@HDBcX@~QJH%56eh~jfPO-uKm}~t7VkHxHT;)4sd+?Wc4* z>CyR*{w@4(gnYRdFq=^(#-ytb^5ESD?x<0Skhb%Pt?npNW1m+Nv`tr9+qN<3H1f<% zZvNEqyK5FgPsQ`QIu9P0x_}wJR~^CotL|n zk?dn;tLRw9jJTur4uWoX6iMm914f0AJfB@C74a;_qRrAP4E7l890P&{v<}>_&GLrW z)klculcg`?zJO~4;BBAa=POU%aN|pmZJn2{hA!d!*lwO%YSIzv8bTJ}=nhC^n}g(ld^rn#kq9Z3)z`k9lvV>y#!F4e{5c$tnr9M{V)0m(Z< z#88vX6-AW7T2UUwW`g<;8I$Jb!R%z@rCcGT)-2k7&x9kZZT66}Ztid~6t0jKb&9mm zpa}LCb`bz`{MzpZR#E*QuBiZXI#<`5qxx=&LMr-UUf~@dRk}YI2hbMsAMWOmDzYtm zjof16D=mc`^B$+_bCG$$@R0t;e?~UkF?7<(vkb70*EQB1rfUWXh$j)R2)+dNAH5%R zEBs^?N;UMdy}V};59Gu#0$q53$}|+q7CIGg_w_WlvE}AdqoS<7DY1LWS9?TrfmcvT zaypmplwn=P4;a8-%l^e?f`OpGb}%(_mFsL&GywhyN(-VROj`4~V~9bGv%UhcA|YW% zs{;nh@aDX11y^HOFXB$a7#Sr3cEtNd4eLm@Y#fc&j)TGvbbMwze zXtekX_wJqxe4NhuW$r}cNy|L{V=t#$%SuWEW)YZTH|!iT79k#?632OFse{+BT_gau zJwQcbH{b}dzKO?^dV&3nTILYlGw{27UJ72ZN){BILd_HV_s$WfI2DC<9LIHFmtyw? zQ;?MuK7g%Ym+4e^W#5}WDLpko%jPOC=aN)3!=8)s#Rnercak&b3ESRX3z{xfKBF8L z5%CGkFmGO@x?_mPGlpEej!3!AMddChabyf~nJNZxx!D&{@xEb!TDyvqSj%Y5@A{}9 zRzoBn0?x}=krh{ok3Nn%e)#~uh;6jpezhA)ySb^b#E>73e*frBFu6IZ^D7Ii&rsiU z%jzygxT-n*joJpY4o&8UXr2s%j^Q{?e-voloX`4DQyEK+DmrZh8A$)iWL#NO9+Y@!sO2f@rI!@jN@>HOA< z?q2l{^%mY*PNx2FoX+A7X3N}(RV$B`g&N=e0uvAvEN1W^{*W?zT1i#fxuw10%~))J zjx#gxoVlXREWZf4hRkgdHx5V_S*;p-y%JtGgQ4}lnA~MBz-AFdxUxU1RIT$`sal|X zPB6sEVRjGbXIP0U+?rT|y5+ev&OMX*5C$n2SBPZr`jqzrmpVrNciR0e*Wm?fK6DY& zl(XQZ60yWXV-|Ps!A{EF;=_z(YAF=T(-MkJXUoX zI{UMQDAV2}Ya?EisdEW;@pE6dt;j0fg5oT2dxCi{wqWJ<)|SR6fxX~5CzblPGr8cb zUBVJ2CQd~3L?7yfTpLNbt)He1D>*KXI^GK%<`bq^cUq$Q@uJifG>p3LU(!H=C)aEL zenk7pVg}0{dKU}&l)Y2Y2eFMdS(JS0}oZUuVaf2+K*YFNGHB`^YGcIpnBlMhO7d4@vV zv(@N}(k#REdul8~fP+^F@ky*wt@~&|(&&meNO>rKDEnB{ykAZ}k>e@lad7to>Ao$B zz<1(L=#J*u4_LB=8w+*{KFK^u00NAmeNN7pr+Pf+N*Zl^dO{LM-hMHyP6N!~`24jd zXYP|Ze;dRXKdF2iJG$U{k=S86l@pytLx}$JFFs8e)*Vi?aVBtGJ3JZUj!~c{(rw5>vuRF$`^p!P8w1B=O!skwkO5yd4_XuG^QVF z`-r5K7(IPSiKQ2|U9+`@Js!g6sfJwAHVd|s?|mnC*q zp|B|z)(8+mxXyxQ{8Pg3F4|tdpgZZSoU4P&9I8)nHo1@)9_9u&NcT^FI)6|hsAZFk zZ+arl&@*>RXBf-OZxhZerOr&dN5LW9@gV=oGFbK*J+m#R-|e6(Loz(;g@T^*oO)0R zN`N=X46b{7yk5FZGr#5&n1!-@j@g02g|X>MOpF3#IjZ_4wg{dX+G9eqS+Es9@6nC7 zD9$NuVJI}6ZlwtUm5cCAiYv0(Yi{%eH+}t)!E^>^KxB5^L~a`4%1~5q6h>d;paC9c zTj0wTCKrhWf+F#5>EgX`sl%POl?oyCq0(w0xoL?L%)|Q7d|Hl92rUYAU#lc**I&^6p=4lNQPa0 znQ|A~i0ip@`B=FW-Q;zh?-wF;Wl5!+q3GXDu-x&}$gUO)NoO7^$BeEIrd~1Dh{Tr` z8s<(Bn@gZ(mkIGnmYh_ehXnq78QL$pNDi)|QcT*|GtS%nz1uKE+E{7jdEBp%h0}%r zD2|KmYGiPa4;md-t_m5YDz#c*oV_FqXd85d@eub?9N61QuYcb3CnVWpM(D-^|CmkL z(F}L&N7qhL2PCq)fRh}XO@U`Yn<?TNGR4L(mF7#4u29{i~@k;pLsgl({YW5`Mo+p=zZn3L*4{JU;++dG9 X@eDJUQo;Ye2mwlRs?y0|+_a0zY+Zo%Dkae}+MySoIppb75o?vUW_?)>@g{U2`ERQIXV zeY$JrWnMZ$QC<=ii4X|@0H8`si75jB(ElJb00HAB%>SlLR{!zO|C9P3zxw_U8?1d8uRZ=({Ga4shyN}3 zAK}WA(ds|``G4jA)9}Bt2Hy0+f3rV1E6b|@?hpGA=PI&r8)ah|)I2s(P5Ic*Ndhn^ z*T&j@gbCTv7+8rpYbR^Ty}1AY)YH;p!m948r#%7x^Z@_-w{pDl|1S4`EM3n_PaXvK z1JF)E3qy$qTj5Xs{jU9k=y%SQ0>8E$;x?p9ayU0bZZeo{5Z@&FKX>}s!0+^>C^D#z z>xsCPvxD3Z=dP}TTOSJhNTPyVt14VCQ9MQFN`rn!c&_p?&4<5_PGm4a;WS&1(!qKE z_H$;dDdiPQ!F_gsN`2>`X}$I=B;={R8%L~`>RyKcS$72ai$!2>d(YkciA^J0@X%G4 z4cu!%Ps~2JuJ8ex`&;Fa0NQOq_nDZ&X;^A=oc1&f#3P1(!5il>6?uK4QpEG8z0Rhu zvBJ+A9RV?z%v?!$=(vcH?*;vRs*+PPbOQ3cdPr5=tOcLqmfx@#hOqX0iN)wTTO21jH<>jpmwRIAGw7`a|sl?9y9zRBh>(_%| zF?h|P7}~RKj?HR+q|4U`CjRmV-$mLW>MScKnNXiv{vD3&2@*u)-6P@h0A`eeZ7}71 zK(w%@R<4lLt`O7fs1E)$5iGb~fPfJ?WxhY7c3Q>T-w#wT&zW522pH-B%r5v#5y^CF zcC30Se|`D2mY$hAlIULL%-PNXgbbpRHgn<&X3N9W!@BUk@9g*P5mz-YnZBb*-$zMM z7Qq}ic0mR8n{^L|=+diODdV}Q!gwr?y+2m=3HWwMq4z)DqYVg0J~^}-%7rMR@S1;9 z7GFj6K}i32X;3*$SmzB&HW{PJ55kT+EI#SsZf}bD7nW^Haf}_gXciYKX{QBxIPSx2Ma? zHQqgzZq!_{&zg{yxqv3xq8YV+`S}F6A>Gtl39_m;K4dA{pP$BW0oIXJ>jEQ!2V3A2 zdpoTxG&V=(?^q?ZTj2ZUpDUdMb)T?E$}CI>r@}PFPWD9@*%V6;4Ag>D#h>!s)=$0R zRXvdkZ%|c}ubej`jl?cS$onl9Tw52rBKT)kgyw~Xy%z62Lr%V6Y=f?2)J|bZJ5(Wx zmji`O;_B+*X@qe-#~`HFP<{8$w@z4@&`q^Q-Zk8JG3>WalhnW1cvnoVw>*R@c&|o8 zZ%w!{Z+MHeZ*OE4v*otkZqz11*s!#s^Gq>+o`8Z5 z^i-qzJLJh9!W-;SmFkR8HEZJWiXk$40i6)7 zZpr=k2lp}SasbM*Nbn3j$sn0;rUI;%EDbi7T1ZI4qL6PNNM2Y%6{LMIKW+FY_yF3) zSKQ2QSujzNMSL2r&bYs`|i2Dnn z=>}c0>a}>|uT!IiMOA~pVT~R@bGlm}Edf}Kq0?*Af6#mW9f9!}RjW7om0c9Qlp;yK z)=XQs(|6GCadQbWIhYF=rf{Y)sj%^Id-ARO0=O^Ad;Ph+ z0?$eE1xhH?{T$QI>0JP75`r)U_$#%K1^BQ8z#uciKf(C701&RyLQWBUp*Q7eyn76} z6JHpC9}R$J#(R0cDCkXoFSp;j6{x{b&0yE@P7{;pCEpKjS(+1RQy38`=&Yxo%F=3y zCPeefABp34U-s?WmU#JJw23dcC{sPPFc2#J$ZgEN%zod}J~8dLm*fx9f6SpO zn^Ww3bt9-r0XaT2a@Wpw;C23XM}7_14#%QpubrIw5aZtP+CqIFmsG4`Cm6rfxl9n5 z7=r2C-+lM2AB9X0T_`?EW&Byv&K?HS4QLoylJ|OAF z`8atBNTzJ&AQ!>sOo$?^0xj~D(;kS$`9zbEGd>f6r`NC3X`tX)sWgWUUOQ7w=$TO&*j;=u%25ay-%>3@81tGe^_z*C7pb9y*Ed^H3t$BIKH2o+olp#$q;)_ zfpjCb_^VFg5fU~K)nf*d*r@BCC>UZ!0&b?AGk_jTPXaSnCuW110wjHPPe^9R^;jo3 zwvzTl)C`Zl5}O2}3lec=hZ*$JnkW#7enKKc)(pM${_$9Hc=Sr_A9Biwe*Y=T?~1CK z6eZ9uPICjy-sMGbZl$yQmpB&`ouS8v{58__t0$JP%i3R&%QR3ianbZqDs<2#5FdN@n5bCn^ZtH992~5k(eA|8|@G9u`wdn7bnpg|@{m z^d6Y`*$Zf2Xr&|g%sai#5}Syvv(>Jnx&EM7-|Jr7!M~zdAyjt*xl;OLhvW-a%H1m0 z*x5*nb=R5u><7lyVpNAR?q@1U59 zO+)QWwL8t zyip?u_nI+K$uh{y)~}qj?(w0&=SE^8`_WMM zTybjG=999h38Yes7}-4*LJ7H)UE8{mE(6;8voE+TYY%33A>S6`G_95^5QHNTo_;Ao ztIQIZ_}49%{8|=O;isBZ?=7kfdF8_@azfoTd+hEJKWE!)$)N%HIe2cplaK`ry#=pV z0q{9w-`i0h@!R8K3GC{ivt{70IWG`EP|(1g7i_Q<>aEAT{5(yD z=!O?kq61VegV+st@XCw475j6vS)_z@efuqQgHQR1T4;|-#OLZNQJPV4k$AX1Uk8Lm z{N*b*ia=I+MB}kWpupJ~>!C@xEN#Wa7V+7{m4j8c?)ChV=D?o~sjT?0C_AQ7B-vxqX30s0I_`2$in86#`mAsT-w?j{&AL@B3$;P z31G4(lV|b}uSDCIrjk+M1R!X7s4Aabn<)zpgT}#gE|mIvV38^ODy@<&yflpCwS#fRf9ZX3lPV_?8@C5)A;T zqmouFLFk;qIs4rA=hh=GL~sCFsXHsqO6_y~*AFt939UYVBSx1s(=Kb&5;j7cSowdE;7()CC2|-i9Zz+_BIw8#ll~-tyH?F3{%`QCsYa*b#s*9iCc`1P1oC26?`g<9))EJ3%xz+O!B3 zZ7$j~To)C@PquR>a1+Dh>-a%IvH_Y7^ys|4o?E%3`I&ADXfC8++hAdZfzIT#%C+Jz z1lU~K_vAm0m8Qk}K$F>|>RPK%<1SI0(G+8q~H zAsjezyP+u!Se4q3GW)`h`NPSRlMoBjCzNPesWJwVTY!o@G8=(6I%4XHGaSiS3MEBK zhgGFv6Jc>L$4jVE!I?TQuwvz_%CyO!bLh94nqK11C2W$*aa2ueGopG8DnBICVUORP zgytv#)49fVXDaR$SukloYC3u7#5H)}1K21=?DKj^U)8G;MS)&Op)g^zR2($<>C*zW z;X7`hLxiIO#J`ANdyAOJle4V%ppa*(+0i3w;8i*BA_;u8gOO6)MY`ueq7stBMJTB; z-a0R>hT*}>z|Gg}@^zDL1MrH+2hsR8 zHc}*9IvuQC^Ju)^#Y{fOr(96rQNPNhxc;mH@W*m206>Lo<*SaaH?~8zg&f&%YiOEG zGiz?*CP>Bci}!WiS=zj#K5I}>DtpregpP_tfZtPa(N<%vo^#WCQ5BTv0vr%Z{)0q+ z)RbfHktUm|lg&U3YM%lMUM(fu}i#kjX9h>GYctkx9Mt_8{@s%!K_EI zScgwy6%_fR?CGJQtmgNAj^h9B#zmaMDWgH55pGuY1Gv7D z;8Psm(vEPiwn#MgJYu4Ty9D|h!?Rj0ddE|&L3S{IP%H4^N!m`60ZwZw^;eg4sk6K{ ziA^`Sbl_4~f&Oo%n;8Ye(tiAdlZKI!Z=|j$5hS|D$bDJ}p{gh$KN&JZYLUjv4h{NY zBJ>X9z!xfDGY z+oh_Z&_e#Q(-}>ssZfm=j$D&4W4FNy&-kAO1~#3Im;F)Nwe{(*75(p=P^VI?X0GFakfh+X-px4a%Uw@fSbmp9hM1_~R>?Z8+ ziy|e9>8V*`OP}4x5JjdWp}7eX;lVxp5qS}0YZek;SNmm7tEeSF*-dI)6U-A%m6YvCgM(}_=k#a6o^%-K4{`B1+}O4x zztDT%hVb;v#?j`lTvlFQ3aV#zkX=7;YFLS$uIzb0E3lozs5`Xy zi~vF+%{z9uLjKvKPhP%x5f~7-Gj+%5N`%^=yk*Qn{`> z;xj&ROY6g`iy2a@{O)V(jk&8#hHACVDXey5a+KDod_Z&}kHM}xt7}Md@pil{2x7E~ zL$k^d2@Ec2XskjrN+IILw;#7((abu;OJii&v3?60x>d_Ma(onIPtcVnX@ELF0aL?T zSmWiL3(dOFkt!x=1O!_0n(cAzZW+3nHJ{2S>tgSK?~cFha^y(l@-Mr2W$%MN{#af8J;V*>hdq!gx=d0h$T7l}>91Wh07)9CTX zh2_ZdQCyFOQ)l(}gft0UZG`Sh2`x-w`5vC2UD}lZs*5 zG76$akzn}Xi))L3oGJ75#pcN=cX3!=57$Ha=hQ2^lwdyU#a}4JJOz6ddR%zae%#4& za)bFj)z=YQela(F#Y|Q#dp}PJghITwXouVaMq$BM?K%cXn9^Y@g43$=O)F&ZlOUom zJiad#dea;-eywBA@e&D6Pdso1?2^(pXiN91?jvcaUyYoKUmvl5G9e$W!okWe*@a<^ z8cQQ6cNSf+UPDx%?_G4aIiybZHHagF{;IcD(dPO!#=u zWfqLcPc^+7Uu#l(Bpxft{*4lv#*u7X9AOzDO z1D9?^jIo}?%iz(_dwLa{ex#T}76ZfN_Z-hwpus9y+4xaUu9cX}&P{XrZVWE{1^0yw zO;YhLEW!pJcbCt3L8~a7>jsaN{V3>tz6_7`&pi%GxZ=V3?3K^U+*ryLSb)8^IblJ0 zSRLNDvIxt)S}g30?s_3NX>F?NKIGrG_zB9@Z>uSW3k2es_H2kU;Rnn%j5qP)!XHKE zPB2mHP~tLCg4K_vH$xv`HbRsJwbZMUV(t=ez;Ec(vyHH)FbfLg`c61I$W_uBB>i^r z&{_P;369-&>23R%qNIULe=1~T$(DA`ev*EWZ6j(B$(te}x1WvmIll21zvygkS%vwG zzkR6Z#RKA2!z!C%M!O>!=Gr0(J0FP=-MN=5t-Ir)of50y10W}j`GtRCsXBakrKtG& zazmITDJMA0C51&BnLY)SY9r)NVTMs);1<=oosS9g31l{4ztjD3#+2H7u_|66b|_*O z;Qk6nalpqdHOjx|K&vUS_6ITgGll;TdaN*ta=M_YtyC)I9Tmr~VaPrH2qb6sd~=AcIxV+%z{E&0@y=DPArw zdV7z(G1hBx7hd{>(cr43^WF%4Y@PXZ?wPpj{OQ#tvc$pABJbvPGvdR`cAtHn)cSEV zrpu}1tJwQ3y!mSmH*uz*x0o|CS<^w%&KJzsj~DU0cLQUxk5B!hWE>aBkjJle8z~;s z-!A=($+}Jq_BTK5^B!`R>!MulZN)F=iXXeUd0w5lUsE5VP*H*oCy(;?S$p*TVvTxwAeWFB$jHyb0593)$zqalVlDX=GcCN1gU0 zlgU)I$LcXZ8Oyc2TZYTPu@-;7<4YYB-``Qa;IDcvydIA$%kHhJKV^m*-zxcvU4viy&Kr5GVM{IT>WRywKQ9;>SEiQD*NqplK-KK4YR`p0@JW)n_{TU3bt0 zim%;(m1=#v2}zTps=?fU5w^(*y)xT%1vtQH&}50ZF!9YxW=&7*W($2kgKyz1mUgfs zfV<*XVVIFnohW=|j+@Kfo!#liQR^x>2yQdrG;2o8WZR+XzU_nG=Ed2rK?ntA;K5B{ z>M8+*A4!Jm^Bg}aW?R?6;@QG@uQ8&oJ{hFixcfEnJ4QH?A4>P=q29oDGW;L;= z9-a0;g%c`C+Ai!UmK$NC*4#;Jp<1=TioL=t^YM)<<%u#hnnfSS`nq63QKGO1L8RzX z@MFDqs1z ztYmxDl@LU)5acvHk)~Z`RW7=aJ_nGD!mOSYD>5Odjn@TK#LY{jf?+piB5AM-CAoT_ z?S-*q7}wyLJzK>N%eMPuFgN)Q_otKP;aqy=D5f!7<=n(lNkYRXVpkB{TAYLYg{|(jtRqYmg$xH zjmq?B(RE4 zQx^~Pt}gxC2~l=K$$-sYy_r$CO(d=+b3H1MB*y_5g6WLaWTXn+TKQ|hNY^>Mp6k*$ zwkovomhu776vQATqT4blf~g;TY(MWCrf^^yfWJvSAB$p5l;jm@o#=!lqw+Lqfq>X= z$6~kxfm7`3q4zUEB;u4qa#BdJxO!;xGm)wwuisj{0y2x{R(IGMrsIzDY9LW>m!Y`= z04sx3IjnYvL<4JqxQ8f7qYd0s2Ig%`ytYPEMKI)s(LD}D@EY>x`VFtqvnADNBdeao zC96X+MxnwKmjpg{U&gP3HE}1=s!lv&D{6(g_lzyF3A`7Jn*&d_kL<;dAFx!UZ>hB8 z5A*%LsAn;VLp>3${0>M?PSQ)9s3}|h2e?TG4_F{}{Cs>#3Q*t$(CUc}M)I}8cPF6% z=+h(Kh^8)}gj(0}#e7O^FQ6`~fd1#8#!}LMuo3A0bN`o}PYsm!Y}sdOz$+Tegc=qT z8x`PH$7lvnhJp{kHWb22l;@7B7|4yL4UOOVM0MP_>P%S1Lnid)+k9{+3D+JFa#Pyf zhVc#&df87APl4W9X)F3pGS>@etfl=_E5tBcVoOfrD4hmVeTY-cj((pkn%n@EgN{0f zwb_^Rk0I#iZuHK!l*lN`ceJn(sI{$Fq6nN& zE<-=0_2WN}m+*ivmIOxB@#~Q-cZ>l136w{#TIJe478`KE7@=a{>SzPHsKLzYAyBQO zAtuuF$-JSDy_S@6GW0MOE~R)b;+0f%_NMrW(+V#c_d&U8Z9+ec4=HmOHw?gdjF(Lu zzra83M_BoO-1b3;9`%&DHfuUY)6YDV21P$C!Rc?mv&{lx#f8oc6?0?x zK08{WP65?#>(vPfA-c=MCY|%*1_<3D4NX zeVTi-JGl2uP_2@0F{G({pxQOXt_d{g_CV6b?jNpfUG9;8yle-^4KHRvZs-_2siata zt+d_T@U$&t*xaD22(fH(W1r$Mo?3dc%Tncm=C6{V9y{v&VT#^1L04vDrLM9qBoZ4@ z6DBN#m57hX7$C(=#$Y5$bJmwA$T8jKD8+6A!-IJwA{WOfs%s}yxUw^?MRZjF$n_KN z6`_bGXcmE#5e4Ym)aQJ)xg3Pg0@k`iGuHe?f(5LtuzSq=nS^5z>vqU0EuZ&75V%Z{ zYyhRLN^)$c6Ds{f7*FBpE;n5iglx5PkHfWrj3`x^j^t z7ntuV`g!9Xg#^3!x)l*}IW=(Tz3>Y5l4uGaB&lz{GDjm2D5S$CExLT`I1#n^lBH7Y zDgpMag@`iETKAI=p<5E#LTkwzVR@=yY|uBVI1HG|8h+d;G-qfuj}-ZR6fN>EfCCW z9~wRQoAPEa#aO?3h?x{YvV*d+NtPkf&4V0k4|L=uj!U{L+oLa(z#&iuhJr3-PjO3R z5s?=nn_5^*^Rawr>>Nr@K(jwkB#JK-=+HqwfdO<+P5byeim)wvqGlP-P|~Nse8=XF zz`?RYB|D6SwS}C+YQv+;}k6$-%D(@+t14BL@vM z2q%q?f6D-A5s$_WY3{^G0F131bbh|g!}#BKw=HQ7mx;Dzg4Z*bTLQSfo{ed{4}NZW zfrRm^Ca$rlE{Ue~uYv>R9{3smwATcdM_6+yWIO z*ZRH~uXE@#p$XTbCt5j7j2=86e{9>HIB6xDzV+vAo&B?KUiMP|ttOElepnl%|DPqL b{|{}U^kRn2wo}j7|0ATu<;8xA7zX}7|B6mN literal 0 HcmV?d00001 diff --git a/examples/react/develop/simple-todo-list/public/manifest.json b/examples/react/develop/simple-todo-list/public/manifest.json new file mode 100644 index 00000000..080d6c77 --- /dev/null +++ b/examples/react/develop/simple-todo-list/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/examples/react/develop/simple-todo-list/public/robots.txt b/examples/react/develop/simple-todo-list/public/robots.txt new file mode 100644 index 00000000..e9e57dc4 --- /dev/null +++ b/examples/react/develop/simple-todo-list/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/examples/react/develop/simple-todo-list/src/App.js b/examples/react/develop/simple-todo-list/src/App.js new file mode 100644 index 00000000..7844b8c2 --- /dev/null +++ b/examples/react/develop/simple-todo-list/src/App.js @@ -0,0 +1,53 @@ +import React from 'react'; +import { generateId } from '@agile-ts/core'; +import { useAgile } from '@agile-ts/react'; +import { TODOS } from './core'; + +const App = () => { + // With the 'useAgile' Hook we bind our first Collection to the 'RandomComponent' for reactivity + const todos = useAgile(TODOS); + + // Current Input of Name Form + const [currentInput, setCurrentInput] = React.useState(''); + + return ( +
+

Simple TODOS

+ { + setCurrentInput(event.target.value); + }} + /> + + {todos.map((value) => ( +
+
{value.name}
+ +
+ ))} +
+ ); +}; + +export default App; diff --git a/examples/react/develop/simple-todo-list/src/core.js b/examples/react/develop/simple-todo-list/src/core.js new file mode 100644 index 00000000..885a77f8 --- /dev/null +++ b/examples/react/develop/simple-todo-list/src/core.js @@ -0,0 +1,9 @@ +import { createCollection } from '@agile-ts/core'; +import { assignSharedAgileLoggerConfig, Logger } from '@agile-ts/logger'; + +assignSharedAgileLoggerConfig({ level: Logger.level.DEBUG }); + +// Create Collection +export const TODOS = createCollection({ + initialData: [{ id: 1, name: 'Clean Bathroom' }], +}).persist('todos'); // perist does store the Collection in the Local Storage diff --git a/examples/react/develop/simple-todo-list/src/index.js b/examples/react/develop/simple-todo-list/src/index.js new file mode 100644 index 00000000..c1f31c5f --- /dev/null +++ b/examples/react/develop/simple-todo-list/src/index.js @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import App from './App'; + +ReactDOM.render( + + + , + document.getElementById('root') +); diff --git a/packages/core/src/collection/collection.persistent.ts b/packages/core/src/collection/collection.persistent.ts index 2b2232f1..855b8430 100644 --- a/packages/core/src/collection/collection.persistent.ts +++ b/packages/core/src/collection/collection.persistent.ts @@ -36,7 +36,7 @@ export class CollectionPersistent< config: CreatePersistentConfigInterface = {} ) { super(collection.agileInstance(), { - instantiate: false, + loadValue: false, }); config = defineConfig(config, { instantiate: true, @@ -51,7 +51,7 @@ export class CollectionPersistent< }); // Load/Store persisted value/s for the first time - if (this.ready && config.instantiate) this.initialLoading(); + if (this.ready && config.loadValue) this.initialLoading(); } /** @@ -104,6 +104,7 @@ export class CollectionPersistent< // Persist default Group and load its value manually to be 100% sure // that it was loaded completely + defaultGroup.loadedInitialValue = false; defaultGroup.persist(defaultGroupStorageKey, { loadValue: false, defaultStorageKey: this.config.defaultStorageKey || undefined, @@ -113,14 +114,6 @@ export class CollectionPersistent< if (defaultGroup.persistent?.ready) await defaultGroup.persistent.initialLoading(); - // TODO rebuild the default Group once at the end when all Items were loaded into the Collection - // because otherwise it rebuilds the Group for each loaded Item - // (-> warnings are printed for all not yet loaded Items when rebuilding the Group) - // or rethink the whole Group rebuild process by adding a 'addItem()', 'removeItem()' and 'updateItem()' function - // so that there is no need for rebuilding the whole Group when for example only Item B changed or Item C was added - // - // See Issue by starting the vue develop example app and adding some todos to the _todo_ list - // Persist Items found in the default Group's value for (const itemKey of defaultGroup._value) { const item = this.collection().getItem(itemKey); @@ -156,13 +149,14 @@ export class CollectionPersistent< followCollectionPersistKeyPattern: false, // Because of the dynamic 'storageItemKey', the key is already formatted above }); if (placeholderItem?.persistent?.ready) { - const loadedPersistedValueIntoItem = await placeholderItem.persistent.loadPersistedValue(); // TODO FIRST GROUP REBUILD (by assigning loaded value to Item) + const loadedPersistedValueIntoItem = await placeholderItem.persistent.loadPersistedValue(); // If successfully loaded Item value, assign Item to Collection if (loadedPersistedValueIntoItem) { this.collection().assignItem(placeholderItem, { overwrite: true, // Overwrite to overwrite the existing placeholder Item, if one exists - }); // TODO SECOND GROUP REBUILD (by calling rebuildGroupThatInclude() in the assignItem() method) + rebuildGroups: false, // Not necessary since the Groups that include the to assign Item were already rebuild while assigning the loaded value to the Item via 'loadPersistedValue()' + }); placeholderItem.isPersisted = true; @@ -176,6 +170,7 @@ export class CollectionPersistent< } } + defaultGroup.loadedInitialValue = true; return true; }; const success = await loadValuesIntoCollection(); diff --git a/packages/core/src/collection/collection.ts b/packages/core/src/collection/collection.ts index bf713291..f6107554 100644 --- a/packages/core/src/collection/collection.ts +++ b/packages/core/src/collection/collection.ts @@ -8,6 +8,7 @@ import { Group, GroupAddConfigInterface, GroupConfigInterface, + GroupIngestConfigInterface, GroupKey, isFunction, isValidObject, @@ -19,7 +20,6 @@ import { Selector, SelectorConfigInterface, SelectorKey, - SideEffectConfigInterface, StorageKey, TrackedChangeMethod, } from '../internal'; @@ -979,7 +979,7 @@ export class Collection< // Create Persistent (-> persist value) this.persistent = new CollectionPersistent(this, { - instantiate: _config.loadValue, + loadValue: _config.loadValue, storageKeys: _config.storageKeys, key: key, defaultStorageKey: _config.defaultStorageKey, @@ -1406,6 +1406,7 @@ export class Collection< config = defineConfig(config, { overwrite: false, background: false, + rebuildGroups: true, }); const primaryKey = this.config.primaryKey; let itemKey = item._value[primaryKey]; @@ -1443,9 +1444,10 @@ export class Collection< // Rebuild Groups that include itemKey // after adding Item with itemKey to the Collection // (because otherwise it can't find the Item as it isn't added yet) - this.rebuildGroupsThatIncludeItemKey(itemKey, { - background: config.background, - }); + if (config.rebuildGroups) + this.rebuildGroupsThatIncludeItemKey(itemKey, { + background: config.background, + }); if (increaseCollectionSize) this.size++; @@ -1461,18 +1463,10 @@ export class Collection< */ public rebuildGroupsThatIncludeItemKey( itemKey: ItemKey, - config: RebuildGroupsThatIncludeItemKeyConfigInterface = {} + config: GroupIngestConfigInterface = {} ): void { - config = defineConfig(config, { - background: false, - sideEffects: { - enabled: true, - exclude: [], - }, - }); - // Rebuild Groups that include itemKey - for (const groupKey in this.groups) { + for (const groupKey of Object.keys(this.groups)) { const group = this.getGroup(groupKey); if (group != null && group.has(itemKey)) { group.trackChange({ @@ -1481,11 +1475,7 @@ export class Collection< method: TrackedChangeMethod.UPDATE, }); - group.rebuild({ - background: config?.background, - sideEffects: config?.sideEffects, - storage: false, - }); + group.rebuild(config); } } } @@ -1604,20 +1594,6 @@ export interface UpdateItemKeyConfigInterface { background?: boolean; } -export interface RebuildGroupsThatIncludeItemKeyConfigInterface { - /** - * Whether to rebuilt the Group in background. - * So that the UI isn't notified of these changes and thus doesn't rerender. - * @default false - */ - background?: boolean; - /** - * Whether to execute the defined side effects. - * @default true - */ - sideEffects?: SideEffectConfigInterface; -} - export interface HasConfigInterface { /** * Whether Items that do not officially exist, @@ -1697,4 +1673,9 @@ export interface AssignItemConfigInterface { * @default false */ background?: boolean; + /** + * Whether to rebuild all Groups that include the itemKey of the to assign Item. + * @default true + */ + rebuildGroups?: boolean; } diff --git a/packages/core/src/collection/group/index.ts b/packages/core/src/collection/group/index.ts index ece977e4..b34eda3d 100644 --- a/packages/core/src/collection/group/index.ts +++ b/packages/core/src/collection/group/index.ts @@ -18,6 +18,7 @@ import { GroupObserver, StateObserver, defineConfig, + GroupIngestConfigInterface, } from '../../internal'; export class Group< @@ -48,6 +49,10 @@ export class Group< // (-> Simplicity and keeping the current structure to not rewrite all tests) public trackedChanges: TrackedChangeInterface[] = []; + // Whether the initial value was loaded from the corresponding Persistent + // https://github.com/agile-ts/agile/issues/155 + public loadedInitialValue = true; + /** * An extension of the State Class that categorizes and preserves the ordering of structured data. * It allows us to cluster together data from a Collection as an array of Item keys. @@ -113,7 +118,7 @@ export class Group< * @param itemKey - Key/Name identifier of the Item. */ public has(itemKey: ItemKey) { - return this.value.findIndex((key) => key === itemKey) !== -1; + return this.value.indexOf(itemKey) !== -1; } /** @@ -377,7 +382,7 @@ export class Group< * @internal * @param config - Configuration object */ - public rebuild(config: StateIngestConfigInterface = {}): this { + public rebuild(config: GroupIngestConfigInterface = {}): this { // Don't rebuild Group if Collection isn't correctly instantiated yet // (because only after a successful instantiation the Collection // contains the Items which are essential for a proper rebuild) @@ -425,7 +430,7 @@ export class Group< }); // Logging - if (notFoundItemKeys.length > 0) { + if (notFoundItemKeys.length > 0 && this.loadedInitialValue) { LogCodeManager.log( '1C:02:00', [this.collection()._key, this._key], diff --git a/packages/core/src/state/state.enhanced.ts b/packages/core/src/state/state.enhanced.ts index fe3a4c67..a922d7cc 100644 --- a/packages/core/src/state/state.enhanced.ts +++ b/packages/core/src/state/state.enhanced.ts @@ -455,7 +455,7 @@ export class EnhancedState extends State { // Create Persistent (-> persist value) this.persistent = new StatePersistent(this, { - instantiate: _config.loadValue, + loadValue: _config.loadValue, storageKeys: _config.storageKeys, key: key, defaultStorageKey: _config.defaultStorageKey, diff --git a/packages/core/src/state/state.persistent.ts b/packages/core/src/state/state.persistent.ts index 5a1d7f58..759f14a2 100644 --- a/packages/core/src/state/state.persistent.ts +++ b/packages/core/src/state/state.persistent.ts @@ -25,7 +25,7 @@ export class StatePersistent extends Persistent { config: CreatePersistentConfigInterface = {} ) { super(state.agileInstance(), { - instantiate: false, + loadValue: false, }); config = defineConfig(config, { instantiate: true, @@ -40,7 +40,7 @@ export class StatePersistent extends Persistent { }); // Load/Store persisted value/s for the first time - if (this.ready && config.instantiate) this.initialLoading(); + if (this.ready && config.loadValue) this.initialLoading(); } /** diff --git a/packages/core/src/storages/persistent.ts b/packages/core/src/storages/persistent.ts index 11deec56..4ff6f9c7 100644 --- a/packages/core/src/storages/persistent.ts +++ b/packages/core/src/storages/persistent.ts @@ -53,7 +53,7 @@ export class Persistent { this.config = { defaultStorageKey: config.defaultStorageKey as any }; // Instantiate Persistent - if (config.instantiate) { + if (config.loadValue) { this.instantiatePersistent({ storageKeys: config.storageKeys, key: config.key, @@ -318,11 +318,11 @@ export interface CreatePersistentConfigInterface { */ defaultStorageKey?: StorageKey; /** - * Whether the Persistent should be instantiated immediately + * Whether the Persistent should load/persist the value immediately * or whether this should be done manually. * @default true */ - instantiate?: boolean; + loadValue?: boolean; } export interface PersistentConfigInterface { diff --git a/packages/core/tests/unit/collection/collection.persistent.test.ts b/packages/core/tests/unit/collection/collection.persistent.test.ts index b4f64ef1..ca1b74bd 100644 --- a/packages/core/tests/unit/collection/collection.persistent.test.ts +++ b/packages/core/tests/unit/collection/collection.persistent.test.ts @@ -144,7 +144,7 @@ describe('CollectionPersistent Tests', () => { }); const collectionPersistent = new CollectionPersistent(dummyCollection, { - instantiate: false, + loadValue: false, }); expect(collectionPersistent).toBeInstanceOf(CollectionPersistent); @@ -428,10 +428,9 @@ 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 @@ -580,10 +579,9 @@ 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( @@ -853,7 +851,9 @@ describe('CollectionPersistent Tests', () => { it("shouldn't add rebuild Storage side effect to the default Group", () => { collectionPersistent.setupSideEffects(); - expect(dummyDefaultGroup.addSideEffect).toHaveBeenCalledWith( + expect( + dummyDefaultGroup.addSideEffect + ).toHaveBeenCalledWith( CollectionPersistent.defaultGroupSideEffectKey, expect.any(Function), { weight: 0 } diff --git a/packages/core/tests/unit/state/state.persistent.test.ts b/packages/core/tests/unit/state/state.persistent.test.ts index 1fa45381..5cc5531d 100644 --- a/packages/core/tests/unit/state/state.persistent.test.ts +++ b/packages/core/tests/unit/state/state.persistent.test.ts @@ -117,7 +117,7 @@ describe('StatePersistent Tests', () => { }); const statePersistent = new StatePersistent(dummyState, { - instantiate: false, + loadValue: false, }); expect(statePersistent.initialLoading).not.toHaveBeenCalled(); @@ -315,7 +315,9 @@ describe('StatePersistent Tests', () => { () => { statePersistent.setupSideEffects(); - expect(dummyState.addSideEffect).toHaveBeenCalledWith( + expect( + dummyState.addSideEffect + ).toHaveBeenCalledWith( StatePersistent.storeValueSideEffectKey, expect.any(Function), { weight: 0 } diff --git a/packages/core/tests/unit/storages/persistent.test.ts b/packages/core/tests/unit/storages/persistent.test.ts index 44d09106..5c404777 100644 --- a/packages/core/tests/unit/storages/persistent.test.ts +++ b/packages/core/tests/unit/storages/persistent.test.ts @@ -83,7 +83,7 @@ describe('Persistent Tests', () => { .spyOn(Persistent.prototype, 'instantiatePersistent') .mockReturnValueOnce(undefined); - const persistent = new Persistent(dummyAgile, { instantiate: false }); + const persistent = new Persistent(dummyAgile, { loadValue: false }); expect(persistent).toBeInstanceOf(Persistent); expect(persistent.instantiatePersistent).not.toHaveBeenCalled(); From 4a408502670a7f1a48a6c8d5600b8f5569bb3444 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Fri, 10 Sep 2021 08:22:28 +0200 Subject: [PATCH 84/93] fixed typos --- .../benchmarks/react/1000fields/index.ts | 22 ++++---- .../typescript/cloneDeep/bench/lodash.ts | 6 ++ .../typescript/cloneDeep/bench/looper.ts | 19 +++++++ .../typescript/cloneDeep/bench/stringify.ts | 3 + .../benchmarks/typescript/cloneDeep/index.ts | 56 +++++++++++++++++++ benchmark/package.json | 1 + benchmark/yarn.lock | 7 ++- packages/core/src/collection/group/index.ts | 1 + packages/core/src/collection/item.ts | 3 - 9 files changed, 103 insertions(+), 15 deletions(-) create mode 100644 benchmark/benchmarks/typescript/cloneDeep/bench/lodash.ts create mode 100644 benchmark/benchmarks/typescript/cloneDeep/bench/looper.ts create mode 100644 benchmark/benchmarks/typescript/cloneDeep/bench/stringify.ts create mode 100644 benchmark/benchmarks/typescript/cloneDeep/index.ts diff --git a/benchmark/benchmarks/react/1000fields/index.ts b/benchmark/benchmarks/react/1000fields/index.ts index 1c2b070e..3bb8218a 100644 --- a/benchmark/benchmarks/react/1000fields/index.ts +++ b/benchmark/benchmarks/react/1000fields/index.ts @@ -78,18 +78,18 @@ const results: CycleResultInterface[] = []; // Add Tests to the Benchmark Test Suite suite .add('Agile Collection', configTest(agileCollection)) - // .add('Agile State', configTest(agileState)) - // .add('Agile nested State', configTest(agileNestedState)) + .add('Agile State', configTest(agileState)) + .add('Agile nested State', configTest(agileNestedState)) .add('Pulse Collection', configTest(pulseCollection)) - // .add('Pulse State', configTest(pulseState)) - // .add('Pulse nested State', configTest(pulseNestedState)) - // .add('Hookstate', configTest(hookstate)) - // .add('Jotai', configTest(jotai)) - // .add('Mobx', configTest(mobx)) - // .add('Nano Stores', configTest(nanostores)) - // .add('Recoil', configTest(recoil)) - // .add('Redux', configTest(redux)) - // .add('Valtio', configTest(valtio)) + .add('Pulse State', configTest(pulseState)) + .add('Pulse nested State', configTest(pulseNestedState)) + .add('Hookstate', configTest(hookstate)) + .add('Jotai', configTest(jotai)) + .add('Mobx', configTest(mobx)) + .add('Nano Stores', configTest(nanostores)) + .add('Recoil', configTest(recoil)) + .add('Redux', configTest(redux)) + .add('Valtio', configTest(valtio)) // Add Listener .on('start', function (this: any) { diff --git a/benchmark/benchmarks/typescript/cloneDeep/bench/lodash.ts b/benchmark/benchmarks/typescript/cloneDeep/bench/lodash.ts new file mode 100644 index 00000000..16e2fa76 --- /dev/null +++ b/benchmark/benchmarks/typescript/cloneDeep/bench/lodash.ts @@ -0,0 +1,6 @@ +// @ts-ignore +import _ from 'lodash'; + +export function cloneDeep(value: T): T { + return _.cloneDeep(value); +} diff --git a/benchmark/benchmarks/typescript/cloneDeep/bench/looper.ts b/benchmark/benchmarks/typescript/cloneDeep/bench/looper.ts new file mode 100644 index 00000000..146c5c7e --- /dev/null +++ b/benchmark/benchmarks/typescript/cloneDeep/bench/looper.ts @@ -0,0 +1,19 @@ +export function cloneDeep(value: T): T { + // Extra checking 'value == null' because 'typeof null === object' + if (value == null || typeof value !== 'object') return value; + + // Ignore everything that is no object or array but has the type of an object (e.g. classes) + const valConstructorName = Object.getPrototypeOf( + value + ).constructor.name.toLowerCase(); + if (valConstructorName !== 'object' && valConstructorName !== 'array') + return value; + + let temp; + const newObject: any = Array.isArray(value) ? [] : {}; + for (const property in value) { + temp = value[property]; + newObject[property] = cloneDeep(temp); + } + return newObject as T; +} diff --git a/benchmark/benchmarks/typescript/cloneDeep/bench/stringify.ts b/benchmark/benchmarks/typescript/cloneDeep/bench/stringify.ts new file mode 100644 index 00000000..87cee1d2 --- /dev/null +++ b/benchmark/benchmarks/typescript/cloneDeep/bench/stringify.ts @@ -0,0 +1,3 @@ +export function cloneDeep(value: T): T { + return JSON.parse(JSON.stringify(value)); +} diff --git a/benchmark/benchmarks/typescript/cloneDeep/index.ts b/benchmark/benchmarks/typescript/cloneDeep/index.ts new file mode 100644 index 00000000..1ecda695 --- /dev/null +++ b/benchmark/benchmarks/typescript/cloneDeep/index.ts @@ -0,0 +1,56 @@ +import Benchmark, { Suite } from 'benchmark'; +import { + cycleLog, + CycleResultInterface, + endBenchmarkLog, + getCycleResult, + startBenchmarkLog, +} from '../../benchmarkManager'; + +// Files to run the Benchmark on +import * as lodash from './bench/lodash'; +import * as looper from './bench/looper'; +import * as stringify from './bench/stringify'; + +const toClone = { x1: true, x2: undefined }; + +// @ts-ignore +// Benchmark.js requires an instance of itself globally +window.Benchmark = Benchmark; + +// Create new Benchmark Test Suite +const suite = new Suite('clone deep'); + +const results: CycleResultInterface[] = []; + +// Add Tests to the Benchmark Test Suite +suite + .add('Lodash', function () { + lodash.cloneDeep(toClone); + }) + .add('Looper', function () { + looper.cloneDeep(toClone); + }) + .add('Stringify', function () { + stringify.cloneDeep(toClone); + }) + + // Add Listener + .on('start', function (this: any) { + startBenchmarkLog(this.name); + }) + .on('cycle', (event: any) => { + const cycleResult = getCycleResult(event); + cycleLog(cycleResult); + results.push(cycleResult); + }) + .on('complete', function (this: any) { + endBenchmarkLog(this.name, results, this.filter('fastest').map('name')); + + // @ts-ignore + // Notify server that the Benchmark Test Suite has ended + window.TEST.ended = true; + }) + + // Run Benchmark Test Suite + .run({ async: true }); diff --git a/benchmark/package.json b/benchmark/package.json index 137ac46f..21852877 100644 --- a/benchmark/package.json +++ b/benchmark/package.json @@ -12,6 +12,7 @@ "test:1000fields": "yarn test ./benchmarks/react/1000fields", "test:computed": "yarn test ./benchmarks/react/computed", "test:defineConfig": "yarn test ./benchmarks/typescript/defineConfig", + "test:cloneDeep": "yarn test ./benchmarks/typescript/cloneDeep", "install:dev:agile": "yalc add @agile-ts/core @agile-ts/react & yarn install", "install:prod:agile": "yarn add @agile-ts/core @agile-ts/react & yarn install" }, diff --git a/benchmark/yarn.lock b/benchmark/yarn.lock index 5e123e81..a7d7c012 100644 --- a/benchmark/yarn.lock +++ b/benchmark/yarn.lock @@ -3,7 +3,12 @@ "@agile-ts/core@file:.yalc/@agile-ts/core": - version "0.2.0-alpha.3" + version "0.2.0-alpha.5" + dependencies: + "@agile-ts/utils" "^0.0.7" + +"@agile-ts/logger@file:.yalc/@agile-ts/logger": + version "0.0.7" dependencies: "@agile-ts/utils" "^0.0.7" diff --git a/packages/core/src/collection/group/index.ts b/packages/core/src/collection/group/index.ts index b34eda3d..e2c75421 100644 --- a/packages/core/src/collection/group/index.ts +++ b/packages/core/src/collection/group/index.ts @@ -410,6 +410,7 @@ export class Group< this.nextGroupOutput.splice(change.index, 1); break; default: + break; } }); this.trackedChanges = []; diff --git a/packages/core/src/collection/item.ts b/packages/core/src/collection/item.ts index db98104f..44266aee 100644 --- a/packages/core/src/collection/item.ts +++ b/packages/core/src/collection/item.ts @@ -166,9 +166,6 @@ export class Item extends EnhancedState< this.addSideEffect>( Item.updateGroupSideEffectKey, (instance, config) => { - // TODO optimise this because currently the whole Group rebuilds - // although only one Item value has changed which definitely needs no complete rebuild - // https://github.com/agile-ts/agile/issues/113 instance.collection().rebuildGroupsThatIncludeItemKey(itemKey, config); }, { weight: 100 } From 61565512d94a85900821f2209f1bcbb446941d85 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Fri, 10 Sep 2021 11:57:19 +0200 Subject: [PATCH 85/93] fixed some tests --- .../src/collection/collection.persistent.ts | 2 +- packages/core/src/collection/group/index.ts | 64 +++--- packages/core/src/state/state.persistent.ts | 2 +- packages/core/src/storages/persistent.ts | 2 +- .../collection/collection.persistent.test.ts | 47 ++-- .../tests/unit/collection/collection.test.ts | 93 ++++---- .../collection/group/group.observer.test.ts | 47 ++-- .../tests/unit/collection/group/group.test.ts | 200 +++++++++++++----- .../tests/unit/state/state.enhanced.test.ts | 16 +- .../tests/unit/state/state.persistent.test.ts | 66 ++++-- 10 files changed, 361 insertions(+), 178 deletions(-) diff --git a/packages/core/src/collection/collection.persistent.ts b/packages/core/src/collection/collection.persistent.ts index 855b8430..0cbf3987 100644 --- a/packages/core/src/collection/collection.persistent.ts +++ b/packages/core/src/collection/collection.persistent.ts @@ -39,7 +39,7 @@ export class CollectionPersistent< loadValue: false, }); config = defineConfig(config, { - instantiate: true, + loadValue: true, storageKeys: [], defaultStorageKey: null as any, }); diff --git a/packages/core/src/collection/group/index.ts b/packages/core/src/collection/group/index.ts index e2c75421..b0a5ac99 100644 --- a/packages/core/src/collection/group/index.ts +++ b/packages/core/src/collection/group/index.ts @@ -42,11 +42,12 @@ export class Group< // Keeps track of all Item identifiers for Items that couldn't be found in the Collection public notFoundItemKeys: Array = []; - // Keeps track of all changes made between rebuilds (add, remove, update) - // Why not rebuilding the Group directly in the add(), remove() method? - // Because rebuilding the Group is a side effect of the Group. - // A rebuild should always happen whenever the Group mutates. + // Keeps track of all changes made between rebuilds (add, remove, update). + // Why not rebuilding the Group directly in the add(), remove() methods? + // Because rebuilding the Group is also a side effect of the Group. + // Thus a rebuild should always happen whenever the Group mutates. // (-> Simplicity and keeping the current structure to not rewrite all tests) + // Note: Changes array must be processed from front to back otherwise issues might arise!! public trackedChanges: TrackedChangeInterface[] = []; // Whether the initial value was loaded from the corresponding Persistent @@ -156,8 +157,10 @@ export class Group< // Remove itemKeys from Group _itemKeys.forEach((itemKey) => { + const exists = newGroupValue.includes(itemKey); + // Check if itemKey exists in Group - if (!newGroupValue.includes(itemKey)) { + if (!exists) { notExistingItemKeys.push(itemKey); notExistingItemKeysInCollection.push(itemKey); return; @@ -187,7 +190,7 @@ export class Group< if (notExistingItemKeysInCollection.length >= _itemKeys.length) config.background = true; - this.set(newGroupValue, config); + this.set(newGroupValue, removeProperties(config, ['softRebuild'])); return this; } @@ -208,10 +211,9 @@ export class Group< const _itemKeys = normalizeArray(itemKeys); const notExistingItemKeysInCollection: Array = []; const existingItemKeys: Array = []; - let newGroupValue = copy(this.nextStateValue); + const newGroupValue = copy(this.nextStateValue); config = defineConfig(config, { method: 'push', - overwrite: false, softRebuild: true, }); @@ -223,30 +225,21 @@ export class Group< if (!this.collection().getItem(itemKey)) notExistingItemKeysInCollection.push(itemKey); + // Handle existing Item + if (exists) { + existingItemKeys.push(itemKey); + return; + } + // Track changes to soft rebuild the Group when rebuilding the Group - if (config.softRebuild && (!exists || (exists && config.overwrite))) { + if (config.softRebuild) { this.trackChange({ - method: exists ? TrackedChangeMethod.UPDATE : TrackedChangeMethod.ADD, + method: TrackedChangeMethod.ADD, key: itemKey, - index: exists - ? newGroupValue.findIndex((ik) => ik === itemKey) - : config.method === 'push' - ? newGroupValue.length - 1 - : 0, + index: config.method === 'push' ? newGroupValue.length - 1 : 0, }); } - // Remove itemKey temporary from newGroupValue - // if it should be overwritten and already exists in the newGroupValue - if (exists) { - if (config.overwrite) { - newGroupValue = newGroupValue.filter((key) => key !== itemKey); - } else { - existingItemKeys.push(itemKey); - return; - } - } - // Add new itemKey to Group newGroupValue[config.method || 'push'](itemKey); }); @@ -262,7 +255,10 @@ export class Group< ) config.background = true; - this.set(newGroupValue, removeProperties(config, ['method', 'overwrite'])); + this.set( + newGroupValue, + removeProperties(config, ['method', 'softRebuild']) + ); return this; } @@ -481,13 +477,9 @@ export interface GroupAddConfigInterface extends StateIngestConfigInterface { */ method?: 'unshift' | 'push'; /** - * If the to add `itemKey` already exists, - * whether its position should be overwritten with the position of the new `itemKey`. - * @default false - */ - overwrite?: boolean; - /** - * TODO + * Whether to soft rebuild the Group. + * -> only rebuild the parts of the Group that have actually changed + * instead of rebuilding the whole Group. * @default true */ softRebuild?: boolean; @@ -495,7 +487,9 @@ export interface GroupAddConfigInterface extends StateIngestConfigInterface { export interface GroupRemoveConfigInterface extends StateIngestConfigInterface { /** - * TODO + * Whether to soft rebuild the Group. + * -> only rebuild the parts of the Group that have actually changed + * instead of rebuilding the whole Group. * @default true */ softRebuild?: boolean; diff --git a/packages/core/src/state/state.persistent.ts b/packages/core/src/state/state.persistent.ts index 759f14a2..8d1fa0d9 100644 --- a/packages/core/src/state/state.persistent.ts +++ b/packages/core/src/state/state.persistent.ts @@ -28,7 +28,7 @@ export class StatePersistent extends Persistent { loadValue: false, }); config = defineConfig(config, { - instantiate: true, + loadValue: true, storageKeys: [], defaultStorageKey: null as any, }); diff --git a/packages/core/src/storages/persistent.ts b/packages/core/src/storages/persistent.ts index 4ff6f9c7..f83aeae9 100644 --- a/packages/core/src/storages/persistent.ts +++ b/packages/core/src/storages/persistent.ts @@ -46,7 +46,7 @@ export class Persistent { this.agileInstance = () => agileInstance; this._key = Persistent.placeHolderKey; config = defineConfig(config, { - instantiate: true, + loadValue: true, storageKeys: [], defaultStorageKey: null as any, }); diff --git a/packages/core/tests/unit/collection/collection.persistent.test.ts b/packages/core/tests/unit/collection/collection.persistent.test.ts index ca1b74bd..4c44d3f8 100644 --- a/packages/core/tests/unit/collection/collection.persistent.test.ts +++ b/packages/core/tests/unit/collection/collection.persistent.test.ts @@ -61,6 +61,7 @@ describe('CollectionPersistent Tests', () => { }); expect(collectionPersistent.initialLoading).toHaveBeenCalled(); + // Check if Persistent was called with correct parameters expect(collectionPersistent._key).toBe(CollectionPersistent.placeHolderKey); expect(collectionPersistent.ready).toBeTruthy(); expect(collectionPersistent.isPersisted).toBeFalsy(); @@ -94,6 +95,7 @@ describe('CollectionPersistent Tests', () => { }); expect(collectionPersistent.initialLoading).toHaveBeenCalled(); + // Check if Persistent was called with correct parameters expect(collectionPersistent._key).toBe(CollectionPersistent.placeHolderKey); expect(collectionPersistent.ready).toBeTruthy(); expect(collectionPersistent.isPersisted).toBeFalsy(); @@ -124,6 +126,7 @@ describe('CollectionPersistent Tests', () => { }); expect(collectionPersistent.initialLoading).not.toHaveBeenCalled(); + // Check if Persistent was called with correct parameters expect(collectionPersistent._key).toBe(CollectionPersistent.placeHolderKey); expect(collectionPersistent.ready).toBeFalsy(); expect(collectionPersistent.isPersisted).toBeFalsy(); @@ -134,7 +137,7 @@ describe('CollectionPersistent Tests', () => { }); }); - it("should create CollectionPersistent and shouldn't call initialLoading if Persistent is ready (config.instantiate = false)", () => { + it("should create CollectionPersistent and shouldn't call initialLoading if Persistent is ready (config.loadValue = false)", () => { // Overwrite instantiatePersistent once to not call it and set ready property jest .spyOn(CollectionPersistent.prototype, 'instantiatePersistent') @@ -156,6 +159,7 @@ describe('CollectionPersistent Tests', () => { }); expect(collectionPersistent.initialLoading).not.toHaveBeenCalled(); + // Check if Persistent was called with correct parameters expect(collectionPersistent._key).toBe(CollectionPersistent.placeHolderKey); expect(collectionPersistent.ready).toBeTruthy(); expect(collectionPersistent.isPersisted).toBeFalsy(); @@ -428,9 +432,13 @@ describe('CollectionPersistent Tests', () => { expect( placeholderItem1?.persistent?.loadPersistedValue ).toHaveBeenCalledTimes(1); - expect( - dummyCollection.assignItem - ).toHaveBeenCalledWith(placeholderItem1, { overwrite: true }); + expect(dummyCollection.assignItem).toHaveBeenCalledWith( + placeholderItem1, + { + overwrite: true, + rebuildGroups: false, + } + ); expect(placeholderItem1.isPersisted).toBeTruthy(); // Placeholder Item 2 @@ -453,7 +461,11 @@ describe('CollectionPersistent Tests', () => { placeholderItem2?.persistent?.loadPersistedValue ).not.toHaveBeenCalled(); expect(dummyCollection.assignItem).not.toHaveBeenCalledWith( - placeholderItem2 + placeholderItem2, + { + overwrite: true, + rebuildGroups: false, + } ); // Because Item persistent isn't ready expect(placeholderItem2.isPersisted).toBeFalsy(); @@ -476,9 +488,12 @@ describe('CollectionPersistent Tests', () => { expect( placeholderItem3?.persistent?.loadPersistedValue ).toHaveBeenCalledTimes(1); - expect(dummyCollection.assignItem).not.toHaveBeenCalledWith( - placeholderItem3 - ); // Because Item persistent 'leadPersistedValue()' returned false -> Item properly doesn't exist in Storage + expect( + dummyCollection.assignItem + ).not.toHaveBeenCalledWith(placeholderItem3, { + overwrite: true, + rebuildGroups: false, + }); // Because Item persistent 'leadPersistedValue()' returned false -> Item properly doesn't exist in Storage expect(placeholderItem3.isPersisted).toBeFalsy(); expect(collectionPersistent.setupSideEffects).toHaveBeenCalledWith( @@ -560,7 +575,11 @@ describe('CollectionPersistent Tests', () => { '3' ); // Because Item 3 is already present in the Collection expect(dummyCollection.assignItem).not.toHaveBeenCalledWith( - placeholderItem3 + placeholderItem3, + { + overwrite: true, + rebuildGroups: false, + } ); // Because Item 3 is already present in the Collection // Placeholder Item 1 @@ -579,9 +598,13 @@ describe('CollectionPersistent Tests', () => { expect( placeholderItem1?.persistent?.loadPersistedValue ).toHaveBeenCalledTimes(1); - expect( - dummyCollection.assignItem - ).toHaveBeenCalledWith(placeholderItem1, { overwrite: true }); + expect(dummyCollection.assignItem).toHaveBeenCalledWith( + placeholderItem1, + { + overwrite: true, + rebuildGroups: false, + } + ); expect(placeholderItem1.isPersisted).toBeTruthy(); expect(collectionPersistent.setupSideEffects).toHaveBeenCalledWith( diff --git a/packages/core/tests/unit/collection/collection.test.ts b/packages/core/tests/unit/collection/collection.test.ts index 86a019a1..83d9671e 100644 --- a/packages/core/tests/unit/collection/collection.test.ts +++ b/packages/core/tests/unit/collection/collection.test.ts @@ -7,6 +7,7 @@ import { CollectionPersistent, ComputedTracker, StatePersistent, + TrackedChangeMethod, } from '../../../src'; import * as Utils from '@agile-ts/utils'; import { LogMock } from '../../helper/logMock'; @@ -264,10 +265,10 @@ describe('Collection Tests', () => { key: 'group1Key', }); - expect(collection.createGroup).toHaveBeenCalledWith( - 'group1Key', - [1, 2] - ); + expect(collection.createGroup).toHaveBeenCalledWith('group1Key', [ + 1, + 2, + ]); LogMock.hasLoggedCode('1B:02:00'); expect(response).toBeInstanceOf(Group); @@ -1820,19 +1821,19 @@ describe('Collection Tests', () => { }); describe('persist function tests', () => { - it('should create persistent with CollectionKey (default config)', () => { + it('should create Persistent with CollectionKey (default config)', () => { collection.persist(); expect(collection.persistent).toBeInstanceOf(CollectionPersistent); expect(CollectionPersistent).toHaveBeenCalledWith(collection, { - instantiate: true, + loadValue: true, storageKeys: [], key: collection._key, defaultStorageKey: null, }); }); - it('should create persistent with CollectionKey (specific config)', () => { + it('should create Persistent with CollectionKey (specific config)', () => { collection.persist({ storageKeys: ['test1', 'test2'], loadValue: false, @@ -1841,26 +1842,26 @@ describe('Collection Tests', () => { expect(collection.persistent).toBeInstanceOf(CollectionPersistent); expect(CollectionPersistent).toHaveBeenCalledWith(collection, { - instantiate: false, + loadValue: false, storageKeys: ['test1', 'test2'], key: collection._key, defaultStorageKey: 'test1', }); }); - it('should create persistent with passed Key (default config)', () => { + it('should create Persistent with passed Key (default config)', () => { collection.persist('passedKey'); expect(collection.persistent).toBeInstanceOf(CollectionPersistent); expect(CollectionPersistent).toHaveBeenCalledWith(collection, { - instantiate: true, + loadValue: true, storageKeys: [], key: 'passedKey', defaultStorageKey: null, }); }); - it('should create persistent with passed Key (specific config)', () => { + it('should create Persistent with passed Key (specific config)', () => { collection.persist('passedKey', { storageKeys: ['test1', 'test2'], loadValue: false, @@ -1869,14 +1870,14 @@ describe('Collection Tests', () => { expect(collection.persistent).toBeInstanceOf(CollectionPersistent); expect(CollectionPersistent).toHaveBeenCalledWith(collection, { - instantiate: false, + loadValue: false, storageKeys: ['test1', 'test2'], key: 'passedKey', defaultStorageKey: 'test1', }); }); - it("shouldn't overwrite existing persistent", () => { + it("shouldn't overwrite existing Persistent", () => { const dummyPersistent = new CollectionPersistent(collection); collection.persistent = dummyPersistent; collection.isPersisted = true; @@ -2871,7 +2872,7 @@ describe('Collection Tests', () => { collection.assignData = jest.fn(); }); - it('should assign valid Item to Collection (default config)', () => { + it('should assign valid Item to the Collection (default config)', () => { const response = collection.assignItem(toAddDummyItem2); expect(response).toBeTruthy(); @@ -2893,21 +2894,19 @@ describe('Collection Tests', () => { LogMock.hasNotLogged('warn'); }); - it('should assign valid Item to Collection (config.background = true)', () => { + it('should assign valid Item to the Collection (specific config)', () => { const response = collection.assignItem(toAddDummyItem2, { background: true, + rebuildGroups: false, }); expect(response).toBeTruthy(); expect(collection.size).toBe(2); expect(collection.data).toHaveProperty('dummyItem2'); expect(collection.data['dummyItem2']).toBe(toAddDummyItem2); - expect(collection.rebuildGroupsThatIncludeItemKey).toHaveBeenCalledWith( - 'dummyItem2', - { - background: true, - } - ); + expect( + collection.rebuildGroupsThatIncludeItemKey + ).not.toHaveBeenCalled(); expect(collection.assignData).not.toHaveBeenCalled(); expect(toAddDummyItem2.patch).not.toHaveBeenCalled(); @@ -3034,44 +3033,56 @@ describe('Collection Tests', () => { }; dummyGroup1.rebuild = jest.fn(); + dummyGroup1.trackChange = jest.fn(); dummyGroup2.rebuild = jest.fn(); + dummyGroup2.trackChange = jest.fn(); }); - it('should call ingest on each Group that includes the passed ItemKey (default config)', () => { + it('should rebuild each Group that includes the specified itemKey (default config)', () => { collection.rebuildGroupsThatIncludeItemKey('dummyItem1'); - expect(dummyGroup1.rebuild).toHaveBeenCalledWith({ - background: false, - sideEffects: { - enabled: true, - exclude: [], - }, - storage: false, + // Group 1 + expect(dummyGroup1.rebuild).toHaveBeenCalledWith({}); + expect(dummyGroup1.trackChange).toHaveBeenCalledWith({ + key: 'dummyItem1', + index: 0, + method: TrackedChangeMethod.UPDATE, }); + + // Group 2 expect(dummyGroup2.rebuild).not.toHaveBeenCalled(); + expect(dummyGroup2.trackChange).not.toHaveBeenCalled(); }); - it('should call ingest on each Group that includes the passed ItemKey (specific config)', () => { + it('should rebuild each Group that includes the specified itemKey (specific config)', () => { collection.rebuildGroupsThatIncludeItemKey('dummyItem2', { + key: 'frank', background: true, - sideEffects: { - enabled: false, - }, + force: true, }); + // Group 1 expect(dummyGroup1.rebuild).toHaveBeenCalledWith({ + key: 'frank', background: true, - sideEffects: { - enabled: false, - }, - storage: false, + force: true, + }); + expect(dummyGroup1.trackChange).toHaveBeenCalledWith({ + key: 'dummyItem2', + index: 1, + method: TrackedChangeMethod.UPDATE, }); + + // Group 2 expect(dummyGroup2.rebuild).toHaveBeenCalledWith({ + key: 'frank', background: true, - sideEffects: { - enabled: false, - }, - storage: false, + force: true, + }); + expect(dummyGroup2.trackChange).toHaveBeenCalledWith({ + key: 'dummyItem2', + index: 0, + method: TrackedChangeMethod.UPDATE, }); }); }); 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 d5b6a783..439cff9b 100644 --- a/packages/core/tests/unit/collection/group/group.observer.test.ts +++ b/packages/core/tests/unit/collection/group/group.observer.test.ts @@ -110,31 +110,41 @@ describe('GroupObserver Tests', () => { describe('ingest function tests', () => { beforeEach(() => { - dummyGroup.rebuild = jest.fn(); + groupObserver.ingestOutput = jest.fn(); }); - it('should rebuild the Group and ingests it into the runtime (default config)', () => { + it('should call ingestOutput with nextGroupOutput (default config)', () => { + groupObserver.group().nextGroupOutput = 'jeff' as any; + groupObserver.ingest(); - expect(dummyGroup.rebuild).toHaveBeenCalledWith({}); + expect(groupObserver.ingestOutput).toHaveBeenCalledWith( + groupObserver.group().nextGroupOutput, + {} + ); }); - it('should rebuild the Group and ingests it into the runtime (specific config)', () => { + it('should call ingestOutput with nextGroupOutput (specific config)', () => { + groupObserver.group().nextGroupOutput = 'jeff' as any; + groupObserver.ingest({ background: true, force: true, maxTriesToUpdate: 5, }); - expect(dummyGroup.rebuild).toHaveBeenCalledWith({ - background: true, - force: true, - maxTriesToUpdate: 5, - }); + expect(groupObserver.ingestOutput).toHaveBeenCalledWith( + groupObserver.group().nextGroupOutput, + { + background: true, + force: true, + maxTriesToUpdate: 5, + } + ); }); }); - describe('ingestItems function tests', () => { + describe('ingestOutput function tests', () => { beforeEach(() => { dummyAgile.runtime.ingest = jest.fn(); }); @@ -158,7 +168,7 @@ describe('GroupObserver Tests', () => { }); }); - groupObserver.ingestOutput([dummyItem1, dummyItem2]); + groupObserver.ingestOutput([dummyItem1._value, dummyItem2._value]); expect(groupObserver.nextGroupOutput).toStrictEqual([ dummyItem1._value, @@ -190,7 +200,7 @@ describe('GroupObserver Tests', () => { }); }); - groupObserver.ingestOutput([dummyItem1, dummyItem2], { + groupObserver.ingestOutput([dummyItem1._value, dummyItem2._value], { perform: false, force: true, sideEffects: { @@ -219,7 +229,7 @@ describe('GroupObserver Tests', () => { () => { dummyGroup._output = [dummyItem1._value, dummyItem2._value]; - groupObserver.ingestOutput([dummyItem1, dummyItem2]); + groupObserver.ingestOutput([dummyItem1._value, dummyItem2._value]); expect(groupObserver.nextGroupOutput).toStrictEqual([ dummyItem1._value, @@ -249,7 +259,9 @@ describe('GroupObserver Tests', () => { }); }); - groupObserver.ingestOutput([dummyItem1, dummyItem2], { force: true }); + groupObserver.ingestOutput([dummyItem1._value, dummyItem2._value], { + force: true, + }); expect(groupObserver.nextGroupOutput).toStrictEqual([ dummyItem1._value, @@ -281,7 +293,7 @@ describe('GroupObserver Tests', () => { }); dummyGroup.isPlaceholder = true; - groupObserver.ingestOutput([dummyItem1, dummyItem2]); + groupObserver.ingestOutput([dummyItem1._value, dummyItem2._value]); expect(groupObserver.nextGroupOutput).toStrictEqual([ dummyItem1._value, @@ -312,6 +324,7 @@ describe('GroupObserver Tests', () => { ]; dummyJob.observer.value = [dummyItem1._value]; dummyGroup._output = [dummyItem1._value]; + dummyGroup.nextGroupOutput = [dummyItem1._value]; groupObserver.perform(dummyJob); @@ -319,6 +332,10 @@ describe('GroupObserver Tests', () => { dummyItem1._value, dummyItem2._value, ]); + expect(dummyGroup.nextGroupOutput).toStrictEqual([ + dummyItem1._value, + dummyItem2._value, + ]); expect(groupObserver.value).toStrictEqual([ dummyItem1._value, diff --git a/packages/core/tests/unit/collection/group/group.test.ts b/packages/core/tests/unit/collection/group/group.test.ts index 45ac8d30..6c8e3c1a 100644 --- a/packages/core/tests/unit/collection/group/group.test.ts +++ b/packages/core/tests/unit/collection/group/group.test.ts @@ -8,6 +8,7 @@ import { CollectionPersistent, GroupObserver, EnhancedState, + TrackedChangeMethod, } from '../../../../src'; import { LogMock } from '../../../helper/logMock'; @@ -47,7 +48,10 @@ describe('Group Tests', () => { expect(group.collection()).toBe(dummyCollection); expect(group._output).toStrictEqual([]); + expect(group.nextGroupOutput).toStrictEqual([]); expect(group.notFoundItemKeys).toStrictEqual([]); + expect(group.trackedChanges).toStrictEqual([]); + expect(group.loadedInitialValue).toBeTruthy(); // Check if State was called with correct parameters expect(group._key).toBeUndefined(); @@ -86,7 +90,10 @@ describe('Group Tests', () => { expect(group.collection()).toBe(dummyCollection); expect(group._output).toStrictEqual([]); + expect(group.nextGroupOutput).toStrictEqual([]); expect(group.notFoundItemKeys).toStrictEqual([]); + expect(group.trackedChanges).toStrictEqual([]); + expect(group.loadedInitialValue).toBeTruthy(); // Check if State was called with correct parameters expect(group._key).toBe('dummyKey'); @@ -122,7 +129,10 @@ describe('Group Tests', () => { expect(group.collection()).toBe(dummyCollection); expect(group._output).toStrictEqual([]); + expect(group.nextGroupOutput).toStrictEqual([]); expect(group.notFoundItemKeys).toStrictEqual([]); + expect(group.trackedChanges).toStrictEqual([]); + expect(group.loadedInitialValue).toBeTruthy(); // Check if State was called with correct parameters expect(group._key).toBeUndefined(); @@ -230,117 +240,183 @@ describe('Group Tests', () => { 'dummyItem3Key', ]; group.set = jest.fn(); + group.trackChange = jest.fn(); }); - it('should remove Item from Group not in background (default config)', () => { + it('should remove Item from Group (default config)', () => { group.remove('dummyItem1Key'); + expect(group.trackChange).toHaveBeenCalledTimes(1); + expect(group.trackChange).toHaveBeenCalledWith({ + index: 0, + method: TrackedChangeMethod.REMOVE, + key: 'dummyItem1Key', + }); + expect(group.set).toHaveBeenCalledWith( ['dummyItem2Key', 'dummyItem3Key'], {} ); }); - it('should remove Item from Group in background (config.background = true)', () => { - group.remove('dummyItem1Key', { background: true }); + it('should remove Item from Group (specific config)', () => { + group.remove('dummyItem1Key', { + background: true, + force: true, + storage: false, + softRebuild: false, + }); + + expect(group.trackChange).not.toHaveBeenCalled(); expect(group.set).toHaveBeenCalledWith( ['dummyItem2Key', 'dummyItem3Key'], - { background: true } + { background: true, force: true, storage: false } ); }); - it("shouldn't remove not existing Item from Group (default config)", () => { + it("shouldn't remove not existing Item from Group", () => { group.remove('notExistingKey'); - expect(group.set).not.toHaveBeenCalled(); - }); - - it("should remove Item from Group that doesn't exist in Collection in background (default config)", () => { - group.remove('dummyItem3Key'); + expect(group.trackChange).not.toHaveBeenCalled(); - expect(group.set).toHaveBeenCalledWith( - ['dummyItem1Key', 'dummyItem2Key'], - { background: true } - ); + expect(group.set).not.toHaveBeenCalled(); }); - it('should remove Items from Group not in background (default config)', () => { + it('should remove Items from Group', () => { group.remove(['dummyItem1Key', 'notExistingItemKey', 'dummyItem3Key']); + expect(group.trackChange).toHaveBeenCalledTimes(2); + expect(group.trackChange).toHaveBeenCalledWith({ + index: 0, + method: TrackedChangeMethod.REMOVE, + key: 'dummyItem1Key', + }); + expect(group.trackChange).toHaveBeenCalledWith({ + index: 1, + method: TrackedChangeMethod.REMOVE, + key: 'dummyItem3Key', + }); + expect(group.set).toHaveBeenCalledWith(['dummyItem2Key'], {}); }); - it("should remove Items from Group in background if passing not existing Item and Item that doesn't exist in Collection (default config)", () => { - group.remove(['notExistingItemKey', 'dummyItem3Key']); + it("should remove Item/s from Group that doesn't exist in the Collection in background", () => { + group.remove('dummyItem3Key'); + + expect(group.trackChange).toHaveBeenCalledTimes(1); + expect(group.trackChange).toHaveBeenCalledWith({ + index: 2, + method: TrackedChangeMethod.REMOVE, + key: 'dummyItem3Key', + }); expect(group.set).toHaveBeenCalledWith( ['dummyItem1Key', 'dummyItem2Key'], { background: true } ); }); + + it( + 'should remove Items from Group in background ' + + 'if passing not existing Items to remove ' + + "and Items that doesn't exist in the Collection", + () => { + group.remove(['notExistingItemKey', 'dummyItem3Key']); + + expect(group.trackChange).toHaveBeenCalledTimes(1); + expect(group.trackChange).toHaveBeenCalledWith({ + index: 2, + method: TrackedChangeMethod.REMOVE, + key: 'dummyItem3Key', + }); + + expect(group.set).toHaveBeenCalledWith( + ['dummyItem1Key', 'dummyItem2Key'], + { background: true } + ); + } + ); }); describe('add function tests', () => { beforeEach(() => { group.nextStateValue = ['placeholder', 'dummyItem1Key', 'placeholder']; group.set = jest.fn(); + group.trackChange = jest.fn(); }); - it('should add Item to Group at the end not in background (default config)', () => { + it('should add Item at the end of the Group (default config)', () => { group.add('dummyItem2Key'); + expect(group.trackChange).toHaveBeenCalledTimes(1); + expect(group.trackChange).toHaveBeenCalledWith({ + index: 2, + method: TrackedChangeMethod.ADD, + key: 'dummyItem2Key', + }); + expect(group.set).toHaveBeenCalledWith( ['placeholder', 'dummyItem1Key', 'placeholder', 'dummyItem2Key'], {} ); }); - it("should add Item to Group at the beginning not in background (config.method = 'unshift')", () => { - group.add('dummyItem2Key', { method: 'unshift' }); - - expect(group.set).toHaveBeenCalledWith( - ['dummyItem2Key', 'placeholder', 'dummyItem1Key', 'placeholder'], - {} - ); - }); + it('should add Item at the end of the Group (specific config)', () => { + group.add('dummyItem2Key', { + background: true, + force: true, + storage: false, + softRebuild: false, + }); - it('should add Item to Group at the end in background (config.background = true)', () => { - group.add('dummyItem2Key', { background: true }); + expect(group.trackChange).not.toHaveBeenCalled(); expect(group.set).toHaveBeenCalledWith( ['placeholder', 'dummyItem1Key', 'placeholder', 'dummyItem2Key'], - { background: true } + { background: true, force: true, storage: false } ); }); - it("should add Item to Group at the end that doesn't exist in Collection in background (default config)", () => { - group.add('dummyItem3Key'); + it("should add Item at the beginning of the Group (config.method = 'unshift')", () => { + group.add('dummyItem2Key', { method: 'unshift' }); + + expect(group.trackChange).toHaveBeenCalledTimes(1); + expect(group.trackChange).toHaveBeenCalledWith({ + index: 0, + method: TrackedChangeMethod.ADD, + key: 'dummyItem2Key', + }); expect(group.set).toHaveBeenCalledWith( - ['placeholder', 'dummyItem1Key', 'placeholder', 'dummyItem3Key'], - { background: true } + ['dummyItem2Key', 'placeholder', 'dummyItem1Key', 'placeholder'], + {} ); }); - it("shouldn't add existing Item to Group again (default config)", () => { + it("shouldn't add already existing Item to the Group (default config)", () => { group.add('dummyItem1Key'); - expect(group.set).not.toHaveBeenCalled(); - }); - - it('should remove existingItem and add it again at the end to the Group not in background (config.overwrite = true)', () => { - group.add('dummyItem1Key', { overwrite: true }); + expect(group.trackChange).not.toHaveBeenCalled(); - expect(group.set).toHaveBeenCalledWith( - ['placeholder', 'placeholder', 'dummyItem1Key'], - {} - ); + expect(group.set).not.toHaveBeenCalled(); }); - it('should add Items to Group at the end not in background (default config)', () => { + it('should add Items at the end of the Group', () => { group.add(['dummyItem1Key', 'dummyItem2Key', 'dummyItem3Key']); + expect(group.trackChange).toHaveBeenCalledTimes(2); + expect(group.trackChange).toHaveBeenCalledWith({ + index: 2, + method: TrackedChangeMethod.ADD, + key: 'dummyItem2Key', + }); + expect(group.trackChange).toHaveBeenCalledWith({ + index: 3, + method: TrackedChangeMethod.ADD, + key: 'dummyItem3Key', + }); + expect(group.set).toHaveBeenCalledWith( [ 'placeholder', @@ -353,14 +429,42 @@ describe('Group Tests', () => { ); }); - it('should add Items toGroup at the end in background if passing existing Item and in Collection not existing Item (default config)', () => { - group.add(['dummyItem1Key', 'dummyItem3Key']); + it("should add Item that doesn't exist in Collection at the end of the Group in background", () => { + group.add('dummyItem3Key'); + + expect(group.trackChange).toHaveBeenCalledTimes(1); + expect(group.trackChange).toHaveBeenCalledWith({ + index: 2, + method: TrackedChangeMethod.ADD, + key: 'dummyItem3Key', + }); expect(group.set).toHaveBeenCalledWith( ['placeholder', 'dummyItem1Key', 'placeholder', 'dummyItem3Key'], { background: true } ); }); + + it( + 'should add Items at the end of the Group in background ' + + 'if passing already added Items ' + + "and Items that doesn't exist in the Collection", + () => { + group.add(['dummyItem1Key', 'dummyItem3Key']); + + expect(group.trackChange).toHaveBeenCalledTimes(1); + expect(group.trackChange).toHaveBeenCalledWith({ + index: 2, + method: TrackedChangeMethod.ADD, + key: 'dummyItem3Key', + }); + + expect(group.set).toHaveBeenCalledWith( + ['placeholder', 'dummyItem1Key', 'placeholder', 'dummyItem3Key'], + { background: true } + ); + } + ); }); describe('replace function tests', () => { @@ -533,13 +637,13 @@ describe('Group Tests', () => { }); it('should ingest the built Group output and set notFoundItemKeys to the not found Item Keys (specific config)', () => { - group.rebuild({ storage: true, overwrite: true, background: false }); + group.rebuild({ background: true, force: false }); expect(group.notFoundItemKeys).toStrictEqual(['dummyItem3Key']); expect(group._output).toStrictEqual([]); // because of mocking 'ingestValue' expect(group.observers['output'].ingestOutput).toHaveBeenCalledWith( [dummyItem1, dummyItem2], - { storage: true, overwrite: true, background: false } + { background: true, force: false } ); LogMock.hasLoggedCode( diff --git a/packages/core/tests/unit/state/state.enhanced.test.ts b/packages/core/tests/unit/state/state.enhanced.test.ts index 7c80127c..97afe465 100644 --- a/packages/core/tests/unit/state/state.enhanced.test.ts +++ b/packages/core/tests/unit/state/state.enhanced.test.ts @@ -415,19 +415,19 @@ describe('Enhanced State Tests', () => { }); describe('persist function tests', () => { - it('should create persistent with StateKey (default config)', () => { + it('should create Persistent with StateKey (default config)', () => { numberState.persist(); expect(numberState.persistent).toBeInstanceOf(StatePersistent); expect(StatePersistent).toHaveBeenCalledWith(numberState, { - instantiate: true, + loadValue: true, storageKeys: [], key: numberState._key, defaultStorageKey: null, }); }); - it('should create persistent with StateKey (specific config)', () => { + it('should create Persistent with StateKey (specific config)', () => { numberState.persist({ storageKeys: ['test1', 'test2'], loadValue: false, @@ -436,26 +436,26 @@ describe('Enhanced State Tests', () => { expect(numberState.persistent).toBeInstanceOf(StatePersistent); expect(StatePersistent).toHaveBeenCalledWith(numberState, { - instantiate: false, + loadValue: false, storageKeys: ['test1', 'test2'], key: numberState._key, defaultStorageKey: 'test1', }); }); - it('should create persistent with passed Key (default config)', () => { + it('should create Persistent with passed Key (default config)', () => { numberState.persist('passedKey'); expect(numberState.persistent).toBeInstanceOf(StatePersistent); expect(StatePersistent).toHaveBeenCalledWith(numberState, { - instantiate: true, + loadValue: true, storageKeys: [], key: 'passedKey', defaultStorageKey: null, }); }); - it('should create persistent with passed Key (specific config)', () => { + it('should create Persistent with passed Key (specific config)', () => { numberState.persist('passedKey', { storageKeys: ['test1', 'test2'], loadValue: false, @@ -464,7 +464,7 @@ describe('Enhanced State Tests', () => { expect(numberState.persistent).toBeInstanceOf(StatePersistent); expect(StatePersistent).toHaveBeenCalledWith(numberState, { - instantiate: false, + loadValue: false, storageKeys: ['test1', 'test2'], key: 'passedKey', defaultStorageKey: 'test1', diff --git a/packages/core/tests/unit/state/state.persistent.test.ts b/packages/core/tests/unit/state/state.persistent.test.ts index 5cc5531d..95591c38 100644 --- a/packages/core/tests/unit/state/state.persistent.test.ts +++ b/packages/core/tests/unit/state/state.persistent.test.ts @@ -32,41 +32,44 @@ describe('StatePersistent Tests', () => { jest.clearAllMocks(); }); - it("should create StatePersistent and shouldn't call initialLoading if Persistent isn't ready (default config)", () => { - // Overwrite instantiatePersistent once to not call it and set ready property + it('should create StatePersistent and should call initialLoading if Persistent is ready (default config)', () => { + // Overwrite instantiatePersistent once to not call it jest .spyOn(StatePersistent.prototype, 'instantiatePersistent') .mockImplementationOnce(function () { // @ts-ignore - this.ready = false; + this.ready = true; }); const statePersistent = new StatePersistent(dummyState); expect(statePersistent).toBeInstanceOf(StatePersistent); - expect(statePersistent.state()).toBe(dummyState); + expect(statePersistent.instantiatePersistent).toHaveBeenCalledWith({ key: undefined, storageKeys: [], defaultStorageKey: null, }); - expect(statePersistent.initialLoading).not.toHaveBeenCalled(); + expect(statePersistent.initialLoading).toHaveBeenCalled(); + // Check if Persistent was called with correct parameters expect(statePersistent._key).toBe(StatePersistent.placeHolderKey); - expect(statePersistent.ready).toBeFalsy(); + expect(statePersistent.ready).toBeTruthy(); expect(statePersistent.isPersisted).toBeFalsy(); expect(statePersistent.onLoad).toBeUndefined(); expect(statePersistent.storageKeys).toStrictEqual([]); - expect(statePersistent.config).toStrictEqual({ defaultStorageKey: null }); + expect(statePersistent.config).toStrictEqual({ + defaultStorageKey: null, // gets set in 'instantiatePersistent' which is mocked + }); }); - it("should create StatePersistent and shouldn't call initialLoading if Persistent isn't ready (specific config)", () => { + it('should create StatePersistent and should call initialLoading if Persistent is ready (specific config)', () => { // Overwrite instantiatePersistent once to not call it and set ready property jest .spyOn(StatePersistent.prototype, 'instantiatePersistent') .mockImplementationOnce(function () { // @ts-ignore - this.ready = false; + this.ready = true; }); const statePersistent = new StatePersistent(dummyState, { @@ -81,10 +84,11 @@ describe('StatePersistent Tests', () => { storageKeys: ['test1', 'test2'], defaultStorageKey: 'test2', }); - expect(statePersistent.initialLoading).not.toHaveBeenCalled(); + expect(statePersistent.initialLoading).toHaveBeenCalled(); + // Check if Persistent was called with correct parameters expect(statePersistent._key).toBe(StatePersistent.placeHolderKey); - expect(statePersistent.ready).toBeFalsy(); + expect(statePersistent.ready).toBeTruthy(); expect(statePersistent.isPersisted).toBeFalsy(); expect(statePersistent.onLoad).toBeUndefined(); expect(statePersistent.storageKeys).toStrictEqual([]); @@ -93,21 +97,36 @@ describe('StatePersistent Tests', () => { }); }); - it('should create StatePersistent and should call initialLoading if Persistent is ready (default config)', () => { - // Overwrite instantiatePersistent once to not call it + it("should create StatePersistent and shouldn't call initialLoading if Persistent isn't ready", () => { + // Overwrite instantiatePersistent once to not call it and set ready property jest .spyOn(StatePersistent.prototype, 'instantiatePersistent') .mockImplementationOnce(function () { // @ts-ignore - this.ready = true; + this.ready = false; }); const statePersistent = new StatePersistent(dummyState); - expect(statePersistent.initialLoading).toHaveBeenCalled(); + expect(statePersistent).toBeInstanceOf(StatePersistent); + expect(statePersistent.state()).toBe(dummyState); + expect(statePersistent.instantiatePersistent).toHaveBeenCalledWith({ + key: undefined, + storageKeys: [], + defaultStorageKey: null, + }); + expect(statePersistent.initialLoading).not.toHaveBeenCalled(); + + // Check if Persistent was called with correct parameters + expect(statePersistent._key).toBe(StatePersistent.placeHolderKey); + expect(statePersistent.ready).toBeFalsy(); + expect(statePersistent.isPersisted).toBeFalsy(); + expect(statePersistent.onLoad).toBeUndefined(); + expect(statePersistent.storageKeys).toStrictEqual([]); + expect(statePersistent.config).toStrictEqual({ defaultStorageKey: null }); }); - it("should create StatePersistent and shouldn't call initialLoading if Persistent is ready (config.instantiate = false)", () => { + it("should create StatePersistent and shouldn't call initialLoading if Persistent is ready (config.loadValue = false)", () => { // Overwrite instantiatePersistent once to not call it and set ready property jest .spyOn(StatePersistent.prototype, 'instantiatePersistent') @@ -120,7 +139,22 @@ describe('StatePersistent Tests', () => { loadValue: false, }); + expect(statePersistent).toBeInstanceOf(StatePersistent); + expect(statePersistent.state()).toBe(dummyState); + expect(statePersistent.instantiatePersistent).toHaveBeenCalledWith({ + key: undefined, + storageKeys: [], + defaultStorageKey: null, + }); expect(statePersistent.initialLoading).not.toHaveBeenCalled(); + + // Check if Persistent was called with correct parameters + expect(statePersistent._key).toBe(StatePersistent.placeHolderKey); + expect(statePersistent.ready).toBeTruthy(); + expect(statePersistent.isPersisted).toBeFalsy(); + expect(statePersistent.onLoad).toBeUndefined(); + expect(statePersistent.storageKeys).toStrictEqual([]); + expect(statePersistent.config).toStrictEqual({ defaultStorageKey: null }); }); describe('StatePersistent Function Tests', () => { From f00c1c7cb62f92e4336c7c6e10983d22793e440b Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Fri, 10 Sep 2021 17:15:48 +0200 Subject: [PATCH 86/93] fixed tests --- benchmark/package.json | 2 +- benchmark/yarn.lock | 8 +- .../develop/simple-todo-list/src/core.js | 2 +- packages/core/src/collection/collection.ts | 14 +- .../src/collection/group/group.observer.ts | 20 +- packages/core/src/collection/group/index.ts | 44 ++-- packages/core/src/runtime/runtime.job.ts | 6 + packages/core/src/state/state.observer.ts | 24 +- packages/core/src/state/state.runtime.job.ts | 2 + .../tests/unit/collection/collection.test.ts | 64 ++--- .../collection/group/group.observer.test.ts | 4 + .../tests/unit/collection/group/group.test.ts | 220 ++++++++++-------- .../tests/unit/runtime/runtime.job.test.ts | 5 + .../tests/unit/state/state.observer.test.ts | 4 + .../unit/state/state.runtime.job.test.ts | 5 + 15 files changed, 231 insertions(+), 193 deletions(-) diff --git a/benchmark/package.json b/benchmark/package.json index 21852877..006b4707 100644 --- a/benchmark/package.json +++ b/benchmark/package.json @@ -24,7 +24,7 @@ "@agile-ts/core": "file:.yalc/@agile-ts/core", "@agile-ts/logger": "file:.yalc/@agile-ts/logger", "@agile-ts/react": "file:.yalc/@agile-ts/react", - "@hookstate/core": "^3.0.8", + "@hookstate/core": "^3.0.11", "@pulsejs/core": "^4.0.0-beta.3", "@pulsejs/react": "^4.0.0-beta.3", "@reduxjs/toolkit": "^1.6.0", diff --git a/benchmark/yarn.lock b/benchmark/yarn.lock index a7d7c012..fdae0a71 100644 --- a/benchmark/yarn.lock +++ b/benchmark/yarn.lock @@ -27,10 +27,10 @@ dependencies: regenerator-runtime "^0.13.4" -"@hookstate/core@^3.0.8": - version "3.0.8" - resolved "https://registry.yarnpkg.com/@hookstate/core/-/core-3.0.8.tgz#d6838153d6d43c2f35cfca475c31248192564e62" - integrity sha512-blQagGIVIbNoUiNCRrvaXqFmUe7WGMY35ok/LENfl2pcIsLBjkreYIZiaSFi83tkycwq7ZOmcQz/R1nvLKhH8w== +"@hookstate/core@^3.0.11": + version "3.0.11" + resolved "https://registry.yarnpkg.com/@hookstate/core/-/core-3.0.11.tgz#515f2748b3741f3d7e90cbc1acff8d6ac84e5494" + integrity sha512-cbI711aFWX4d8+xkLxikmLnR+f55ePHrBMWFA4gTLEoCa1+Cg0pfNw7h7zSodYMeYt8Y5A5TVSh7a5p8lBC89A== "@pulsejs/core@^4.0.0-beta.3": version "4.0.0-beta.3" diff --git a/examples/react/develop/simple-todo-list/src/core.js b/examples/react/develop/simple-todo-list/src/core.js index 885a77f8..316d5f31 100644 --- a/examples/react/develop/simple-todo-list/src/core.js +++ b/examples/react/develop/simple-todo-list/src/core.js @@ -6,4 +6,4 @@ assignSharedAgileLoggerConfig({ level: Logger.level.DEBUG }); // Create Collection export const TODOS = createCollection({ initialData: [{ id: 1, name: 'Clean Bathroom' }], -}).persist('todos'); // perist does store the Collection in the Local Storage +}).persist('todos'); // persist does store the Collection in the Local Storage diff --git a/packages/core/src/collection/collection.ts b/packages/core/src/collection/collection.ts index f6107554..ba1845f0 100644 --- a/packages/core/src/collection/collection.ts +++ b/packages/core/src/collection/collection.ts @@ -1469,13 +1469,13 @@ export class Collection< for (const groupKey of Object.keys(this.groups)) { const group = this.getGroup(groupKey); if (group != null && group.has(itemKey)) { - group.trackChange({ - key: itemKey, - index: group.nextStateValue.findIndex((ik) => itemKey === ik), - method: TrackedChangeMethod.UPDATE, - }); - - group.rebuild(config); + group.rebuild(config, [ + { + key: itemKey, + index: group.nextStateValue.findIndex((ik) => itemKey === ik), + method: TrackedChangeMethod.UPDATE, + }, + ]); } } } diff --git a/packages/core/src/collection/group/group.observer.ts b/packages/core/src/collection/group/group.observer.ts index 0029ecd1..3e5d490a 100644 --- a/packages/core/src/collection/group/group.observer.ts +++ b/packages/core/src/collection/group/group.observer.ts @@ -9,6 +9,7 @@ import { IngestConfigInterface, CreateRuntimeJobConfigInterface, defineConfig, + removeProperties, } from '../../internal'; export class GroupObserver extends Observer { @@ -72,13 +73,7 @@ export class GroupObserver extends Observer { const group = this.group(); config = defineConfig(config, { perform: true, - background: false, - sideEffects: { - enabled: true, - exclude: [], - }, force: false, - maxTriesToUpdate: 3, }); // Force overwriting the Group value if it is a placeholder. @@ -95,13 +90,12 @@ export class GroupObserver extends Observer { // Create Runtime-Job const job = new RuntimeJob(this, { - sideEffects: config.sideEffects, - force: config.force, - background: config.background, - key: - config.key ?? - `${this._key != null ? this._key + '_' : ''}${generateId()}_output`, - maxTriesToUpdate: config.maxTriesToUpdate, + ...removeProperties(config, ['perform']), + ...{ + key: + config.key ?? + `${this._key != null ? this._key + '_' : ''}${generateId()}_output`, + }, }); // Pass created Job into the Runtime diff --git a/packages/core/src/collection/group/index.ts b/packages/core/src/collection/group/index.ts index b0a5ac99..63e749a9 100644 --- a/packages/core/src/collection/group/index.ts +++ b/packages/core/src/collection/group/index.ts @@ -42,14 +42,6 @@ export class Group< // Keeps track of all Item identifiers for Items that couldn't be found in the Collection public notFoundItemKeys: Array = []; - // Keeps track of all changes made between rebuilds (add, remove, update). - // Why not rebuilding the Group directly in the add(), remove() methods? - // Because rebuilding the Group is also a side effect of the Group. - // Thus a rebuild should always happen whenever the Group mutates. - // (-> Simplicity and keeping the current structure to not rewrite all tests) - // Note: Changes array must be processed from front to back otherwise issues might arise!! - public trackedChanges: TrackedChangeInterface[] = []; - // Whether the initial value was loaded from the corresponding Persistent // https://github.com/agile-ts/agile/issues/155 public loadedInitialValue = true; @@ -87,7 +79,9 @@ export class Group< // Add side effect to Group // that rebuilds the Group whenever the Group value changes - this.addSideEffect(Group.rebuildGroupSideEffectKey, () => this.rebuild()); + this.addSideEffect(Group.rebuildGroupSideEffectKey, (state, config) => { + this.rebuild(config, config?.any?.trackedChanges || []); + }); // Initial rebuild this.rebuild(); @@ -153,7 +147,9 @@ export class Group< let newGroupValue = copy(this.nextStateValue); config = defineConfig(config, { softRebuild: true, + any: {}, }); + config.any['trackedChanges'] = []; // TODO might be improved since the 'any' property is very vague // Remove itemKeys from Group _itemKeys.forEach((itemKey) => { @@ -166,8 +162,9 @@ export class Group< return; } + // Track changes to soft rebuild the Group when rebuilding the Group in a side effect if (config.softRebuild) { - this.trackChange({ + config.any['trackedChanges'].push({ index: newGroupValue.findIndex((ik) => ik === itemKey), method: TrackedChangeMethod.REMOVE, key: itemKey, @@ -215,7 +212,9 @@ export class Group< config = defineConfig(config, { method: 'push', softRebuild: true, + any: {}, }); + config.any['trackedChanges'] = []; // TODO might be improved since the 'any' property is very vague // Add itemKeys to Group _itemKeys.forEach((itemKey) => { @@ -231,12 +230,12 @@ export class Group< return; } - // Track changes to soft rebuild the Group when rebuilding the Group + // Track changes to soft rebuild the Group when rebuilding the Group in a side effect if (config.softRebuild) { - this.trackChange({ + config.any['trackedChanges'].push({ + index: config.method === 'push' ? newGroupValue.length - 1 : 0, method: TrackedChangeMethod.ADD, key: itemKey, - index: config.method === 'push' ? newGroupValue.length - 1 : 0, }); } @@ -377,16 +376,20 @@ export class Group< * * @internal * @param config - Configuration object + * @param trackedChanges - Changes that were made between two rebuilds. */ - public rebuild(config: GroupIngestConfigInterface = {}): this { + public rebuild( + config: GroupIngestConfigInterface = {}, + trackedChanges: TrackedChangeInterface[] = [] + ): this { // Don't rebuild Group if Collection isn't correctly instantiated yet // (because only after a successful instantiation the Collection // contains the Items which are essential for a proper rebuild) if (!this.collection().isInstantiated) return this; // Soft rebuild the Collection (-> rebuild only parts of the Collection) - if (this.trackedChanges.length > 0) { - this.trackedChanges.forEach((change) => { + if (trackedChanges.length > 0) { + trackedChanges.forEach((change) => { const item = this.collection().getItem(change.key); switch (change.method) { @@ -409,7 +412,6 @@ export class Group< break; } }); - this.trackedChanges = []; this.observers['output'].ingest(config); return this; } @@ -447,14 +449,6 @@ export class Group< return this; } - - /** - * TODO - * @param change - */ - public trackChange(change: TrackedChangeInterface) { - this.trackedChanges.push(change); - } } export type GroupKey = string | number; diff --git a/packages/core/src/runtime/runtime.job.ts b/packages/core/src/runtime/runtime.job.ts index 54748c87..73dbd4b3 100644 --- a/packages/core/src/runtime/runtime.job.ts +++ b/packages/core/src/runtime/runtime.job.ts @@ -40,6 +40,7 @@ export class RuntimeJob { }, force: false, maxTriesToUpdate: 3, + any: {}, }); this.config = { @@ -47,6 +48,7 @@ export class RuntimeJob { force: config.force, sideEffects: config.sideEffects, maxTriesToUpdate: config.maxTriesToUpdate, + any: config.any, }; this.observer = observer; this.rerender = @@ -115,6 +117,10 @@ export interface RuntimeJobConfigInterface { * @default 3 */ maxTriesToUpdate?: number | null; + /** + * Anything unrelated that might be required by a side effect. + */ + any?: any; } export interface SideEffectConfigInterface { diff --git a/packages/core/src/state/state.observer.ts b/packages/core/src/state/state.observer.ts index 6994237c..332a9f9e 100644 --- a/packages/core/src/state/state.observer.ts +++ b/packages/core/src/state/state.observer.ts @@ -14,6 +14,7 @@ import { SubscriptionContainer, ObserverKey, defineConfig, + removeProperties, } from '../internal'; export class StateObserver extends Observer { @@ -87,15 +88,7 @@ export class StateObserver extends Observer { const state = this.state(); config = defineConfig(config, { perform: true, - background: false, - sideEffects: { - enabled: true, - exclude: [], - }, force: false, - storage: true, - overwrite: false, - maxTriesToUpdate: 3, }); // Force overwriting the State value if it is a placeholder. @@ -115,15 +108,12 @@ export class StateObserver extends Observer { // Create Runtime-Job const job = new StateRuntimeJob(this, { - storage: config.storage, - sideEffects: config.sideEffects, - force: config.force, - background: config.background, - overwrite: config.overwrite, - key: - config.key ?? - `${this._key != null ? this._key + '_' : ''}${generateId()}_value`, - maxTriesToUpdate: config.maxTriesToUpdate, + ...removeProperties(config, ['perform']), + ...{ + key: + config.key ?? + `${this._key != null ? this._key + '_' : ''}${generateId()}_value`, + }, }); // Pass created Job into the Runtime diff --git a/packages/core/src/state/state.runtime.job.ts b/packages/core/src/state/state.runtime.job.ts index 16e1628a..2578c7f9 100644 --- a/packages/core/src/state/state.runtime.job.ts +++ b/packages/core/src/state/state.runtime.job.ts @@ -35,6 +35,7 @@ export class StateRuntimeJob extends RuntimeJob { storage: true, overwrite: false, maxTriesToUpdate: 3, + any: {}, }); this.config = { @@ -44,6 +45,7 @@ export class StateRuntimeJob extends RuntimeJob { storage: config.storage, overwrite: config.overwrite, maxTriesToUpdate: config.maxTriesToUpdate, + any: config.any, }; } } diff --git a/packages/core/tests/unit/collection/collection.test.ts b/packages/core/tests/unit/collection/collection.test.ts index 83d9671e..463a092a 100644 --- a/packages/core/tests/unit/collection/collection.test.ts +++ b/packages/core/tests/unit/collection/collection.test.ts @@ -3033,25 +3033,23 @@ describe('Collection Tests', () => { }; dummyGroup1.rebuild = jest.fn(); - dummyGroup1.trackChange = jest.fn(); dummyGroup2.rebuild = jest.fn(); - dummyGroup2.trackChange = jest.fn(); }); it('should rebuild each Group that includes the specified itemKey (default config)', () => { collection.rebuildGroupsThatIncludeItemKey('dummyItem1'); // Group 1 - expect(dummyGroup1.rebuild).toHaveBeenCalledWith({}); - expect(dummyGroup1.trackChange).toHaveBeenCalledWith({ - key: 'dummyItem1', - index: 0, - method: TrackedChangeMethod.UPDATE, - }); + expect(dummyGroup1.rebuild).toHaveBeenCalledWith({}, [ + { + key: 'dummyItem1', + index: 0, + method: TrackedChangeMethod.UPDATE, + }, + ]); // Group 2 expect(dummyGroup2.rebuild).not.toHaveBeenCalled(); - expect(dummyGroup2.trackChange).not.toHaveBeenCalled(); }); it('should rebuild each Group that includes the specified itemKey (specific config)', () => { @@ -3062,28 +3060,36 @@ describe('Collection Tests', () => { }); // Group 1 - expect(dummyGroup1.rebuild).toHaveBeenCalledWith({ - key: 'frank', - background: true, - force: true, - }); - expect(dummyGroup1.trackChange).toHaveBeenCalledWith({ - key: 'dummyItem2', - index: 1, - method: TrackedChangeMethod.UPDATE, - }); + expect(dummyGroup1.rebuild).toHaveBeenCalledWith( + { + key: 'frank', + background: true, + force: true, + }, + [ + { + key: 'dummyItem2', + index: 1, + method: TrackedChangeMethod.UPDATE, + }, + ] + ); // Group 2 - expect(dummyGroup2.rebuild).toHaveBeenCalledWith({ - key: 'frank', - background: true, - force: true, - }); - expect(dummyGroup2.trackChange).toHaveBeenCalledWith({ - key: 'dummyItem2', - index: 0, - method: TrackedChangeMethod.UPDATE, - }); + expect(dummyGroup2.rebuild).toHaveBeenCalledWith( + { + key: 'frank', + background: true, + force: true, + }, + [ + { + key: 'dummyItem2', + index: 0, + method: TrackedChangeMethod.UPDATE, + }, + ] + ); }); }); }); 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 439cff9b..24b30205 100644 --- a/packages/core/tests/unit/collection/group/group.observer.test.ts +++ b/packages/core/tests/unit/collection/group/group.observer.test.ts @@ -165,6 +165,7 @@ describe('GroupObserver Tests', () => { }, force: false, maxTriesToUpdate: 3, + any: {}, }); }); @@ -197,6 +198,7 @@ describe('GroupObserver Tests', () => { }, force: true, maxTriesToUpdate: 5, + any: {}, }); }); @@ -256,6 +258,7 @@ describe('GroupObserver Tests', () => { }, force: true, maxTriesToUpdate: 3, + any: {}, }); }); @@ -289,6 +292,7 @@ describe('GroupObserver Tests', () => { }, force: true, maxTriesToUpdate: 3, + any: {}, }); }); dummyGroup.isPlaceholder = true; diff --git a/packages/core/tests/unit/collection/group/group.test.ts b/packages/core/tests/unit/collection/group/group.test.ts index 6c8e3c1a..377ad586 100644 --- a/packages/core/tests/unit/collection/group/group.test.ts +++ b/packages/core/tests/unit/collection/group/group.test.ts @@ -50,7 +50,6 @@ describe('Group Tests', () => { expect(group._output).toStrictEqual([]); expect(group.nextGroupOutput).toStrictEqual([]); expect(group.notFoundItemKeys).toStrictEqual([]); - expect(group.trackedChanges).toStrictEqual([]); expect(group.loadedInitialValue).toBeTruthy(); // Check if State was called with correct parameters @@ -92,7 +91,6 @@ describe('Group Tests', () => { expect(group._output).toStrictEqual([]); expect(group.nextGroupOutput).toStrictEqual([]); expect(group.notFoundItemKeys).toStrictEqual([]); - expect(group.trackedChanges).toStrictEqual([]); expect(group.loadedInitialValue).toBeTruthy(); // Check if State was called with correct parameters @@ -131,7 +129,6 @@ describe('Group Tests', () => { expect(group._output).toStrictEqual([]); expect(group.nextGroupOutput).toStrictEqual([]); expect(group.notFoundItemKeys).toStrictEqual([]); - expect(group.trackedChanges).toStrictEqual([]); expect(group.loadedInitialValue).toBeTruthy(); // Check if State was called with correct parameters @@ -240,22 +237,24 @@ describe('Group Tests', () => { 'dummyItem3Key', ]; group.set = jest.fn(); - group.trackChange = jest.fn(); }); it('should remove Item from Group (default config)', () => { group.remove('dummyItem1Key'); - expect(group.trackChange).toHaveBeenCalledTimes(1); - expect(group.trackChange).toHaveBeenCalledWith({ - index: 0, - method: TrackedChangeMethod.REMOVE, - key: 'dummyItem1Key', - }); - expect(group.set).toHaveBeenCalledWith( ['dummyItem2Key', 'dummyItem3Key'], - {} + { + any: { + trackedChanges: [ + { + index: 0, + method: TrackedChangeMethod.REMOVE, + key: 'dummyItem1Key', + }, + ], + }, + } ); }); @@ -267,53 +266,61 @@ describe('Group Tests', () => { softRebuild: false, }); - expect(group.trackChange).not.toHaveBeenCalled(); - expect(group.set).toHaveBeenCalledWith( ['dummyItem2Key', 'dummyItem3Key'], - { background: true, force: true, storage: false } + { + background: true, + force: true, + storage: false, + any: { trackedChanges: [] }, + } ); }); it("shouldn't remove not existing Item from Group", () => { group.remove('notExistingKey'); - expect(group.trackChange).not.toHaveBeenCalled(); - expect(group.set).not.toHaveBeenCalled(); }); it('should remove Items from Group', () => { group.remove(['dummyItem1Key', 'notExistingItemKey', 'dummyItem3Key']); - expect(group.trackChange).toHaveBeenCalledTimes(2); - expect(group.trackChange).toHaveBeenCalledWith({ - index: 0, - method: TrackedChangeMethod.REMOVE, - key: 'dummyItem1Key', - }); - expect(group.trackChange).toHaveBeenCalledWith({ - index: 1, - method: TrackedChangeMethod.REMOVE, - key: 'dummyItem3Key', + expect(group.set).toHaveBeenCalledWith(['dummyItem2Key'], { + any: { + trackedChanges: [ + { + index: 0, + method: TrackedChangeMethod.REMOVE, + key: 'dummyItem1Key', + }, + { + index: 1, + method: TrackedChangeMethod.REMOVE, + key: 'dummyItem3Key', + }, + ], + }, }); - - expect(group.set).toHaveBeenCalledWith(['dummyItem2Key'], {}); }); it("should remove Item/s from Group that doesn't exist in the Collection in background", () => { group.remove('dummyItem3Key'); - expect(group.trackChange).toHaveBeenCalledTimes(1); - expect(group.trackChange).toHaveBeenCalledWith({ - index: 2, - method: TrackedChangeMethod.REMOVE, - key: 'dummyItem3Key', - }); - expect(group.set).toHaveBeenCalledWith( ['dummyItem1Key', 'dummyItem2Key'], - { background: true } + { + background: true, + any: { + trackedChanges: [ + { + index: 2, + method: TrackedChangeMethod.REMOVE, + key: 'dummyItem3Key', + }, + ], + }, + } ); }); @@ -324,16 +331,20 @@ describe('Group Tests', () => { () => { group.remove(['notExistingItemKey', 'dummyItem3Key']); - expect(group.trackChange).toHaveBeenCalledTimes(1); - expect(group.trackChange).toHaveBeenCalledWith({ - index: 2, - method: TrackedChangeMethod.REMOVE, - key: 'dummyItem3Key', - }); - expect(group.set).toHaveBeenCalledWith( ['dummyItem1Key', 'dummyItem2Key'], - { background: true } + { + background: true, + any: { + trackedChanges: [ + { + index: 2, + method: TrackedChangeMethod.REMOVE, + key: 'dummyItem3Key', + }, + ], + }, + } ); } ); @@ -343,22 +354,24 @@ describe('Group Tests', () => { beforeEach(() => { group.nextStateValue = ['placeholder', 'dummyItem1Key', 'placeholder']; group.set = jest.fn(); - group.trackChange = jest.fn(); }); it('should add Item at the end of the Group (default config)', () => { group.add('dummyItem2Key'); - expect(group.trackChange).toHaveBeenCalledTimes(1); - expect(group.trackChange).toHaveBeenCalledWith({ - index: 2, - method: TrackedChangeMethod.ADD, - key: 'dummyItem2Key', - }); - expect(group.set).toHaveBeenCalledWith( ['placeholder', 'dummyItem1Key', 'placeholder', 'dummyItem2Key'], - {} + { + any: { + trackedChanges: [ + { + index: 2, + method: TrackedChangeMethod.ADD, + key: 'dummyItem2Key', + }, + ], + }, + } ); }); @@ -370,53 +383,45 @@ describe('Group Tests', () => { softRebuild: false, }); - expect(group.trackChange).not.toHaveBeenCalled(); - expect(group.set).toHaveBeenCalledWith( ['placeholder', 'dummyItem1Key', 'placeholder', 'dummyItem2Key'], - { background: true, force: true, storage: false } + { + background: true, + force: true, + storage: false, + any: { trackedChanges: [] }, + } ); }); it("should add Item at the beginning of the Group (config.method = 'unshift')", () => { group.add('dummyItem2Key', { method: 'unshift' }); - expect(group.trackChange).toHaveBeenCalledTimes(1); - expect(group.trackChange).toHaveBeenCalledWith({ - index: 0, - method: TrackedChangeMethod.ADD, - key: 'dummyItem2Key', - }); - expect(group.set).toHaveBeenCalledWith( ['dummyItem2Key', 'placeholder', 'dummyItem1Key', 'placeholder'], - {} + { + any: { + trackedChanges: [ + { + index: 0, + method: TrackedChangeMethod.ADD, + key: 'dummyItem2Key', + }, + ], + }, + } ); }); it("shouldn't add already existing Item to the Group (default config)", () => { group.add('dummyItem1Key'); - expect(group.trackChange).not.toHaveBeenCalled(); - expect(group.set).not.toHaveBeenCalled(); }); it('should add Items at the end of the Group', () => { group.add(['dummyItem1Key', 'dummyItem2Key', 'dummyItem3Key']); - expect(group.trackChange).toHaveBeenCalledTimes(2); - expect(group.trackChange).toHaveBeenCalledWith({ - index: 2, - method: TrackedChangeMethod.ADD, - key: 'dummyItem2Key', - }); - expect(group.trackChange).toHaveBeenCalledWith({ - index: 3, - method: TrackedChangeMethod.ADD, - key: 'dummyItem3Key', - }); - expect(group.set).toHaveBeenCalledWith( [ 'placeholder', @@ -425,23 +430,42 @@ describe('Group Tests', () => { 'dummyItem2Key', 'dummyItem3Key', ], - {} + { + any: { + trackedChanges: [ + { + index: 2, + method: TrackedChangeMethod.ADD, + key: 'dummyItem2Key', + }, + { + index: 3, + method: TrackedChangeMethod.ADD, + key: 'dummyItem3Key', + }, + ], + }, + } ); }); it("should add Item that doesn't exist in Collection at the end of the Group in background", () => { group.add('dummyItem3Key'); - expect(group.trackChange).toHaveBeenCalledTimes(1); - expect(group.trackChange).toHaveBeenCalledWith({ - index: 2, - method: TrackedChangeMethod.ADD, - key: 'dummyItem3Key', - }); - expect(group.set).toHaveBeenCalledWith( ['placeholder', 'dummyItem1Key', 'placeholder', 'dummyItem3Key'], - { background: true } + { + background: true, + any: { + trackedChanges: [ + { + index: 2, + method: TrackedChangeMethod.ADD, + key: 'dummyItem3Key', + }, + ], + }, + } ); }); @@ -452,16 +476,20 @@ describe('Group Tests', () => { () => { group.add(['dummyItem1Key', 'dummyItem3Key']); - expect(group.trackChange).toHaveBeenCalledTimes(1); - expect(group.trackChange).toHaveBeenCalledWith({ - index: 2, - method: TrackedChangeMethod.ADD, - key: 'dummyItem3Key', - }); - expect(group.set).toHaveBeenCalledWith( ['placeholder', 'dummyItem1Key', 'placeholder', 'dummyItem3Key'], - { background: true } + { + background: true, + any: { + trackedChanges: [ + { + index: 2, + method: TrackedChangeMethod.ADD, + key: 'dummyItem3Key', + }, + ], + }, + } ); } ); diff --git a/packages/core/tests/unit/runtime/runtime.job.test.ts b/packages/core/tests/unit/runtime/runtime.job.test.ts index 9d0845f2..a93c132c 100644 --- a/packages/core/tests/unit/runtime/runtime.job.test.ts +++ b/packages/core/tests/unit/runtime/runtime.job.test.ts @@ -36,6 +36,7 @@ describe('RuntimeJob Tests', () => { }, force: false, maxTriesToUpdate: 3, + any: {}, }); expect(job.rerender).toBeTruthy(); expect(job.performed).toBeFalsy(); @@ -60,6 +61,7 @@ describe('RuntimeJob Tests', () => { }, force: true, maxTriesToUpdate: 10, + any: { jeff: 'frank' }, }); expect(job._key).toBe('dummyJob'); @@ -72,6 +74,7 @@ describe('RuntimeJob Tests', () => { }, force: true, maxTriesToUpdate: 10, + any: { jeff: 'frank' }, }); expect(job.rerender).toBeTruthy(); expect(job.performed).toBeFalsy(); @@ -97,6 +100,7 @@ describe('RuntimeJob Tests', () => { }, force: false, maxTriesToUpdate: 3, + any: {}, }); expect(job.rerender).toBeFalsy(); expect(job.performed).toBeFalsy(); @@ -124,6 +128,7 @@ describe('RuntimeJob Tests', () => { }, force: false, maxTriesToUpdate: 3, + any: {}, }); expect(job.rerender).toBeFalsy(); expect(job.performed).toBeFalsy(); diff --git a/packages/core/tests/unit/state/state.observer.test.ts b/packages/core/tests/unit/state/state.observer.test.ts index e6e64367..2c348943 100644 --- a/packages/core/tests/unit/state/state.observer.test.ts +++ b/packages/core/tests/unit/state/state.observer.test.ts @@ -183,6 +183,7 @@ describe('StateObserver Tests', () => { storage: true, overwrite: false, maxTriesToUpdate: 3, + any: {}, }); }); @@ -214,6 +215,7 @@ describe('StateObserver Tests', () => { storage: true, overwrite: true, maxTriesToUpdate: 5, + any: {}, }); }); @@ -270,6 +272,7 @@ describe('StateObserver Tests', () => { storage: true, overwrite: false, maxTriesToUpdate: 3, + any: {}, }); }); @@ -300,6 +303,7 @@ describe('StateObserver Tests', () => { storage: true, overwrite: true, maxTriesToUpdate: 3, + any: {}, }); }); dummyState.isPlaceholder = true; diff --git a/packages/core/tests/unit/state/state.runtime.job.test.ts b/packages/core/tests/unit/state/state.runtime.job.test.ts index fc2d2461..63bb50a8 100644 --- a/packages/core/tests/unit/state/state.runtime.job.test.ts +++ b/packages/core/tests/unit/state/state.runtime.job.test.ts @@ -48,6 +48,7 @@ describe('RuntimeJob Tests', () => { storage: true, overwrite: false, maxTriesToUpdate: 3, + any: {}, }); expect(job.rerender).toBeTruthy(); expect(job.performed).toBeFalsy(); @@ -70,6 +71,7 @@ describe('RuntimeJob Tests', () => { }, force: true, maxTriesToUpdate: 5, + any: { jeff: 'frank' }, }); expect(job._key).toBe('dummyJob'); @@ -83,6 +85,7 @@ describe('RuntimeJob Tests', () => { storage: true, overwrite: false, maxTriesToUpdate: 5, + any: { jeff: 'frank' }, }); expect(job.rerender).toBeTruthy(); expect(job.performed).toBeFalsy(); @@ -110,6 +113,7 @@ describe('RuntimeJob Tests', () => { storage: true, overwrite: false, maxTriesToUpdate: 3, + any: {}, }); expect(job.rerender).toBeFalsy(); expect(job.performed).toBeFalsy(); @@ -139,6 +143,7 @@ describe('RuntimeJob Tests', () => { storage: true, overwrite: false, maxTriesToUpdate: 3, + any: {}, }); expect(job.rerender).toBeFalsy(); expect(job.performed).toBeFalsy(); From 3e4fa1bf8b01d77716a39a43d6f83f4b0f308ae4 Mon Sep 17 00:00:00 2001 From: BennoDev Date: Sat, 11 Sep 2021 09:33:49 +0200 Subject: [PATCH 87/93] fixed typos --- packages/core/src/collection/collection.ts | 23 +-- packages/core/src/collection/group/index.ts | 42 +++-- .../tests/unit/collection/collection.test.ts | 49 +++--- .../tests/unit/collection/group/group.test.ts | 154 +++++++++++------- 4 files changed, 150 insertions(+), 118 deletions(-) diff --git a/packages/core/src/collection/collection.ts b/packages/core/src/collection/collection.ts index ba1845f0..ff552fbf 100644 --- a/packages/core/src/collection/collection.ts +++ b/packages/core/src/collection/collection.ts @@ -107,7 +107,7 @@ export class Collection< // Rebuild of Groups // Not necessary because if Items are added to the Collection, // (after 'isInstantiated = true') - // the Groups which contain these added Items are rebuilt. + // the Groups which contain these added Items get rebuilt. // for (const key in this.groups) this.groups[key].rebuild(); } @@ -1218,9 +1218,7 @@ export class Collection< * @public * @param itemKeys - Item/s with identifier/s to be removed. */ - public remove( - itemKeys: ItemKey | Array - ): { + public remove(itemKeys: ItemKey | Array): { fromGroups: (groups: Array | ItemKey) => Collection; everywhere: (config?: RemoveItemsConfigInterface) => Collection; } { @@ -1469,13 +1467,16 @@ export class Collection< for (const groupKey of Object.keys(this.groups)) { const group = this.getGroup(groupKey); if (group != null && group.has(itemKey)) { - group.rebuild(config, [ - { - key: itemKey, - index: group.nextStateValue.findIndex((ik) => itemKey === ik), - method: TrackedChangeMethod.UPDATE, - }, - ]); + group.rebuild( + [ + { + key: itemKey, + index: group.nextStateValue.findIndex((ik) => itemKey === ik), + method: TrackedChangeMethod.UPDATE, + }, + ], + config + ); } } } diff --git a/packages/core/src/collection/group/index.ts b/packages/core/src/collection/group/index.ts index 63e749a9..5cb957b7 100644 --- a/packages/core/src/collection/group/index.ts +++ b/packages/core/src/collection/group/index.ts @@ -80,7 +80,7 @@ export class Group< // Add side effect to Group // that rebuilds the Group whenever the Group value changes this.addSideEffect(Group.rebuildGroupSideEffectKey, (state, config) => { - this.rebuild(config, config?.any?.trackedChanges || []); + this.rebuild(config?.any?.trackedChanges || [], config); }); // Initial rebuild @@ -375,18 +375,21 @@ export class Group< * [Learn more..](https://agile-ts.org/docs/core/collection/group/methods#rebuild) * * @internal + * @param trackedChanges - Changes that were tracked between two rebuilds. * @param config - Configuration object - * @param trackedChanges - Changes that were made between two rebuilds. */ public rebuild( - config: GroupIngestConfigInterface = {}, - trackedChanges: TrackedChangeInterface[] = [] + trackedChanges: TrackedChangeInterface[] = [], + config: GroupIngestConfigInterface = {} ): this { // Don't rebuild Group if Collection isn't correctly instantiated yet // (because only after a successful instantiation the Collection // contains the Items which are essential for a proper rebuild) if (!this.collection().isInstantiated) return this; + // Item keys that couldn't be found in the Collection + const notFoundItemKeys: Array = []; + // Soft rebuild the Collection (-> rebuild only parts of the Collection) if (trackedChanges.length > 0) { trackedChanges.forEach((change) => { @@ -413,20 +416,21 @@ export class Group< } }); this.observers['output'].ingest(config); - return this; } - // Hard rebuild the whole Collection + else { + const groupItemValues: Array = []; + + // Fetch Items from Collection + this._value.forEach((itemKey) => { + const item = this.collection().getItem(itemKey); + if (item != null) groupItemValues.push(item._value); + else notFoundItemKeys.push(itemKey); + }); - const notFoundItemKeys: Array = []; // Item keys that couldn't be found in the Collection - const groupItems: Array> = []; - - // Fetch Items from Collection - this._value.forEach((itemKey) => { - const item = this.collection().getItem(itemKey); - if (item != null) groupItems.push(item); - else notFoundItemKeys.push(itemKey); - }); + // Ingest rebuilt Group output into the Runtime + this.observers['output'].ingestOutput(groupItemValues, config); + } // Logging if (notFoundItemKeys.length > 0 && this.loadedInitialValue) { @@ -439,14 +443,6 @@ export class Group< this.notFoundItemKeys = notFoundItemKeys; - // Ingest rebuilt Group output into the Runtime - this.observers['output'].ingestOutput( - groupItems.map((item) => { - return item._value; - }), - config - ); - return this; } } diff --git a/packages/core/tests/unit/collection/collection.test.ts b/packages/core/tests/unit/collection/collection.test.ts index 463a092a..b01d3085 100644 --- a/packages/core/tests/unit/collection/collection.test.ts +++ b/packages/core/tests/unit/collection/collection.test.ts @@ -265,10 +265,10 @@ describe('Collection Tests', () => { key: 'group1Key', }); - expect(collection.createGroup).toHaveBeenCalledWith('group1Key', [ - 1, - 2, - ]); + expect(collection.createGroup).toHaveBeenCalledWith( + 'group1Key', + [1, 2] + ); LogMock.hasLoggedCode('1B:02:00'); expect(response).toBeInstanceOf(Group); @@ -3040,13 +3040,16 @@ describe('Collection Tests', () => { collection.rebuildGroupsThatIncludeItemKey('dummyItem1'); // Group 1 - expect(dummyGroup1.rebuild).toHaveBeenCalledWith({}, [ - { - key: 'dummyItem1', - index: 0, - method: TrackedChangeMethod.UPDATE, - }, - ]); + expect(dummyGroup1.rebuild).toHaveBeenCalledWith( + [ + { + key: 'dummyItem1', + index: 0, + method: TrackedChangeMethod.UPDATE, + }, + ], + {} + ); // Group 2 expect(dummyGroup2.rebuild).not.toHaveBeenCalled(); @@ -3061,34 +3064,34 @@ describe('Collection Tests', () => { // Group 1 expect(dummyGroup1.rebuild).toHaveBeenCalledWith( - { - key: 'frank', - background: true, - force: true, - }, [ { key: 'dummyItem2', index: 1, method: TrackedChangeMethod.UPDATE, }, - ] - ); - - // Group 2 - expect(dummyGroup2.rebuild).toHaveBeenCalledWith( + ], { key: 'frank', background: true, force: true, - }, + } + ); + + // Group 2 + expect(dummyGroup2.rebuild).toHaveBeenCalledWith( [ { key: 'dummyItem2', index: 0, method: TrackedChangeMethod.UPDATE, }, - ] + ], + { + key: 'frank', + background: true, + force: true, + } ); }); }); diff --git a/packages/core/tests/unit/collection/group/group.test.ts b/packages/core/tests/unit/collection/group/group.test.ts index 377ad586..0ae2be7d 100644 --- a/packages/core/tests/unit/collection/group/group.test.ts +++ b/packages/core/tests/unit/collection/group/group.test.ts @@ -160,14 +160,12 @@ describe('Group Tests', () => { group = new Group(dummyCollection, [], { key: 'groupKey', }); - dummyCollection.collect({ id: 'dummyItem1Key', name: 'coolName' }); - dummyCollection.collect({ id: 'dummyItem2Key', name: 'coolName' }); + dummyCollection.collect({ id: 'dummyItem1Key', name: 'jeff' }); + dummyCollection.collect({ id: 'dummyItem2Key', name: 'frank' }); + dummyCollection.collect({ id: 'dummyItem3Key', name: 'hans' }); dummyItem1 = dummyCollection.getItem('dummyItem1Key') as any; dummyItem2 = dummyCollection.getItem('dummyItem2Key') as any; - dummyItem3 = new Item(dummyCollection, { - id: 'dummyItem3Key', - name: 'coolName', - }); + dummyItem3 = dummyCollection.getItem('dummyItem3Key') as any; }); describe('output get function tests', () => { @@ -234,7 +232,7 @@ describe('Group Tests', () => { group.nextStateValue = [ 'dummyItem1Key', 'dummyItem2Key', - 'dummyItem3Key', + 'missingInCollectionItemKey', ]; group.set = jest.fn(); }); @@ -243,7 +241,7 @@ describe('Group Tests', () => { group.remove('dummyItem1Key'); expect(group.set).toHaveBeenCalledWith( - ['dummyItem2Key', 'dummyItem3Key'], + ['dummyItem2Key', 'missingInCollectionItemKey'], { any: { trackedChanges: [ @@ -267,7 +265,7 @@ describe('Group Tests', () => { }); expect(group.set).toHaveBeenCalledWith( - ['dummyItem2Key', 'dummyItem3Key'], + ['dummyItem2Key', 'missingInCollectionItemKey'], { background: true, force: true, @@ -284,7 +282,11 @@ describe('Group Tests', () => { }); it('should remove Items from Group', () => { - group.remove(['dummyItem1Key', 'notExistingItemKey', 'dummyItem3Key']); + group.remove([ + 'dummyItem1Key', + 'notExistingItemKey', + 'missingInCollectionItemKey', + ]); expect(group.set).toHaveBeenCalledWith(['dummyItem2Key'], { any: { @@ -297,7 +299,7 @@ describe('Group Tests', () => { { index: 1, method: TrackedChangeMethod.REMOVE, - key: 'dummyItem3Key', + key: 'missingInCollectionItemKey', }, ], }, @@ -305,7 +307,7 @@ describe('Group Tests', () => { }); it("should remove Item/s from Group that doesn't exist in the Collection in background", () => { - group.remove('dummyItem3Key'); + group.remove('missingInCollectionItemKey'); expect(group.set).toHaveBeenCalledWith( ['dummyItem1Key', 'dummyItem2Key'], @@ -316,7 +318,7 @@ describe('Group Tests', () => { { index: 2, method: TrackedChangeMethod.REMOVE, - key: 'dummyItem3Key', + key: 'missingInCollectionItemKey', }, ], }, @@ -329,7 +331,7 @@ describe('Group Tests', () => { 'if passing not existing Items to remove ' + "and Items that doesn't exist in the Collection", () => { - group.remove(['notExistingItemKey', 'dummyItem3Key']); + group.remove(['notExistingItemKey', 'missingInCollectionItemKey']); expect(group.set).toHaveBeenCalledWith( ['dummyItem1Key', 'dummyItem2Key'], @@ -340,7 +342,7 @@ describe('Group Tests', () => { { index: 2, method: TrackedChangeMethod.REMOVE, - key: 'dummyItem3Key', + key: 'missingInCollectionItemKey', }, ], }, @@ -420,7 +422,7 @@ describe('Group Tests', () => { }); it('should add Items at the end of the Group', () => { - group.add(['dummyItem1Key', 'dummyItem2Key', 'dummyItem3Key']); + group.add(['dummyItem1Key', 'dummyItem2Key', 'notExistingItemKey']); expect(group.set).toHaveBeenCalledWith( [ @@ -428,7 +430,7 @@ describe('Group Tests', () => { 'dummyItem1Key', 'placeholder', 'dummyItem2Key', - 'dummyItem3Key', + 'notExistingItemKey', ], { any: { @@ -441,7 +443,7 @@ describe('Group Tests', () => { { index: 3, method: TrackedChangeMethod.ADD, - key: 'dummyItem3Key', + key: 'notExistingItemKey', }, ], }, @@ -450,10 +452,10 @@ describe('Group Tests', () => { }); it("should add Item that doesn't exist in Collection at the end of the Group in background", () => { - group.add('dummyItem3Key'); + group.add('notExistingItemKey'); expect(group.set).toHaveBeenCalledWith( - ['placeholder', 'dummyItem1Key', 'placeholder', 'dummyItem3Key'], + ['placeholder', 'dummyItem1Key', 'placeholder', 'notExistingItemKey'], { background: true, any: { @@ -461,7 +463,7 @@ describe('Group Tests', () => { { index: 2, method: TrackedChangeMethod.ADD, - key: 'dummyItem3Key', + key: 'notExistingItemKey', }, ], }, @@ -474,10 +476,15 @@ describe('Group Tests', () => { 'if passing already added Items ' + "and Items that doesn't exist in the Collection", () => { - group.add(['dummyItem1Key', 'dummyItem3Key']); + group.add(['dummyItem1Key', 'notExistingItemKey']); expect(group.set).toHaveBeenCalledWith( - ['placeholder', 'dummyItem1Key', 'placeholder', 'dummyItem3Key'], + [ + 'placeholder', + 'dummyItem1Key', + 'placeholder', + 'notExistingItemKey', + ], { background: true, any: { @@ -485,7 +492,7 @@ describe('Group Tests', () => { { index: 2, method: TrackedChangeMethod.ADD, - key: 'dummyItem3Key', + key: 'notExistingItemKey', }, ], }, @@ -527,7 +534,7 @@ describe('Group Tests', () => { describe('getItems function tests', () => { beforeEach(() => { - group._value = ['dummyItem1Key', 'dummyItem3Key', 'dummyItem2Key']; + group._value = ['dummyItem1Key', 'notExistingItemKey', 'dummyItem2Key']; }); it('should return all existing Items of the Group', () => { @@ -643,54 +650,79 @@ describe('Group Tests', () => { describe('rebuild function tests', () => { beforeEach(() => { - group._value = ['dummyItem1Key', 'dummyItem3Key', 'dummyItem2Key']; + group._value = [ + 'dummyItem1Key', + 'notExistingItemKey', + 'dummyItem2Key', + 'dummyItem3Key', + ]; group.observers['output'].ingestOutput = jest.fn(); }); - it('should ingest the built Group output and set notFoundItemKeys to the not found Item Keys (default config)', () => { - group.rebuild(); + it( + 'should hard rebuild the Group if no trackedChanges were specified ' + + 'and set notExistingItemKeys to the not found Item Keys (default config)', + () => { + group.rebuild(); - expect(group.notFoundItemKeys).toStrictEqual(['dummyItem3Key']); - expect(group._output).toStrictEqual([]); // because of mocking 'ingestValue' - expect(group.observers['output'].ingestOutput).toHaveBeenCalledWith( - [dummyItem1, dummyItem2], - {} - ); + expect(group.notFoundItemKeys).toStrictEqual(['notExistingItemKey']); + expect(group._output).toStrictEqual([]); // because of mocking 'ingestValue' + expect(group.observers['output'].ingestOutput).toHaveBeenCalledWith( + [dummyItem1._value, dummyItem2._value], + {} + ); - LogMock.hasLoggedCode( - '1C:02:00', - [dummyCollection._key, group._key], - ['dummyItem3Key'] - ); - }); + LogMock.hasLoggedCode( + '1C:02:00', + [dummyCollection._key, group._key], + ['notExistingItemKey'] + ); + } + ); - it('should ingest the built Group output and set notFoundItemKeys to the not found Item Keys (specific config)', () => { - group.rebuild({ background: true, force: false }); + it( + 'should hard rebuild the Group if no trackedChanges were specified ' + + 'and set notExistingItemKeys to the not found Item Keys (specific config)', + () => { + group.rebuild([], { background: true, force: false }); - expect(group.notFoundItemKeys).toStrictEqual(['dummyItem3Key']); - expect(group._output).toStrictEqual([]); // because of mocking 'ingestValue' - expect(group.observers['output'].ingestOutput).toHaveBeenCalledWith( - [dummyItem1, dummyItem2], - { background: true, force: false } - ); + expect(group.notFoundItemKeys).toStrictEqual(['notExistingItemKey']); + expect(group._output).toStrictEqual([]); // because of mocking 'ingestValue' + expect(group.observers['output'].ingestOutput).toHaveBeenCalledWith( + [dummyItem1._value, dummyItem2._value], + { background: true, force: false } + ); - LogMock.hasLoggedCode( - '1C:02:00', - [dummyCollection._key, group._key], - ['dummyItem3Key'] - ); + LogMock.hasLoggedCode( + '1C:02:00', + [dummyCollection._key, group._key], + ['notExistingItemKey'] + ); + } + ); + + it('should soft rebuild the Group if trackedChanges were specified (default config)', () => { + // TODO + }); + + it('should soft rebuild the Group if trackedChanges were specified (specific config)', () => { + // TODO }); - it("shouldn't intest the build Group output if the Collection was not properly instantiated", () => { - dummyCollection.isInstantiated = false; + it( + "shouldn't ingest the build Group output " + + 'if the Collection was not properly instantiated', + () => { + dummyCollection.isInstantiated = false; - group.rebuild(); + group.rebuild(); - expect(group.notFoundItemKeys).toStrictEqual([]); - expect(group._output).toStrictEqual([]); - expect(group.observers['output'].ingestOutput).not.toHaveBeenCalled(); - LogMock.hasNotLogged('warn'); - }); + expect(group.notFoundItemKeys).toStrictEqual([]); + expect(group._output).toStrictEqual([]); + expect(group.observers['output'].ingestOutput).not.toHaveBeenCalled(); + LogMock.hasNotLogged('warn'); + } + ); }); }); }); From 21f0df54e40518078f09160dcf3a8fc78628b2f9 Mon Sep 17 00:00:00 2001 From: BennoDev Date: Sat, 11 Sep 2021 16:52:41 +0200 Subject: [PATCH 88/93] added precise itemKeys --- packages/core/src/collection/group/index.ts | 47 ++++++-- .../tests/unit/collection/group/group.test.ts | 105 +++++++++++------- 2 files changed, 102 insertions(+), 50 deletions(-) diff --git a/packages/core/src/collection/group/index.ts b/packages/core/src/collection/group/index.ts index 5cb957b7..328523b9 100644 --- a/packages/core/src/collection/group/index.ts +++ b/packages/core/src/collection/group/index.ts @@ -34,6 +34,9 @@ export class Group< public _output: Array = []; // Next output of the Group (which can be used for dynamic Group updates) public nextGroupOutput: Array = []; + // Precise itemKeys of the Group only include itemKeys + // that actually exist in the corresponding Collection + public _preciseItemKeys: Array = []; // Manages dependencies to other States and subscriptions of UI-Components. // It also serves as an interface to the runtime. @@ -145,6 +148,10 @@ export class Group< const notExistingItemKeysInCollection: Array = []; const notExistingItemKeys: Array = []; let newGroupValue = copy(this.nextStateValue); + // Need to temporary update the preciseItemKeys + // since in the rebuild one action (trackedChanges) is performed after the other + // which requires a dynamic updated index + let updatedPreciseItemKeys = copy(this._preciseItemKeys); config = defineConfig(config, { softRebuild: true, any: {}, @@ -164,11 +171,15 @@ export class Group< // Track changes to soft rebuild the Group when rebuilding the Group in a side effect if (config.softRebuild) { - config.any['trackedChanges'].push({ - index: newGroupValue.findIndex((ik) => ik === itemKey), - method: TrackedChangeMethod.REMOVE, - key: itemKey, - }); + const index = updatedPreciseItemKeys.findIndex((ik) => ik === itemKey); + if (index !== -1) { + updatedPreciseItemKeys.splice(index, 1); + config.any['trackedChanges'].push({ + index, + method: TrackedChangeMethod.REMOVE, + key: itemKey, + }); + } } // Check if itemKey exists in Collection @@ -209,6 +220,10 @@ export class Group< const notExistingItemKeysInCollection: Array = []; const existingItemKeys: Array = []; const newGroupValue = copy(this.nextStateValue); + // Need to temporary update the preciseItemKeys + // since in the rebuild one action (trackedChanges) is performed after the other + // which requires a dynamic updated index + const updatedPreciseItemKeys = copy(this._preciseItemKeys); config = defineConfig(config, { method: 'push', softRebuild: true, @@ -232,8 +247,11 @@ export class Group< // Track changes to soft rebuild the Group when rebuilding the Group in a side effect if (config.softRebuild) { + const index = + config.method === 'push' ? updatedPreciseItemKeys.length : 0; + updatedPreciseItemKeys.push(itemKey); config.any['trackedChanges'].push({ - index: config.method === 'push' ? newGroupValue.length - 1 : 0, + index, method: TrackedChangeMethod.ADD, key: itemKey, }); @@ -397,17 +415,23 @@ export class Group< switch (change.method) { case TrackedChangeMethod.ADD: + this._preciseItemKeys.splice(change.index, 0, change.key); + // this._value.splice(change.index, 0, change.key); // Already updated in 'add' method if (item != null) { - // this._value.splice(change.index, 0, change.key); // Already updated in 'add' method this.nextGroupOutput.splice(change.index, 0, copy(item._value)); + } else { + notFoundItemKeys.push(change.key); } break; case TrackedChangeMethod.UPDATE: if (item != null) { this.nextGroupOutput[change.index] = copy(item._value); + } else { + notFoundItemKeys.push(change.key); } break; case TrackedChangeMethod.REMOVE: + this._preciseItemKeys.splice(change.index, 1); // this._value.splice(change.index, 1); // Already updated in 'remove' method this.nextGroupOutput.splice(change.index, 1); break; @@ -421,11 +445,16 @@ export class Group< else { const groupItemValues: Array = []; + // Reset precise itemKeys array to rebuild it from scratch + this._preciseItemKeys = []; + // Fetch Items from Collection this._value.forEach((itemKey) => { const item = this.collection().getItem(itemKey); - if (item != null) groupItemValues.push(item._value); - else notFoundItemKeys.push(itemKey); + if (item != null) { + groupItemValues.push(item._value); + this._preciseItemKeys.push(itemKey); + } else notFoundItemKeys.push(itemKey); }); // Ingest rebuilt Group output into the Runtime diff --git a/packages/core/tests/unit/collection/group/group.test.ts b/packages/core/tests/unit/collection/group/group.test.ts index 0ae2be7d..e828fdbf 100644 --- a/packages/core/tests/unit/collection/group/group.test.ts +++ b/packages/core/tests/unit/collection/group/group.test.ts @@ -1,13 +1,13 @@ import { - Group, Agile, Collection, - StateObserver, - ComputedTracker, - Item, CollectionPersistent, - GroupObserver, + ComputedTracker, EnhancedState, + Group, + GroupObserver, + Item, + StateObserver, TrackedChangeMethod, } from '../../../../src'; import { LogMock } from '../../../helper/logMock'; @@ -229,11 +229,14 @@ describe('Group Tests', () => { describe('remove function tests', () => { beforeEach(() => { - group.nextStateValue = [ + group._value = [ 'dummyItem1Key', 'dummyItem2Key', 'missingInCollectionItemKey', ]; + group.nextStateValue = group._value; + group._preciseItemKeys = ['dummyItem1Key', 'dummyItem2Key']; + group.set = jest.fn(); }); @@ -286,9 +289,10 @@ describe('Group Tests', () => { 'dummyItem1Key', 'notExistingItemKey', 'missingInCollectionItemKey', + 'dummyItem2Key', ]); - expect(group.set).toHaveBeenCalledWith(['dummyItem2Key'], { + expect(group.set).toHaveBeenCalledWith([], { any: { trackedChanges: [ { @@ -297,9 +301,9 @@ describe('Group Tests', () => { key: 'dummyItem1Key', }, { - index: 1, + index: 0, method: TrackedChangeMethod.REMOVE, - key: 'missingInCollectionItemKey', + key: 'dummyItem2Key', }, ], }, @@ -313,15 +317,7 @@ describe('Group Tests', () => { ['dummyItem1Key', 'dummyItem2Key'], { background: true, - any: { - trackedChanges: [ - { - index: 2, - method: TrackedChangeMethod.REMOVE, - key: 'missingInCollectionItemKey', - }, - ], - }, + any: { trackedChanges: [] }, } ); }); @@ -337,15 +333,7 @@ describe('Group Tests', () => { ['dummyItem1Key', 'dummyItem2Key'], { background: true, - any: { - trackedChanges: [ - { - index: 2, - method: TrackedChangeMethod.REMOVE, - key: 'missingInCollectionItemKey', - }, - ], - }, + any: { trackedChanges: [] }, } ); } @@ -354,7 +342,10 @@ describe('Group Tests', () => { describe('add function tests', () => { beforeEach(() => { - group.nextStateValue = ['placeholder', 'dummyItem1Key', 'placeholder']; + group._value = ['placeholder', 'dummyItem1Key', 'placeholder']; + group.nextStateValue = group._value; + group._preciseItemKeys = ['dummyItem1Key']; + group.set = jest.fn(); }); @@ -367,7 +358,7 @@ describe('Group Tests', () => { any: { trackedChanges: [ { - index: 2, + index: 1, method: TrackedChangeMethod.ADD, key: 'dummyItem2Key', }, @@ -436,12 +427,12 @@ describe('Group Tests', () => { any: { trackedChanges: [ { - index: 2, + index: 1, method: TrackedChangeMethod.ADD, key: 'dummyItem2Key', }, { - index: 3, + index: 2, method: TrackedChangeMethod.ADD, key: 'notExistingItemKey', }, @@ -461,7 +452,7 @@ describe('Group Tests', () => { any: { trackedChanges: [ { - index: 2, + index: 1, method: TrackedChangeMethod.ADD, key: 'notExistingItemKey', }, @@ -490,7 +481,7 @@ describe('Group Tests', () => { any: { trackedChanges: [ { - index: 2, + index: 1, method: TrackedChangeMethod.ADD, key: 'notExistingItemKey', }, @@ -652,7 +643,7 @@ describe('Group Tests', () => { beforeEach(() => { group._value = [ 'dummyItem1Key', - 'notExistingItemKey', + 'missingInCollectionItemKey', 'dummyItem2Key', 'dummyItem3Key', ]; @@ -668,14 +659,14 @@ describe('Group Tests', () => { expect(group.notFoundItemKeys).toStrictEqual(['notExistingItemKey']); expect(group._output).toStrictEqual([]); // because of mocking 'ingestValue' expect(group.observers['output'].ingestOutput).toHaveBeenCalledWith( - [dummyItem1._value, dummyItem2._value], + [dummyItem1._value, dummyItem2._value, dummyItem3._value], {} ); LogMock.hasLoggedCode( '1C:02:00', [dummyCollection._key, group._key], - ['notExistingItemKey'] + ['missingInCollectionItemKey'] ); } ); @@ -684,25 +675,57 @@ describe('Group Tests', () => { 'should hard rebuild the Group if no trackedChanges were specified ' + 'and set notExistingItemKeys to the not found Item Keys (specific config)', () => { - group.rebuild([], { background: true, force: false }); + group.rebuild([], { background: true, force: false, key: 'frank' }); expect(group.notFoundItemKeys).toStrictEqual(['notExistingItemKey']); expect(group._output).toStrictEqual([]); // because of mocking 'ingestValue' expect(group.observers['output'].ingestOutput).toHaveBeenCalledWith( - [dummyItem1._value, dummyItem2._value], - { background: true, force: false } + [dummyItem1._value, dummyItem2._value, dummyItem3._value], + { background: true, force: false, key: 'frank' } ); LogMock.hasLoggedCode( '1C:02:00', [dummyCollection._key, group._key], - ['notExistingItemKey'] + ['missingInCollectionItemKey'] ); } ); it('should soft rebuild the Group if trackedChanges were specified (default config)', () => { - // TODO + // 'dummyItem1Key', + // 'missingInCollectionItemKey', + // 'dummyItem2Key', + // 'dummyItem3Key' + // TODO the index of a TrackedChange is based on the Group value + // which also contains not existing Items + // -> the index might differ since in the Group output + // not existing Items were skipped + + group.nextGroupOutput = [ + { id: 'dummyItem1Key', name: 'jeff' }, + { id: 'dummyItem2Key', name: 'frank' }, + { id: 'dummyItem3Key', name: 'hans' }, + ]; + + group.rebuild([ + { index: 3, method: TrackedChangeMethod.ADD, key: 'dummyItem3Key' }, + { + index: 1, + method: TrackedChangeMethod.REMOVE, + key: 'dummyItem2Key', + }, + { + index: 10, + method: TrackedChangeMethod.UPDATE, + key: 'missingInCollectionItemKey', + }, + { + index: 0, + method: TrackedChangeMethod.UPDATE, + key: 'dummyItem1Key', + }, + ]); }); it('should soft rebuild the Group if trackedChanges were specified (specific config)', () => { From a6b2140819e4eb5cb9b3e513899cc8abafed4c4b Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sun, 12 Sep 2021 15:46:21 +0200 Subject: [PATCH 89/93] fixed tests --- .../tests/unit/collection/group/group.test.ts | 160 +++++++++++++----- .../core/tests/unit/runtime/observer.test.ts | 2 + 2 files changed, 120 insertions(+), 42 deletions(-) diff --git a/packages/core/tests/unit/collection/group/group.test.ts b/packages/core/tests/unit/collection/group/group.test.ts index e828fdbf..f7028e7e 100644 --- a/packages/core/tests/unit/collection/group/group.test.ts +++ b/packages/core/tests/unit/collection/group/group.test.ts @@ -648,6 +648,7 @@ describe('Group Tests', () => { 'dummyItem3Key', ]; group.observers['output'].ingestOutput = jest.fn(); + group.observers['output'].ingest = jest.fn(); }); it( @@ -656,12 +657,14 @@ describe('Group Tests', () => { () => { group.rebuild(); - expect(group.notFoundItemKeys).toStrictEqual(['notExistingItemKey']); - expect(group._output).toStrictEqual([]); // because of mocking 'ingestValue' + expect(group.notFoundItemKeys).toStrictEqual([ + 'missingInCollectionItemKey', + ]); expect(group.observers['output'].ingestOutput).toHaveBeenCalledWith( [dummyItem1._value, dummyItem2._value, dummyItem3._value], {} ); + expect(group.observers['output'].ingest).not.toHaveBeenCalled(); LogMock.hasLoggedCode( '1C:02:00', @@ -677,8 +680,9 @@ describe('Group Tests', () => { () => { group.rebuild([], { background: true, force: false, key: 'frank' }); - expect(group.notFoundItemKeys).toStrictEqual(['notExistingItemKey']); - expect(group._output).toStrictEqual([]); // because of mocking 'ingestValue' + expect(group.notFoundItemKeys).toStrictEqual([ + 'missingInCollectionItemKey', + ]); expect(group.observers['output'].ingestOutput).toHaveBeenCalledWith( [dummyItem1._value, dummyItem2._value, dummyItem3._value], { background: true, force: false, key: 'frank' } @@ -692,45 +696,118 @@ describe('Group Tests', () => { } ); - it('should soft rebuild the Group if trackedChanges were specified (default config)', () => { - // 'dummyItem1Key', - // 'missingInCollectionItemKey', - // 'dummyItem2Key', - // 'dummyItem3Key' - // TODO the index of a TrackedChange is based on the Group value - // which also contains not existing Items - // -> the index might differ since in the Group output - // not existing Items were skipped - - group.nextGroupOutput = [ - { id: 'dummyItem1Key', name: 'jeff' }, - { id: 'dummyItem2Key', name: 'frank' }, - { id: 'dummyItem3Key', name: 'hans' }, - ]; + it( + 'should soft rebuild the Group if trackedChanges were specified ' + + 'and set notExistingItemKeys to the not found Item Keys (default config)', + () => { + group.nextGroupOutput = [ + { id: 'dummyItem1Key', name: 'jeff' }, + { id: 'dummyItem2Key', name: 'frank' }, + ]; + group._preciseItemKeys = ['dummyItem1Key', 'dummyItem2Key']; + + group.rebuild([ + { index: 2, method: TrackedChangeMethod.ADD, key: 'dummyItem3Key' }, + { + index: 1, + method: TrackedChangeMethod.REMOVE, + key: 'dummyItem2Key', + }, + { + index: 4, + method: TrackedChangeMethod.UPDATE, + key: 'missingInCollectionItemKey', + }, + { + index: 0, + method: TrackedChangeMethod.UPDATE, + key: 'dummyItem1Key', + }, + ]); - group.rebuild([ - { index: 3, method: TrackedChangeMethod.ADD, key: 'dummyItem3Key' }, - { - index: 1, - method: TrackedChangeMethod.REMOVE, - key: 'dummyItem2Key', - }, - { - index: 10, - method: TrackedChangeMethod.UPDATE, - key: 'missingInCollectionItemKey', - }, - { - index: 0, - method: TrackedChangeMethod.UPDATE, - key: 'dummyItem1Key', - }, - ]); - }); + expect(group.notFoundItemKeys).toStrictEqual([ + 'missingInCollectionItemKey', + ]); + expect(group.observers['output'].ingestOutput).not.toHaveBeenCalled(); + expect(group.observers['output'].ingest).toHaveBeenCalled(); + expect(group.nextGroupOutput).toStrictEqual([ + { id: 'dummyItem1Key', name: 'jeff' }, + { id: 'dummyItem3Key', name: 'hans' }, + ]); + expect(group._preciseItemKeys).toStrictEqual([ + 'dummyItem1Key', + 'dummyItem3Key', + ]); - it('should soft rebuild the Group if trackedChanges were specified (specific config)', () => { - // TODO - }); + LogMock.hasLoggedCode( + '1C:02:00', + [dummyCollection._key, group._key], + ['missingInCollectionItemKey'] + ); + } + ); + + it( + 'should soft rebuild the Group if trackedChanges were specified ' + + 'and set notExistingItemKeys to the not found Item Keys (specific config)', + () => { + group.nextGroupOutput = [ + { id: 'dummyItem1Key', name: 'jeff' }, + { id: 'dummyItem2Key', name: 'frank' }, + ]; + group._preciseItemKeys = ['dummyItem1Key', 'dummyItem2Key']; + + group.rebuild( + [ + { + index: 2, + method: TrackedChangeMethod.ADD, + key: 'dummyItem3Key', + }, + { + index: 1, + method: TrackedChangeMethod.REMOVE, + key: 'dummyItem2Key', + }, + { + index: 4, + method: TrackedChangeMethod.UPDATE, + key: 'missingInCollectionItemKey', + }, + { + index: 0, + method: TrackedChangeMethod.UPDATE, + key: 'dummyItem1Key', + }, + ], + { key: 'frank', force: true, background: true } + ); + + expect(group.notFoundItemKeys).toStrictEqual([ + 'missingInCollectionItemKey', + ]); + expect(group.observers['output'].ingestOutput).not.toHaveBeenCalled(); + expect(group.observers['output'].ingest).toHaveBeenCalledWith({ + key: 'frank', + force: true, + background: true, + }); + expect(group.nextGroupOutput).toStrictEqual([ + { id: 'dummyItem1Key', name: 'jeff' }, + { id: 'dummyItem3Key', name: 'hans' }, + ]); + expect(group._preciseItemKeys).toStrictEqual([ + 'dummyItem1Key', + 'dummyItem3Key', + ]); + + LogMock.hasLoggedCode( + '1C:02:00', + [dummyCollection._key, group._key], + ['missingInCollectionItemKey'] + ); + } + ); it( "shouldn't ingest the build Group output " + @@ -741,7 +818,6 @@ describe('Group Tests', () => { group.rebuild(); expect(group.notFoundItemKeys).toStrictEqual([]); - expect(group._output).toStrictEqual([]); expect(group.observers['output'].ingestOutput).not.toHaveBeenCalled(); LogMock.hasNotLogged('warn'); } diff --git a/packages/core/tests/unit/runtime/observer.test.ts b/packages/core/tests/unit/runtime/observer.test.ts index 5810c138..5acbcf64 100644 --- a/packages/core/tests/unit/runtime/observer.test.ts +++ b/packages/core/tests/unit/runtime/observer.test.ts @@ -103,6 +103,7 @@ describe('Observer Tests', () => { }, force: false, maxTriesToUpdate: 3, + any: {}, }); }); @@ -128,6 +129,7 @@ describe('Observer Tests', () => { }, force: true, maxTriesToUpdate: 3, + any: {}, }); }); From af798b782601a45b5214555eb22136590313bc2f Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sun, 12 Sep 2021 16:05:55 +0200 Subject: [PATCH 90/93] fixed typo --- packages/core/src/collection/group/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/collection/group/index.ts b/packages/core/src/collection/group/index.ts index 328523b9..4aac3118 100644 --- a/packages/core/src/collection/group/index.ts +++ b/packages/core/src/collection/group/index.ts @@ -151,7 +151,7 @@ export class Group< // Need to temporary update the preciseItemKeys // since in the rebuild one action (trackedChanges) is performed after the other // which requires a dynamic updated index - let updatedPreciseItemKeys = copy(this._preciseItemKeys); + const updatedPreciseItemKeys = copy(this._preciseItemKeys); config = defineConfig(config, { softRebuild: true, any: {}, From ad56d3975c8c00a50ffc75ac417a7654398b02e6 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Sun, 12 Sep 2021 20:02:46 +0200 Subject: [PATCH 91/93] fixed typo --- .../develop/simple-todo-list/src/core.js | 4 +- packages/core/src/collection/collection.ts | 51 +++++-- packages/core/src/collection/group/index.ts | 4 +- .../tests/unit/collection/collection.test.ts | 57 ++++++-- .../tests/unit/collection/group/group.test.ts | 129 +++++++++++------- 5 files changed, 169 insertions(+), 76 deletions(-) diff --git a/examples/react/develop/simple-todo-list/src/core.js b/examples/react/develop/simple-todo-list/src/core.js index 316d5f31..881290a6 100644 --- a/examples/react/develop/simple-todo-list/src/core.js +++ b/examples/react/develop/simple-todo-list/src/core.js @@ -1,4 +1,4 @@ -import { createCollection } from '@agile-ts/core'; +import { createCollection, globalBind } from '@agile-ts/core'; import { assignSharedAgileLoggerConfig, Logger } from '@agile-ts/logger'; assignSharedAgileLoggerConfig({ level: Logger.level.DEBUG }); @@ -7,3 +7,5 @@ assignSharedAgileLoggerConfig({ level: Logger.level.DEBUG }); export const TODOS = createCollection({ initialData: [{ id: 1, name: 'Clean Bathroom' }], }).persist('todos'); // persist does store the Collection in the Local Storage + +globalBind('__core__', { TODOS }); diff --git a/packages/core/src/collection/collection.ts b/packages/core/src/collection/collection.ts index ff552fbf..60535136 100644 --- a/packages/core/src/collection/collection.ts +++ b/packages/core/src/collection/collection.ts @@ -1218,7 +1218,9 @@ export class Collection< * @public * @param itemKeys - Item/s with identifier/s to be removed. */ - public remove(itemKeys: ItemKey | Array): { + public remove( + itemKeys: ItemKey | Array + ): { fromGroups: (groups: Array | ItemKey) => Collection; everywhere: (config?: RemoveItemsConfigInterface) => Collection; } { @@ -1467,16 +1469,43 @@ export class Collection< for (const groupKey of Object.keys(this.groups)) { const group = this.getGroup(groupKey); if (group != null && group.has(itemKey)) { - group.rebuild( - [ - { - key: itemKey, - index: group.nextStateValue.findIndex((ik) => itemKey === ik), - method: TrackedChangeMethod.UPDATE, - }, - ], - config - ); + const index = group._preciseItemKeys.findIndex((ik) => itemKey === ik); + + // Update Group output at index + if (index !== -1) { + group.rebuild( + [ + { + key: itemKey, + index: index, + method: TrackedChangeMethod.UPDATE, + }, + ], + config + ); + } + // Add Item to the Group output if it isn't yet represented there to be updated + else { + const indexOfBeforeItemKey = + group.nextStateValue.findIndex((ik) => itemKey === ik) - 1; + + group.rebuild( + [ + { + key: itemKey, + index: + indexOfBeforeItemKey >= 0 + ? group._preciseItemKeys.findIndex( + (ik) => + group.nextStateValue[indexOfBeforeItemKey] === ik + ) + 1 + : 0, + method: TrackedChangeMethod.ADD, + }, + ], + config + ); + } } } } diff --git a/packages/core/src/collection/group/index.ts b/packages/core/src/collection/group/index.ts index 4aac3118..c404b7db 100644 --- a/packages/core/src/collection/group/index.ts +++ b/packages/core/src/collection/group/index.ts @@ -415,9 +415,9 @@ export class Group< switch (change.method) { case TrackedChangeMethod.ADD: - this._preciseItemKeys.splice(change.index, 0, change.key); // this._value.splice(change.index, 0, change.key); // Already updated in 'add' method if (item != null) { + this._preciseItemKeys.splice(change.index, 0, change.key); this.nextGroupOutput.splice(change.index, 0, copy(item._value)); } else { notFoundItemKeys.push(change.key); @@ -431,8 +431,8 @@ export class Group< } break; case TrackedChangeMethod.REMOVE: - this._preciseItemKeys.splice(change.index, 1); // this._value.splice(change.index, 1); // Already updated in 'remove' method + this._preciseItemKeys.splice(change.index, 1); this.nextGroupOutput.splice(change.index, 1); break; default: diff --git a/packages/core/tests/unit/collection/collection.test.ts b/packages/core/tests/unit/collection/collection.test.ts index b01d3085..9dee6d0b 100644 --- a/packages/core/tests/unit/collection/collection.test.ts +++ b/packages/core/tests/unit/collection/collection.test.ts @@ -265,10 +265,10 @@ describe('Collection Tests', () => { key: 'group1Key', }); - expect(collection.createGroup).toHaveBeenCalledWith( - 'group1Key', - [1, 2] - ); + expect(collection.createGroup).toHaveBeenCalledWith('group1Key', [ + 1, + 2, + ]); LogMock.hasLoggedCode('1B:02:00'); expect(response).toBeInstanceOf(Group); @@ -3020,10 +3020,24 @@ describe('Collection Tests', () => { let dummyGroup1: Group; let dummyGroup2: Group; + let dummyItem1: Item; + let dummyItem2: Item; + beforeEach(() => { - dummyGroup1 = new Group(collection, ['dummyItem1', 'dummyItem2'], { - key: 'dummyGroup1', - }); + dummyItem1 = new Item(collection, { id: 'dummyItem1', name: 'Jeff' }); + dummyItem2 = new Item(collection, { id: 'dummyItem2', name: 'Jeff' }); + collection.data = { + dummyItem1: dummyItem1, + dummyItem2: dummyItem2, + }; + + dummyGroup1 = new Group( + collection, + ['dummyItem1', 'missingInCollectionItemKey', 'dummyItem2'], + { + key: 'dummyGroup1', + } + ); dummyGroup2 = new Group(collection, ['dummyItem2'], { key: 'dummyGroup2', }); @@ -3036,7 +3050,7 @@ describe('Collection Tests', () => { dummyGroup2.rebuild = jest.fn(); }); - it('should rebuild each Group that includes the specified itemKey (default config)', () => { + it('should update the Item in each Group (output) that includes the specified itemKey (default config)', () => { collection.rebuildGroupsThatIncludeItemKey('dummyItem1'); // Group 1 @@ -3055,7 +3069,7 @@ describe('Collection Tests', () => { expect(dummyGroup2.rebuild).not.toHaveBeenCalled(); }); - it('should rebuild each Group that includes the specified itemKey (specific config)', () => { + it('should update the Item in each Group (output) that includes the specified itemKey (specific config)', () => { collection.rebuildGroupsThatIncludeItemKey('dummyItem2', { key: 'frank', background: true, @@ -3094,6 +3108,31 @@ describe('Collection Tests', () => { } ); }); + + it( + 'should update the Item in each Group (output) that includes the specified itemKey ' + + "although the Item doesn't exist in the Group output yet", + () => { + collection.rebuildGroupsThatIncludeItemKey( + 'missingInCollectionItemKey' + ); + + // Group 1 + expect(dummyGroup1.rebuild).toHaveBeenCalledWith( + [ + { + key: 'missingInCollectionItemKey', + index: 1, + method: TrackedChangeMethod.ADD, + }, + ], + {} + ); + + // Group 2 + expect(dummyGroup2.rebuild).not.toHaveBeenCalled(); + } + ); }); }); }); diff --git a/packages/core/tests/unit/collection/group/group.test.ts b/packages/core/tests/unit/collection/group/group.test.ts index f7028e7e..b544c54c 100644 --- a/packages/core/tests/unit/collection/group/group.test.ts +++ b/packages/core/tests/unit/collection/group/group.test.ts @@ -698,38 +698,35 @@ describe('Group Tests', () => { it( 'should soft rebuild the Group if trackedChanges were specified ' + - 'and set notExistingItemKeys to the not found Item Keys (default config)', + 'and set notExistingItemKeys to the not found itemKeys (ADD)', () => { - group.nextGroupOutput = [ - { id: 'dummyItem1Key', name: 'jeff' }, - { id: 'dummyItem2Key', name: 'frank' }, - ]; - group._preciseItemKeys = ['dummyItem1Key', 'dummyItem2Key']; + group.nextGroupOutput = [{ id: 'dummyItem1Key', name: 'jeff' }]; + group._preciseItemKeys = ['dummyItem1Key']; - group.rebuild([ - { index: 2, method: TrackedChangeMethod.ADD, key: 'dummyItem3Key' }, - { - index: 1, - method: TrackedChangeMethod.REMOVE, - key: 'dummyItem2Key', - }, - { - index: 4, - method: TrackedChangeMethod.UPDATE, - key: 'missingInCollectionItemKey', - }, - { - index: 0, - method: TrackedChangeMethod.UPDATE, - key: 'dummyItem1Key', - }, - ]); + group.rebuild( + [ + { + index: 1, + method: TrackedChangeMethod.ADD, + key: 'dummyItem3Key', + }, + { + index: 2, + method: TrackedChangeMethod.ADD, + key: 'missingInCollectionItemKey', + }, + ], + { key: 'test', background: true } + ); expect(group.notFoundItemKeys).toStrictEqual([ 'missingInCollectionItemKey', ]); expect(group.observers['output'].ingestOutput).not.toHaveBeenCalled(); - expect(group.observers['output'].ingest).toHaveBeenCalled(); + expect(group.observers['output'].ingest).toHaveBeenCalledWith({ + key: 'test', + background: true, + }); expect(group.nextGroupOutput).toStrictEqual([ { id: 'dummyItem1Key', name: 'jeff' }, { id: 'dummyItem3Key', name: 'hans' }, @@ -747,40 +744,69 @@ describe('Group Tests', () => { } ); + it('should soft rebuild the Group if trackedChanges were specified (REMOVE)', () => { + group.nextGroupOutput = [ + { id: 'dummyItem1Key', name: 'jeff' }, + { id: 'dummyItem2Key', name: 'frank' }, + { id: 'dummyItem3Key', name: 'hans' }, + ]; + group._preciseItemKeys = [ + 'dummyItem1Key', + 'dummyItem2Key', + 'dummyItem3Key', + ]; + + group.rebuild( + [ + { + index: 1, + method: TrackedChangeMethod.REMOVE, + key: 'dummyItem2Key', + }, + ], + { key: 'test', background: true } + ); + + expect(group.notFoundItemKeys).toStrictEqual([]); + expect(group.observers['output'].ingestOutput).not.toHaveBeenCalled(); + expect(group.observers['output'].ingest).toHaveBeenCalledWith({ + key: 'test', + background: true, + }); + expect(group.nextGroupOutput).toStrictEqual([ + { id: 'dummyItem1Key', name: 'jeff' }, + { id: 'dummyItem3Key', name: 'hans' }, + ]); + expect(group._preciseItemKeys).toStrictEqual([ + 'dummyItem1Key', + 'dummyItem3Key', + ]); + + LogMock.hasNotLogged('warn'); + }); + it( 'should soft rebuild the Group if trackedChanges were specified ' + - 'and set notExistingItemKeys to the not found Item Keys (specific config)', + 'and set notExistingItemKeys to the not found itemKeys (UPDATE)', () => { - group.nextGroupOutput = [ - { id: 'dummyItem1Key', name: 'jeff' }, - { id: 'dummyItem2Key', name: 'frank' }, - ]; - group._preciseItemKeys = ['dummyItem1Key', 'dummyItem2Key']; + dummyItem1._value = { id: 'dummyItem1Key', name: 'frank' }; + group.nextGroupOutput = [{ id: 'dummyItem1Key', name: 'jeff' }]; + group._preciseItemKeys = ['dummyItem1Key']; group.rebuild( [ { - index: 2, - method: TrackedChangeMethod.ADD, - key: 'dummyItem3Key', + index: 0, + method: TrackedChangeMethod.UPDATE, + key: 'dummyItem1Key', }, { index: 1, - method: TrackedChangeMethod.REMOVE, - key: 'dummyItem2Key', - }, - { - index: 4, method: TrackedChangeMethod.UPDATE, key: 'missingInCollectionItemKey', }, - { - index: 0, - method: TrackedChangeMethod.UPDATE, - key: 'dummyItem1Key', - }, ], - { key: 'frank', force: true, background: true } + { key: 'test', background: true } ); expect(group.notFoundItemKeys).toStrictEqual([ @@ -788,18 +814,13 @@ describe('Group Tests', () => { ]); expect(group.observers['output'].ingestOutput).not.toHaveBeenCalled(); expect(group.observers['output'].ingest).toHaveBeenCalledWith({ - key: 'frank', - force: true, + key: 'test', background: true, }); expect(group.nextGroupOutput).toStrictEqual([ - { id: 'dummyItem1Key', name: 'jeff' }, - { id: 'dummyItem3Key', name: 'hans' }, - ]); - expect(group._preciseItemKeys).toStrictEqual([ - 'dummyItem1Key', - 'dummyItem3Key', + { id: 'dummyItem1Key', name: 'frank' }, ]); + expect(group._preciseItemKeys).toStrictEqual(['dummyItem1Key']); LogMock.hasLoggedCode( '1C:02:00', @@ -819,6 +840,8 @@ describe('Group Tests', () => { expect(group.notFoundItemKeys).toStrictEqual([]); expect(group.observers['output'].ingestOutput).not.toHaveBeenCalled(); + expect(group.observers['output'].ingest).not.toHaveBeenCalled(); + LogMock.hasNotLogged('warn'); } ); From 256dabb977df47021f5a7d7d688366c04da08f36 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Mon, 13 Sep 2021 06:51:44 +0200 Subject: [PATCH 92/93] bumped version --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f7257c6f..3a02e780 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ AgileTs is a global State and Logic Library implemented in Typescript. It offers a reimagined API that focuses on **developer experience** and allows you to **easily** and **flexible** manage your application States. Besides [States](https://agile-ts.org/docs/core/state), -AgileTs offers some other powerful APIs that make your life easier, +AgileTs offers some other powerful and tree shakable APIs that make your life easier, such as [Collections](https://agile-ts.org/docs/core/collection) and [Computed States](https://agile-ts.org/docs/core/computed). The philosophy behind AgileTs is simple: From aae64559cee5b71893d0967d5c4690e9a1517a8b Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Mon, 13 Sep 2021 07:19:37 +0200 Subject: [PATCH 93/93] bumped version --- .changeset/calm-pets-attend.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .changeset/calm-pets-attend.md diff --git a/.changeset/calm-pets-attend.md b/.changeset/calm-pets-attend.md new file mode 100644 index 00000000..fc5ca2b8 --- /dev/null +++ b/.changeset/calm-pets-attend.md @@ -0,0 +1,27 @@ +--- +'@agile-ts/api': patch +'@agile-ts/core': patch +'cra-template-agile': patch +'cra-template-agile-typescript': patch +'@agile-ts/event': patch +'@agile-ts/logger': patch +'@agile-ts/multieditor': patch +'@agile-ts/proxytree': patch +'@agile-ts/react': patch +'@agile-ts/utils': patch +'@agile-ts/vue': patch +--- + +#### :rocket: New Feature +* `core`, `event`, `logger`, `multieditor`, `react`, `utils` + * [#188](https://github.com/agile-ts/agile/pull/188) Tree shakeable support ([@bennodev19](https://github.com/bennodev19)) + +#### :nail_care: Polish +* `core` + * [#189](https://github.com/agile-ts/agile/pull/189) Optimize collection rebuilds ([@bennodev19](https://github.com/bennodev19)) +* `api`, `core`, `cra-template-agile-typescript`, `cra-template-agile`, `event`, `logger`, `multieditor`, `proxytree`, `react`, `utils`, `vue` + * [#187](https://github.com/agile-ts/agile/pull/187) Tree shakeable support ([@bennodev19](https://github.com/bennodev19)) + +#### Committers: 1 +- BennoDev ([@bennodev19](https://github.com/bennodev19)) +