From 9db6211e40154d09bdca0a42969033c8248a0659 Mon Sep 17 00:00:00 2001 From: Benno Kohrs Date: Wed, 28 Jul 2021 17:00:19 +0200 Subject: [PATCH 01/27] 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 02/27] 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 03/27] 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 04/27] 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 05/27] 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 06/27] 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 07/27] 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 08/27] 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 09/27] 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 10/27] 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 11/27] 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 12/27] 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 13/27] 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 14/27] 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 15/27] 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 16/27] 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 17/27] 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 18/27] 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 19/27] 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 20/27] 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 21/27] 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 22/27] 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 23/27] 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 24/27] 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 25/27] 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 26/27] 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 27/27] 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,