diff --git a/.changeset/serious-terms-perform.md b/.changeset/serious-terms-perform.md new file mode 100644 index 00000000..afb114d6 --- /dev/null +++ b/.changeset/serious-terms-perform.md @@ -0,0 +1,27 @@ +--- +'@agile-ts/api': patch +'@agile-ts/core': patch +'@agile-ts/event': patch +'@agile-ts/logger': patch +'@agile-ts/multieditor': patch +'@agile-ts/proxytree': patch +'@agile-ts/react': patch +'@agile-ts/utils': patch +'@agile-ts/vue': patch +--- + +#### :rocket: New Feature +* `react` + * [#171](https://github.com/agile-ts/agile/pull/171) Add deps array to useAgile() hook ([@bennodev19](https://github.com/bennodev19)) +* `core`, `event`, `react`, `vue` + * [#166](https://github.com/agile-ts/agile/pull/166) Shared Agile Instance ([@bennodev19](https://github.com/bennodev19)) + +#### :nail_care: Polish +* `api`, `core`, `event`, `logger`, `multieditor`, `react`, `utils` + * [#168](https://github.com/agile-ts/agile/pull/168) Performance optimization ([@bennodev19](https://github.com/bennodev19)) +* `core`, `event`, `react`, `vue` + * [#166](https://github.com/agile-ts/agile/pull/166) Shared Agile Instance ([@bennodev19](https://github.com/bennodev19)) + +#### Committers: 1 +- BennoDev ([@bennodev19](https://github.com/bennodev19)) + diff --git a/README.md b/README.md index 3a1ae771..d054994f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ AgileTs -> Global, simple, spacy State and Logic Framework +> Global State and Logic Framework
@@ -42,20 +42,20 @@ ```tsx // -- core.js ------------------------------------------ -// 1️⃣ Create Instance of AgileTs -const App = new Agile(); - -// 2️⃣ Create State with help of before defined Agile Instance -const MY_FIRST_STATE = App.createState("Hello Friend!"); +// 1️⃣ Create State with the initial value "Hello Friend!" +const MY_FIRST_STATE = createState("Hello Friend!"); // -- MyComponent.whatever ------------------------------------------ -// 3️⃣ Bind initialized State to desired UI-Component -// And wolla, it's reactive. Everytime the State mutates the Component rerenders -const myFirstState = useAgile(MY_FIRST_STATE); // Returns value of State ("Hello Friend!") +// 2️⃣ Bind initialized State to the desired UI-Component. +// And wolla, the Component is reactive. +// Everytime the State mutates the Component re-renders. +const myFirstState = useAgile(MY_FIRST_STATE); +console.log(myFirstState); // Returns "Hello Friend!" ``` -Want to learn more? Check out our [Quick Start Guides](https://agile-ts.org/docs/Installation.md). +Want to learn how to implement AgileTs in your preferred UI-Framework? +Check out our [Quick Start Guides](https://agile-ts.org/docs/Installation.md). ### ⛳️ Sandbox Test AgileTs yourself in a [codesandbox](https://codesandbox.io/s/agilets-first-state-f12cz). @@ -66,7 +66,7 @@ It's only one click away. Just select your preferred Framework below. - [Vue](https://codesandbox.io/s/agilets-first-state-i5xxs) - Angular (coming soon) -More examples can be found in the [Example Section](https://agile-ts.org/docs/examples). +More examples can be found in the [Example section](https://agile-ts.org/docs/examples).
@@ -75,59 +75,59 @@ More examples can be found in the [Example Section](https://agile-ts.org/docs/ex
Why should I use AgileTs? -AgileTs is a global, simple, well-tested State Management Framework implemented in Typescript. +AgileTs is a global State and Logic Framework implemented in Typescript. It offers a reimagined API that focuses on **developer experience** -and allows you to **easily** manage your States. -Besides States, AgileTs offers some other powerful APIs that make your life easier. +and allows you to **easily** and **flexible** manage your application States. +Besides [States](https://agile-ts.org/docs/core/state), +AgileTs offers some other powerful APIs that make your life easier, +such as [Collections](https://agile-ts.org/docs/core/collection) +and [Computed States](https://agile-ts.org/docs/core/computed). The philosophy behind AgileTs is simple: ### 🚅 Straightforward Write minimalistic, boilerplate-free code that captures your intent. ```ts -const MY_STATE = App.createState('frank'); // Create State -MY_STATE.set('jeff'); // Update State value -MY_STATE.undo(); // Undo latest State value change -MY_STATE.is({hello: "jeff"}); // Check if State has the value '{hello: "jeff"}' -MY_STATE.watch((value) => {console.log(value);}); // Watch on State changes -``` +// Create State with inital value 'frank' +const MY_STATE = createState('frank'); -**Some more straightforward syntax examples:** +// Update State value from 'frank' to 'jeff' +MY_STATE.set('jeff'); -- Store State in any Storage, like the [Local Storage](https://www.w3schools.com/html/html5_webstorage.asp) - ```ts - MY_STATE.persist("storage-key"); - ``` -- Create a reactive Array of States - ```ts - const MY_COLLECTION = App.createCollection(); - MY_COLLECTION.collect({id: 1, name: "Frank"}); - MY_COLLECTION.collect({id: 2, name: "Dieter"}); - MY_COLLECTION.update(1, {name: "Jeff"}); - ``` -- Compute State depending on other States - ```ts - const MY_INTRODUCTION = App.createComputed(() => { - return `Hello I am '${MY_NAME.vale}' and I use ${MY_STATE_MANAGER.value} for State Management.`; - }); - ``` +// Undo latest State value change +MY_STATE.undo(); + +// Reset State value to its initial value +MY_STATE.reset(); + +// Permanently store State value in an external Storage +MY_STATE.persist("storage-key"); +``` ### 🤸‍ Flexible -- Works in nearly any UI-Layer. Check [here](https://agile-ts.org/docs/Frameworks) if your preferred Framework is supported too. -- Surly behaves with the workflow which suits you best. No need for _reducers_, _actions_, .. -- Has **0** external dependencies +- Works in nearly any UI-Framework (currently supported are [React](https://reactjs.org/), [React-Native](https://reactnative.dev/) and [Vue](https://vuejs.org/)). +- Surly behaves with the workflow that suits you best. + No need for _reducers_, _actions_, .. +- Has **0** external dependencies. ### ⛳️ Centralize -AgileTs is designed to take all business logic out of UI-Components and put them in a central place, often called `core`. -The benefit of keeping logic separate to UI-Components is to make your code more decoupled, portable, scalable, and above all, easily testable. +AgileTs is designed to take all business logic out of the UI-Components +and put them in a central place, often called `core`. +The advantage of keeping logic separate to UI-Components, +is that your code is more decoupled, portable, scalable, +and above all, easily testable. + +You can learn more about ways to centralize your application logic with AgileTs +in our [Style Guides](https://agile-ts.org/docs/style-guide). ### 🎯 Easy to Use -Learn the powerful tools of AgileTs in a short amount of time. An excellent place to start are -our [Quick Start Guides](https://agile-ts.org/docs/Installation), or if you don't like to follow any tutorials, -you can jump straight into our [Example](https://agile-ts.org/docs/examples/Introduction) Section. +Learn the powerful tools of AgileTs in a short period of time. +An excellent place to start are our [Quick Start Guides](https://agile-ts.org/docs/Installation), +or if you don't like to follow tutorials, +you can jump straight into the [Example section](https://agile-ts.org/docs/examples/Introduction).
@@ -136,17 +136,18 @@ you can jump straight into our [Example](https://agile-ts.org/docs/examples/Intr
Installation -In order to properly use AgileTs, in a UI-Framework, we need to install **two** packages. +In order to use AgileTs in a UI-Framework, we need to install **two packages**. -- The [`core`](https://agile-ts.org/docs/core) package, which contains the State Management Logic of AgileTs - and therefore offers powerful classes such as the [`State Class`](https://agile-ts.org/docs/core/state). +- The [`core`](https://agile-ts.org/docs/core) package contains the State Management Logic of AgileTs + and therefore provides powerful classes like the [`State Class`](https://agile-ts.org/docs/core/state). ``` npm install @agile-ts/core ``` -- And on the other hand, a _fitting Integration_ for your preferred UI-Framework. - In my case, the [React Integration](https://www.npmjs.com/package/@agile-ts/react). - Check [here](https://agile-ts.org/docs/frameworks) if your desired Framework is supported, too. +- A _fitting Integration_ for the UI-Framework of your choice, on the other hand, + is an interface to the actual UI and provides useful functionalities + to bind States to UI-Components for reactivity. + I prefer React, so let's go with the [React Integration](https://www.npmjs.com/package/@agile-ts/react) for now. ``` npm install @agile-ts/react ``` @@ -158,10 +159,39 @@ In order to properly use AgileTs, in a UI-Framework, we need to install **two**
Documentation -Sounds AgileTs interesting to you? -Checkout our **[documentation](https://agile-ts.org/docs/introduction)**, to learn more. -And I promise you. You will be able to use AgileTs in no time. -If you have any further questions, don't hesitate to join our [Community Discord](https://discord.gg/T9GzreAwPH). +Does AgileTs sound interesting to you? +Take a look at our **[documentation](https://agile-ts.org/docs/introduction)**, +to learn more about its functionalities and capabilities. +If you have any further questions, +feel free to join our [Community Discord](https://discord.gg/T9GzreAwPH). +We will be happy to help you. + +- Overview + - [Introduction](https://agile-ts.org/docs/introduction/) + - [Installation](https://agile-ts.org/docs/installation) + - [Style Guides](https://agile-ts.org/docs/style-guide) + - [Supported Frameworks](https://agile-ts.org/docs/frameworks) + - [Contributing](https://agile-ts.org/docs/contributing) +- Quick Start + - [React](https://agile-ts.org/docs/quick-start/react) + - [Vue](https://agile-ts.org/docs/quick-start/vue) + - [Angular](https://agile-ts.org/docs/quick-start/angular) +- Packages + - [core](https://agile-ts.org/docs/core) + - [Agile Instance](https://agile-ts.org/docs/core/agile-instance) + - [State](https://agile-ts.org/docs/core/state) + - [Collection](https://agile-ts.org/docs/core/collection) + - [Computed](https://agile-ts.org/docs/core/computed) + - [Storage](https://agile-ts.org/docs/core/storage) + - [Integration](https://agile-ts.org/docs/core/integration) + - [react](https://agile-ts.org/docs/react) + - [React Hooks](https://agile-ts.org/docs/react/hooks) + - [AgileHOC](https://agile-ts.org/docs/react/AgileHOC) +- Examples + - [React](https://agile-ts.org/docs/examples/react) + - [React-Native](https://agile-ts.org/docs/examples/react-native) + - [Vue](https://agile-ts.org/docs/examples/vue) +- [Typescript Interfaces](https://agile-ts.org/docs/interfaces)
@@ -184,17 +214,17 @@ To find out more about contributing, check out the [CONTRIBUTING.md](https://git
Packages of Agile -| Name | Latest Version | Description | -| ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | -| [@agile-ts/core](/packages/core) | [![badge](https://img.shields.io/npm/v/@agile-ts/core.svg?style=flat-square)](https://www.npmjs.com/package/@agile-ts/core) | State Manager | -| [@agile-ts/react](/packages/react) | [![badge](https://img.shields.io/npm/v/@agile-ts/react.svg?style=flat-square)](https://www.npmjs.com/package/@agile-ts/react) | React Integration | -| [@agile-ts/vue](/packages/vue) | [![badge](https://img.shields.io/npm/v/@agile-ts/vue.svg?style=flat-square)](https://www.npmjs.com/package/@agile-ts/vue) | Vue Integration | -| [@agile-ts/api](/packages/api) | [![badge](https://img.shields.io/npm/v/@agile-ts/api.svg?style=flat-square)](https://www.npmjs.com/package/@agile-ts/api) | Promise based API | -| [@agile-ts/multieditor](/packages/multieditor) | [![badge](https://img.shields.io/npm/v/@agile-ts/multieditor.svg?style=flat-square)](https://www.npmjs.com/package/@agile-ts/multieditor) | Simple Form Manager | -| [@agile-ts/event](/packages/event) | [![badge](https://img.shields.io/npm/v/@agile-ts/event.svg?style=flat-square)](https://www.npmjs.com/package/@agile-ts/event) | Handy class for emitting UI Events | -| [@agile-ts/logger](/packages/logger) | [![badge](https://img.shields.io/npm/v/@agile-ts/logger.svg?style=flat-square)](https://www.npmjs.com/package/@agile-ts/logger) | Manages the logging of AgileTs | -| [@agile-ts/utils](/packages/utils) | [![badge](https://img.shields.io/npm/v/@agile-ts/utils.svg?style=flat-square)](https://www.npmjs.com/package/@agile-ts/utils) | Util functions of AgileTs | -| [@agile-ts/proxytree](/packages/proxytree) | [![badge](https://img.shields.io/npm/v/@agile-ts/proxytree.svg?style=flat-square)](https://www.npmjs.com/package/@agile-ts/proxytree) | Create Proxy Tree | +| Name | Latest Version | Description | +| ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | +| [@agile-ts/core](/packages/core) | [![badge](https://img.shields.io/npm/v/@agile-ts/core.svg?style=flat-square)](https://www.npmjs.com/package/@agile-ts/core) | State Manager Logic | +| [@agile-ts/react](/packages/react) | [![badge](https://img.shields.io/npm/v/@agile-ts/react.svg?style=flat-square)](https://www.npmjs.com/package/@agile-ts/react) | React Integration | +| [@agile-ts/vue](/packages/vue) | [![badge](https://img.shields.io/npm/v/@agile-ts/vue.svg?style=flat-square)](https://www.npmjs.com/package/@agile-ts/vue) | Vue Integration | +| [@agile-ts/api](/packages/api) | [![badge](https://img.shields.io/npm/v/@agile-ts/api.svg?style=flat-square)](https://www.npmjs.com/package/@agile-ts/api) | Promise based API | +| [@agile-ts/multieditor](/packages/multieditor) | [![badge](https://img.shields.io/npm/v/@agile-ts/multieditor.svg?style=flat-square)](https://www.npmjs.com/package/@agile-ts/multieditor) | Simple Form Manager | +| [@agile-ts/event](/packages/event) | [![badge](https://img.shields.io/npm/v/@agile-ts/event.svg?style=flat-square)](https://www.npmjs.com/package/@agile-ts/event) | Handy class for emitting UI Events | +| [@agile-ts/logger](/packages/logger) | [![badge](https://img.shields.io/npm/v/@agile-ts/logger.svg?style=flat-square)](https://www.npmjs.com/package/@agile-ts/logger) | Logging API of AgileTs | +| [@agile-ts/utils](/packages/utils) | [![badge](https://img.shields.io/npm/v/@agile-ts/utils.svg?style=flat-square)](https://www.npmjs.com/package/@agile-ts/utils) | Utilities of AgileTs | +| [@agile-ts/proxytree](/packages/proxytree) | [![badge](https://img.shields.io/npm/v/@agile-ts/proxytree.svg?style=flat-square)](https://www.npmjs.com/package/@agile-ts/proxytree) | Proxy Tree for tracking accessed properties | |
@@ -202,4 +232,5 @@ To find out more about contributing, check out the [CONTRIBUTING.md](https://git
Credits -AgileTs is inspired by [MVVM Frameworks](https://de.wikipedia.org/wiki/Model_View_ViewModel) like [MobX](https://mobx.js.org/README.html) and [PulseJs](https://github.com/pulse-framework/pulse). +AgileTs is inspired by [MVVM Frameworks](https://de.wikipedia.org/wiki/Model_View_ViewModel) +like [MobX](https://mobx.js.org/README.html) and [PulseJs](https://github.com/pulse-framework/pulse). diff --git a/benchmark/.env b/benchmark/.env new file mode 100644 index 00000000..21903adb --- /dev/null +++ b/benchmark/.env @@ -0,0 +1 @@ +MANUAL_BENCHMARK=false diff --git a/benchmark/README.md b/benchmark/README.md new file mode 100644 index 00000000..ce2e9db2 --- /dev/null +++ b/benchmark/README.md @@ -0,0 +1,136 @@ +# 🚀️ Benchmarks + +The `Benchmark Test Suites` are supposed to showcase where AgileTs is roughly located in terms of performance. +I know a counter doesn't really show real world app performance, +but it is better than nothing. + +### What do the results from benchmark js mean? +https://stackoverflow.com/questions/28524653/what-do-the-results-from-benchmark-js-mean + +## Counter Benchmark + +```ts +1. AgileTs.............43028 ops/se ±2.45 (63 runs sampled) +2. PulseJs.............41086 ops/se ±2.60 (63 runs sampled) +3. Nano Stores.........31933 ops/se ±1.27 (63 runs sampled) +4. Zustand.............29329 ops/se ±1.30 (62 runs sampled) +5. Redux...............28845 ops/se ±2.47 (61 runs sampled) +6. Hookstate...........27555 ops/se ±5.00 (59 runs sampled) +7. Mobx................27427 ops/se ±3.69 (62 runs sampled) +8. Redux-Toolkit.......22191 ops/se ±1.06 (65 runs sampled) +9. Jotai...............22157 ops/se ±4.10 (63 runs sampled) +10. Valtio..............21089 ops/se ±0.77 (63 runs sampled) +11. Recoil..............13926 ops/se ±2.12 (62 runs sampled) + +Fastest is AgileTs +``` + +## 1000 Fields + +```ts +// 1 Field +1. Agile nested State..27992 ops/se ±1.73 (64 runs sampled) +2. Pulse Collection....25547 ops/se ±1.04 (64 runs sampled) +3. Agile State.........23962 ops/se ±2.16 (64 runs sampled) +4. Nano Stores.........20662 ops/se ±1.76 (65 runs sampled) +5. Hookstate...........19430 ops/se ±1.81 (61 runs sampled) +6. Agile Collection....18491 ops/se ±2.13 (65 runs sampled) +7. Jotai...............16029 ops/se ±3.39 (62 runs sampled) +8. Mobx................15631 ops/se ±3.42 (61 runs sampled) +9. Redux...............12698 ops/se ±2.86 (61 runs sampled) +10. Recoil..............11183 ops/se ±3.73 (61 runs sampled) +11. Valtio..............9728 ops/se ±2.81 (62 runs sampled) + +Fastest is Agile nested State + +// 10 Fields +1. Agile nested State..27658 ops/se ±1.99 (64 runs sampled) +2. Pulse Collection....24839 ops/se ±1.31 (65 runs sampled) +3. Agile State.........19853 ops/se ±2.15 (64 runs sampled) +4. Nano Stores.........19479 ops/se ±2.12 (60 runs sampled) +5. Hookstate...........18104 ops/se ±3.37 (60 runs sampled) +6. Jotai...............15472 ops/se ±2.45 (62 runs sampled) +7. Agile Collection....13352 ops/se ±3.67 (61 runs sampled) +8. Recoil..............10522 ops/se ±3.79 (58 runs sampled) +9. Mobx................9477 ops/se ±1.94 (62 runs sampled) +10. Redux...............8434 ops/se ±2.67 (47 runs sampled) +11. Valtio..............3532 ops/se ±2.27 (23 runs sampled) + +Fastest is Agile nested State + +// 100 Fields +1. Agile nested State..24124 ops/se ±1.05 (65 runs sampled) +2. Pulse Collection....21912 ops/se ±1.35 (66 runs sampled) +3. Nano Stores.........15638 ops/se ±1.63 (62 runs sampled) +4. Hookstate...........13986 ops/se ±2.28 (59 runs sampled) +5. Jotai...............12167 ops/se ±2.78 (63 runs sampled) +6. Agile State.........9175 ops/se ±1.56 (51 runs sampled) +7. Recoil..............8717 ops/se ±3.51 (49 runs sampled) +8. Agile Collection....4177 ops/se ±1.64 (61 runs sampled) +9. Redux...............1763 ops/se ±1.06 (63 runs sampled) +10. Mobx................1699 ops/se ±1.82 (62 runs sampled) +11. Valtio..............432 ops/se ±2.18 (60 runs sampled) + +Fastest is Agile nested State + +// 1000 Fields +1. Agile nested State..10756 ops/se ±1.43 (58 runs sampled) +2. Pulse Collection....9774 ops/se ±2.39 (58 runs sampled) +3. Hookstate...........4737 ops/se ±4.33 (58 runs sampled) +4. Nano Stores.........4638 ops/se ±6.40 (28 runs sampled) +5. Jotai...............3352 ops/se ±4.17 (53 runs sampled) +6. Recoil..............3139 ops/se ±4.69 (54 runs sampled) +7. Agile State.........1389 ops/se ±1.52 (57 runs sampled) +8. Agile Collection....500 ops/se ±1.89 (61 runs sampled) +9. Redux...............154 ops/se ±1.48 (57 runs sampled) +10. Mobx................144 ops/se ±1.06 (55 runs sampled) +11. Valtio..............37 ops/se ±4.26 (40 runs sampled) + +Fastest is Agile nested State +``` + +## Computed + +```ts +1. Agile Hard Coded....32079 ops/se ±1.51 (62 runs sampled) +2. Agile Auto Tracking.30974 ops/se ±2.21 (64 runs sampled) +3. Nano Stores.........28821 ops/se ±1.49 (64 runs sampled) +4. Jotai...............18922 ops/se ±2.12 (61 runs sampled) +5. Recoil..............10103 ops/se ±2.47 (64 runs sampled) + +Fastest is Agile Hard Coded +``` + +## 🏃 Running Benchmarks + +The Benchmark tests run on top of the [`benchmark.js` library](https://github.com/bestiejs/benchmark.js/) +via [Playwright](https://github.com/microsoft/playwright) in the Chrome Browser. + +Before starting, make sure you are in the `/benchmark` directory. + +### 1️⃣ Install dependencies + +To prepare the dependencies, run: +```ts +yarn install +``` + +### 2️⃣ Run Benchmark Test Suite + +Execute the benchmark located in `./benchmarks/react/counter`. +```ts +yarn run test:counter +``` + +## ⭐️ Contribute + +Get a part of AgileTs and start contributing. We welcome any meaningful contribution. 😀 +To find out more about contributing, check out the [CONTRIBUTING.md](https://github.com/agile-ts/agile/blob/master/CONTRIBUTING.md). + + + Maintainability + + +## 🎉 Credits + +The Benchmark CLI is inspired by [`exome`](https://github.com/Marcisbee/exome). diff --git a/benchmark/benchmarkManager.ts b/benchmark/benchmarkManager.ts new file mode 100644 index 00000000..dd20e5a1 --- /dev/null +++ b/benchmark/benchmarkManager.ts @@ -0,0 +1,73 @@ +export interface CycleResultInterface { + name: string; + opsInSec: number; + failRate: number; + ranSampleCount: number; + ui: any; +} + +export function getCycleResult(event: any): CycleResultInterface { + return { + name: event.target.name, + opsInSec: Math.round(event.target.hz), + failRate: event.target.stats.rme.toFixed(2), + ranSampleCount: event.target.stats.sample.length, + ui: { + count: event.target.output, + }, + }; +} + +export function startBenchmarkLog(testSuiteName: string): void { + console.log(`{white Starting Benchmark "{magenta.bold ${testSuiteName}}"..}`); +} + +export function cycleLog( + cycleResult: CycleResultInterface, + ...addition: any[] +): void { + console.log( + `{gray ..Proceeded {green.bold ${cycleResult.name}} - {yellow ${cycleResult.opsInSec} ops/sec}}`, + ...addition + ); +} + +export function endBenchmarkLog( + testSuiteName: string, + results: CycleResultInterface[], + fastest: string[] +): void { + console.log(`{white ..End Benchmark "{magenta.bold ${testSuiteName}}"}\n\n`); + + results.sort((a, b) => { + if (a.opsInSec < b.opsInSec) return 1; + return -1; + }); + + let resultString = ''; + for (let i = 0; i < results.length; i++) { + const cycleResult = results[i]; + + // Build Cycle Result Log + const cycleString = `{bold.bgGreen ${ + i + 1 + }.} {bold.blue ${cycleResult.name + .padEnd(20, '.') + .replace(/(\.+)$/, '{red $1}')}}{yellow ${ + cycleResult.opsInSec + } ops/se} {gray ±${cycleResult.failRate}%} (${ + cycleResult.ranSampleCount + } runs sampled)`; + + resultString += `${cycleString}${i < results.length - 1 ? '\n' : ''}`; + } + + // Build Leaderboard Header + console.log('{bgYellow.white.bold Leaderboard:}\n'); + + // Print Leaderboard + console.log(resultString); + + // Print fastest + console.log(`\n{bold Fastest is {bold.green ${fastest}}}\n`); +} diff --git a/benchmark/benchmarks/react/1000fields/bench/agilets/collection.tsx b/benchmark/benchmarks/react/1000fields/bench/agilets/collection.tsx new file mode 100644 index 00000000..7269c0a0 --- /dev/null +++ b/benchmark/benchmarks/react/1000fields/bench/agilets/collection.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import ReactDom from 'react-dom'; +import { createCollection, LogCodeManager } 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) => ({ + id: i, + name: `Field #${i + 1}`, + })), + }); + + let updatedFieldsCount = 0; + let renderFieldsCount = 0; + + function Field({ index }: { index: number | string }) { + const ITEM = FIELDS.getItem(index); + const item = useAgile(ITEM); + + renderFieldsCount++; + + return ( +
+ Last {``} render at: {new Date().toISOString()} +   + { + ITEM?.patch({ name: e.target.value }); + + updatedFieldsCount++; + + (document.getElementById( + 'updatedFieldsCount' + ) as any).innerText = updatedFieldsCount; + (document.getElementById( + 'renderFieldsCount' + ) as any).innerText = renderFieldsCount; + }} + /> +
+ ); + } + + function App() { + const fieldKeys = useValue(FIELDS); + + return ( +
+
+ Last {``} render at: {new Date().toISOString()} +
+
+ {fieldKeys.map((key, i) => ( + + ))} +
+
+
+ ); + } + + ReactDom.render(, target); +} diff --git a/benchmark/benchmarks/react/1000fields/bench/agilets/nestedState.tsx b/benchmark/benchmarks/react/1000fields/bench/agilets/nestedState.tsx new file mode 100644 index 00000000..377b6b01 --- /dev/null +++ b/benchmark/benchmarks/react/1000fields/bench/agilets/nestedState.tsx @@ -0,0 +1,64 @@ +import * as React from 'react'; +import * as ReactDom from 'react-dom'; +import { createState, LogCodeManager, 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) => + createState(`Field #${i + 1}`) + ) + ); + + let updatedFieldsCount = 0; + let renderFieldsCount = 0; + + function Field({ field }: { field: State }) { + const name = useAgile(field); + + renderFieldsCount++; + + return ( +
+ Last {``} render at: {new Date().toISOString()} +   + { + field.set(e.target.value); + + updatedFieldsCount++; + + (document.getElementById( + 'updatedFieldsCount' + ) as any).innerText = updatedFieldsCount; + (document.getElementById( + 'renderFieldsCount' + ) as any).innerText = renderFieldsCount; + }} + /> +
+ ); + } + + function App() { + const fields = useAgile(FIELDS); + return ( +
+
+ Last {``} render at: {new Date().toISOString()} +
+
+ {fields.map((field, index) => ( + + ))} +
+
+
+ ); + } + + ReactDom.render(, target); +} diff --git a/benchmark/benchmarks/react/1000fields/bench/agilets/state.tsx b/benchmark/benchmarks/react/1000fields/bench/agilets/state.tsx new file mode 100644 index 00000000..93f9798a --- /dev/null +++ b/benchmark/benchmarks/react/1000fields/bench/agilets/state.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import ReactDom from 'react-dom'; +import { createState, LogCodeManager } 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}`) + ); + + let updatedFieldsCount = 0; + let renderFieldsCount = 0; + + function Field({ index }: { index: number }) { + const fields = useAgile(FIELDS); + + renderFieldsCount++; + + return ( +
+ Last {``} render at: {new Date().toISOString()} +   + { + FIELDS.nextStateValue[index] = e.target.value; + FIELDS.ingest(); + + updatedFieldsCount++; + + (document.getElementById( + 'updatedFieldsCount' + ) as any).innerText = updatedFieldsCount; + (document.getElementById( + 'renderFieldsCount' + ) as any).innerText = renderFieldsCount; + }} + /> +
+ ); + } + + function App() { + return ( +
+
+ Last {``} render at: {new Date().toISOString()} +
+
+ {FIELDS.value.map((field, index) => ( + + ))} +
+
+
+ ); + } + + ReactDom.render(, target); +} diff --git a/benchmark/benchmarks/react/1000fields/bench/hookstate.tsx b/benchmark/benchmarks/react/1000fields/bench/hookstate.tsx new file mode 100644 index 00000000..a64009fb --- /dev/null +++ b/benchmark/benchmarks/react/1000fields/bench/hookstate.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import ReactDom from 'react-dom'; +import { createState, useHookstate, State } from '@hookstate/core'; + +export default function (target: HTMLElement, fieldsCount: number) { + const fields = createState( + Array.from( + Array.from(Array(fieldsCount).keys()).map((i) => `Field #${i + 1} value`) + ) + ); + + let updatedFieldsCount = 0; + let renderFieldsCount = 0; + + function Field({ field }: { field: State }) { + const name = useHookstate(field); + + renderFieldsCount++; + + return ( +
+ Last {``} render at: {new Date().toISOString()} +   + { + name.set(e.target.value); + + updatedFieldsCount++; + + (document.getElementById( + 'updatedFieldsCount' + ) as any).innerText = updatedFieldsCount; + (document.getElementById( + 'renderFieldsCount' + ) as any).innerText = renderFieldsCount; + }} + /> +
+ ); + } + + function App() { + const state = useHookstate(fields); + return ( +
+
+ Last {``} render at: {new Date().toISOString()} +
+
+ {state.map((field, index) => ( + + ))} +
+
+
+ ); + } + + ReactDom.render(, target); +} diff --git a/benchmark/benchmarks/react/1000fields/bench/jotai.tsx b/benchmark/benchmarks/react/1000fields/bench/jotai.tsx new file mode 100644 index 00000000..3823e908 --- /dev/null +++ b/benchmark/benchmarks/react/1000fields/bench/jotai.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import ReactDom from 'react-dom'; +import { atom, useAtom, Atom } from 'jotai'; + +export default function (target: HTMLElement, fieldsCount: number) { + const fields = Array.from(Array(fieldsCount).keys()).map((i) => + atom(`Field #${i + 1}`) + ); + + const fieldsStore = atom(fields); + + let updatedFieldsCount = 0; + let renderFieldsCount = 0; + + function Field({ field }: { field: Atom }) { + const [name, rename] = useAtom(field); + + renderFieldsCount++; + + return ( +
+ Last {``} render at: {new Date().toISOString()} +   + { + rename(e.target.value); + + updatedFieldsCount++; + + (document.getElementById( + 'updatedFieldsCount' + ) as any).innerText = updatedFieldsCount; + (document.getElementById( + 'renderFieldsCount' + ) as any).innerText = renderFieldsCount; + }} + /> +
+ ); + } + + function App() { + const [fields] = useAtom(fieldsStore); + + return ( +
+
+ Last {``} render at: {new Date().toISOString()} +
+
+ {fields.map((field, index) => ( + + ))} +
+
+
+ ); + } + + ReactDom.render(, target); +} diff --git a/benchmark/benchmarks/react/1000fields/bench/mobx.tsx b/benchmark/benchmarks/react/1000fields/bench/mobx.tsx new file mode 100644 index 00000000..d8b05198 --- /dev/null +++ b/benchmark/benchmarks/react/1000fields/bench/mobx.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import ReactDom from 'react-dom'; +import { action, observable } from 'mobx'; +import { observer } from 'mobx-react'; + +export default function (target: HTMLElement, fieldsCount: number) { + const appState = observable({ + fields: Array.from(Array(fieldsCount).keys()).map((i) => `Field #${i + 1}`), + rename: action(function (value: string, index: number) { + // console.log(state) + appState.fields[index] = value; + }), + }); + + let updatedFieldsCount = 0; + let renderFieldsCount = 0; + + function Field({ index }: { index: number }) { + const field = appState.fields[index]; + + renderFieldsCount++; + + return ( +
+ Last {``} render at: {new Date().toISOString()} +   + { + appState.rename(e.target.value, index); + + updatedFieldsCount++; + + (document.getElementById( + 'updatedFieldsCount' + ) as any).innerText = updatedFieldsCount; + (document.getElementById( + 'renderFieldsCount' + ) as any).innerText = renderFieldsCount; + }} + /> +
+ ); + } + + const App = observer(() => { + return ( +
+
+ Last {``} render at: {new Date().toISOString()} +
+
+ {appState.fields.map((field, index) => ( + + ))} +
+
+
+ ); + }); + + ReactDom.render(, target); +} diff --git a/benchmark/benchmarks/react/1000fields/bench/nanostores.tsx b/benchmark/benchmarks/react/1000fields/bench/nanostores.tsx new file mode 100644 index 00000000..4fa38246 --- /dev/null +++ b/benchmark/benchmarks/react/1000fields/bench/nanostores.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import ReactDom from 'react-dom'; +import { createStore, WritableStore } from 'nanostores'; +import { useStore } from 'nanostores/react'; + +export default function (target: HTMLElement, fieldsCount: number) { + const fieldsStore = createStore[]>(() => { + const fields = Array.from(Array(fieldsCount).keys()).map((i) => { + const fieldStore = createStore(() => { + fieldsStore.set(`Field #${i + 1}` as any); + }); + return fieldStore; + }); + + fieldsStore.set(fields); + }); + + let updatedFieldsCount = 0; + let renderFieldsCount = 0; + + function Field({ field }: { field: WritableStore }) { + const name = useStore(field); + + renderFieldsCount++; + + return ( +
+ Last {``} render at: {new Date().toISOString()} +   + { + field.set(e.target.value); + + updatedFieldsCount++; + + (document.getElementById( + 'updatedFieldsCount' + ) as any).innerText = updatedFieldsCount; + (document.getElementById( + 'renderFieldsCount' + ) as any).innerText = renderFieldsCount; + }} + /> +
+ ); + } + + function App() { + const fields = useStore(fieldsStore); + return ( +
+
+ Last {``} render at: {new Date().toISOString()} +
+
+ {fields.map((field, index) => ( + + ))} +
+
+
+ ); + } + + ReactDom.render(, target); +} diff --git a/benchmark/benchmarks/react/1000fields/bench/pulsejs/collection.tsx b/benchmark/benchmarks/react/1000fields/bench/pulsejs/collection.tsx new file mode 100644 index 00000000..078a7ab9 --- /dev/null +++ b/benchmark/benchmarks/react/1000fields/bench/pulsejs/collection.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import ReactDom from 'react-dom'; +import { collection, usePulse } from '@pulsejs/react'; + +export default function (target: HTMLElement, fieldsCount: number) { + const FIELDS = collection<{ id: number; name: string }>(); + FIELDS.collect( + Array.from(Array(fieldsCount).keys()).map((i) => ({ + id: i, + name: `Field #${i + 1}`, + })) + ); + + let updatedFieldsCount = 0; + let renderFieldsCount = 0; + + function Field({ index }: { index: number | string }) { + const ITEM = FIELDS.getData(index); + const item = usePulse(ITEM); + + renderFieldsCount++; + + return ( +
+ Last {``} render at: {new Date().toISOString()} +   + { + ITEM?.patch({ name: e.target.value }); + + updatedFieldsCount++; + + (document.getElementById( + 'updatedFieldsCount' + ) as any).innerText = updatedFieldsCount; + (document.getElementById( + 'renderFieldsCount' + ) as any).innerText = renderFieldsCount; + }} + /> +
+ ); + } + + function App() { + return ( +
+
+ Last {``} render at: {new Date().toISOString()} +
+
+ {Object.keys(FIELDS.data).map((key, i) => ( + + ))} +
+
+
+ ); + } + + ReactDom.render(, target); +} diff --git a/benchmark/benchmarks/react/1000fields/bench/pulsejs/nestedState.tsx b/benchmark/benchmarks/react/1000fields/bench/pulsejs/nestedState.tsx new file mode 100644 index 00000000..d09f86f5 --- /dev/null +++ b/benchmark/benchmarks/react/1000fields/bench/pulsejs/nestedState.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import ReactDom from 'react-dom'; +import { state, State } from '@pulsejs/core'; +import { usePulse } from '@pulsejs/react'; + +export default function (target: HTMLElement, fieldsCount: number) { + const FIELDS = state( + Array.from(Array(fieldsCount).keys()).map((i) => state(`Field #${i + 1}`)) + ); + + let updatedFieldsCount = 0; + let renderFieldsCount = 0; + + function Field({ field }: { field: State }) { + const name = usePulse(field); + + renderFieldsCount++; + + return ( +
+ Last {``} render at: {new Date().toISOString()} +   + { + field.set(e.target.value); + + updatedFieldsCount++; + + (document.getElementById( + 'updatedFieldsCount' + ) as any).innerText = updatedFieldsCount; + (document.getElementById( + 'renderFieldsCount' + ) as any).innerText = renderFieldsCount; + }} + /> +
+ ); + } + + function App() { + const fields = usePulse(FIELDS); + return ( +
+
+ Last {``} render at: {new Date().toISOString()} +
+
+ {fields.map((field, index) => ( + + ))} +
+
+
+ ); + } + + ReactDom.render(, target); +} diff --git a/benchmark/benchmarks/react/1000fields/bench/pulsejs/state.tsx b/benchmark/benchmarks/react/1000fields/bench/pulsejs/state.tsx new file mode 100644 index 00000000..11e55fe7 --- /dev/null +++ b/benchmark/benchmarks/react/1000fields/bench/pulsejs/state.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import ReactDom from 'react-dom'; +import { state } from '@pulsejs/core'; +import { usePulse } from '@pulsejs/react'; + +export default function (target: HTMLElement, fieldsCount: number) { + const FIELDS = state( + Array.from(Array(fieldsCount).keys()).map((i) => `Field #${i + 1}`) + ); + + let updatedFieldsCount = 0; + let renderFieldsCount = 0; + + function Field({ index }: { index: number }) { + const fields = usePulse(FIELDS); + + renderFieldsCount++; + + return ( +
+ Last {``} render at: {new Date().toISOString()} +   + { + FIELDS.nextState[index] = e.target.value; + FIELDS.set(); + + updatedFieldsCount++; + + (document.getElementById( + 'updatedFieldsCount' + ) as any).innerText = updatedFieldsCount; + (document.getElementById( + 'renderFieldsCount' + ) as any).innerText = renderFieldsCount; + }} + /> +
+ ); + } + + function App() { + return ( +
+
+ Last {``} render at: {new Date().toISOString()} +
+
+ {FIELDS.value.map((field, index) => ( + + ))} +
+
+
+ ); + } + + ReactDom.render(, target); +} diff --git a/benchmark/benchmarks/react/1000fields/bench/recoil.tsx b/benchmark/benchmarks/react/1000fields/bench/recoil.tsx new file mode 100644 index 00000000..c6e194ab --- /dev/null +++ b/benchmark/benchmarks/react/1000fields/bench/recoil.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import ReactDom from 'react-dom'; +import { atom, RecoilRoot, RecoilState, useRecoilState } from 'recoil'; + +export default function (target: HTMLElement, fieldsCount: number) { + const fields = Array.from(Array(fieldsCount).keys()).map((i) => + atom({ key: `field-${i}`, default: `Field #${i + 1}` }) + ); + + const fieldsStore = atom({ + key: 'fieldsStore', + default: fields, + }); + + let updatedFieldsCount = 0; + let renderFieldsCount = 0; + + function Field({ field }: { field: RecoilState }) { + const [name, rename] = useRecoilState(field); + + renderFieldsCount++; + + return ( +
+ Last {``} render at: {new Date().toISOString()} +   + { + rename(e.target.value); + + updatedFieldsCount++; + + (document.getElementById( + 'updatedFieldsCount' + ) as any).innerText = updatedFieldsCount; + (document.getElementById( + 'renderFieldsCount' + ) as any).innerText = renderFieldsCount; + }} + /> +
+ ); + } + + function App() { + const [fields] = useRecoilState(fieldsStore); + + return ( +
+
+ Last {``} render at: {new Date().toISOString()} +
+
+ {fields.map((field, index) => ( + + ))} +
+
+
+ ); + } + + ReactDom.render( + + + , + target + ); +} diff --git a/benchmark/benchmarks/react/1000fields/bench/redux.tsx b/benchmark/benchmarks/react/1000fields/bench/redux.tsx new file mode 100644 index 00000000..ac134f70 --- /dev/null +++ b/benchmark/benchmarks/react/1000fields/bench/redux.tsx @@ -0,0 +1,87 @@ +import React from 'react'; +import ReactDom from 'react-dom'; +import { configureStore, createSlice } from '@reduxjs/toolkit'; +import { Provider, useDispatch, useSelector } from 'react-redux'; + +export default function (target: HTMLElement, fieldsCount: number) { + const fieldsSlice = createSlice({ + name: 'fields', + initialState: { + fields: Array.from(Array(fieldsCount).keys()).map( + (i) => `Field #${i + 1}` + ), + }, + reducers: { + rename: (state, action) => { + state.fields[action.payload.index] = action.payload.value; + }, + }, + }); + + const store = configureStore({ + reducer: { + fields: fieldsSlice.reducer, + }, + }); + + let updatedFieldsCount = 0; + let renderFieldsCount = 0; + + function Field({ index, field: name }: { index: number; field: string }) { + const dispatch = useDispatch(); + + renderFieldsCount++; + + return ( +
+ Last {``} render at: {new Date().toISOString()} +   + { + dispatch( + fieldsSlice.actions.rename({ index, value: e.target.value }) + ); + + updatedFieldsCount++; + + (document.getElementById( + 'updatedFieldsCount' + ) as any).innerText = updatedFieldsCount; + (document.getElementById( + 'renderFieldsCount' + ) as any).innerText = renderFieldsCount; + }} + /> +
+ ); + } + + function App() { + const fields: string[] = useSelector( + (state: any) => state.fields.fields, + () => false + ); + + return ( +
+
+ Last {``} render at: {new Date().toISOString()} +
+
+ {fields.map((field, index) => ( + + ))} +
+
+
+ ); + } + + ReactDom.render( + + + , + target + ); +} diff --git a/benchmark/benchmarks/react/1000fields/bench/valtio.tsx b/benchmark/benchmarks/react/1000fields/bench/valtio.tsx new file mode 100644 index 00000000..b27698b0 --- /dev/null +++ b/benchmark/benchmarks/react/1000fields/bench/valtio.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import ReactDom from 'react-dom'; +import { proxy, useSnapshot } from 'valtio'; + +export default function (target: HTMLElement, fieldsCount: number) { + const state = proxy({ + fields: Array.from(Array(fieldsCount).keys()).map((i) => `Field #${i + 1}`), + }); + + let updatedFieldsCount = 0; + let renderFieldsCount = 0; + + function Field({ index }: { index: number }) { + const { fields } = useSnapshot(state); + const name = fields[index]; + + renderFieldsCount++; + + return ( +
+ Last {``} render at: {new Date().toISOString()} +   + { + state.fields[index] = e.target.value; + + updatedFieldsCount++; + + (document.getElementById( + 'updatedFieldsCount' + ) as any).innerText = updatedFieldsCount; + (document.getElementById( + 'renderFieldsCount' + ) as any).innerText = renderFieldsCount; + }} + /> +
+ ); + } + + function App() { + const { fields } = useSnapshot(state, { sync: true }); + + return ( +
+
+ Last {``} render at: {new Date().toISOString()} +
+
+ {fields.map((field, index) => ( + + ))} +
+
+
+ ); + } + + ReactDom.render(, target); +} diff --git a/benchmark/benchmarks/react/1000fields/index.ts b/benchmark/benchmarks/react/1000fields/index.ts new file mode 100644 index 00000000..9b251c1d --- /dev/null +++ b/benchmark/benchmarks/react/1000fields/index.ts @@ -0,0 +1,115 @@ +import ReactDOM from 'react-dom'; +import Benchmark, { Suite, Options } from 'benchmark'; +import { + cycleLog, + CycleResultInterface, + endBenchmarkLog, + getCycleResult, + startBenchmarkLog, +} from '../../../benchmarkManager'; + +// Files to run the Benchmark on +import agileCollection from './bench/agilets/collection'; +import agileState from './bench/agilets/state'; +import agileNestedState from './bench/agilets/nestedState'; +import pulseCollection from './bench/pulsejs/collection'; +import pulseState from './bench/pulsejs/state'; +import pulseNestedState from './bench/pulsejs/nestedState'; +import hookstate from './bench/hookstate'; +import jotai from './bench/jotai'; +import mobx from './bench/mobx'; +import nanostores from './bench/nanostores'; +import recoil from './bench/recoil'; +import redux from './bench/redux'; +import valtio from './bench/valtio'; + +// @ts-ignore +// Benchmark.js requires an instance of itself globally +window.Benchmark = Benchmark; + +const fieldsCount = 1000; + +// Create new Benchmark Test Suite +const suite = new Suite(`${fieldsCount} Fields`); + +// Retrieve the Element to render the Benchmark Test Suite in +const target = document.getElementById('bench')!; + +// Configures a single Benchmark Test +function configTest( + renderElement: (target: HTMLElement, fieldsCount: number) => void +): Options { + return { + fn() { + // Retrieve Input field to update + const fieldToUpdate = Math.floor(Math.random() * fieldsCount); + const input = target.querySelectorAll('input')[fieldToUpdate]; + + // Update retrieved Input value + const evt = document.createEvent('HTMLEvents'); + evt.initEvent('input', true, true); + input.value = '' + Math.random(); + (input as any)._valueTracker.setValue(Math.random()); + input.dispatchEvent(evt); + }, + onStart() { + // Render React Component in the target Element + renderElement(target, fieldsCount); + }, + onComplete() { + (this as any).updatedFieldsCount = parseInt( + (document.getElementById('updatedFieldsCount') as any)?.innerText, + 10 + ); + (this as any).renderFieldsCount = parseInt( + (document.getElementById('renderFieldsCount') as any)?.innerText, + 10 + ); + + // Unmount React Component + ReactDOM.unmountComponentAtNode(target); + target.innerHTML = ''; + }, + }; +} + +const results: CycleResultInterface[] = []; + +// Add Tests to the Benchmark Test Suite +suite + .add('Agile Collection', configTest(agileCollection)) + .add('Agile State', configTest(agileState)) + .add('Agile nested State', configTest(agileNestedState)) + .add('Pulse Collection', configTest(pulseCollection)) + // .add('Pulse State', configTest(pulseState)) + // .add('Pulse nested State', configTest(pulseNestedState)) + .add('Hookstate', configTest(hookstate)) + .add('Jotai', configTest(jotai)) + .add('Mobx', configTest(mobx)) + .add('Nano Stores', configTest(nanostores)) + .add('Recoil', configTest(recoil)) + .add('Redux', configTest(redux)) + .add('Valtio', configTest(valtio)) + + // Add Listener + .on('start', function (this: any) { + startBenchmarkLog(this.name); + }) + .on('cycle', (event: any) => { + const cycleResult = getCycleResult(event); + cycleLog( + cycleResult, + `[updatedFieldsCount: ${event.target.updatedFieldsCount}, renderFieldsCount: ${event.target.renderFieldsCount}]` + ); + results.push(cycleResult); + }) + .on('complete', function (this: any) { + endBenchmarkLog(this.name, results, this.filter('fastest').map('name')); + + // @ts-ignore + // Notify server that the Benchmark Test Suite has ended + window.TEST.ended = true; + }) + + // Run Benchmark Test Suite + .run({ async: true }); diff --git a/benchmark/benchmarks/react/computed/bench/agilets/autoTracking.tsx b/benchmark/benchmarks/react/computed/bench/agilets/autoTracking.tsx new file mode 100644 index 00000000..0b596405 --- /dev/null +++ b/benchmark/benchmarks/react/computed/bench/agilets/autoTracking.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import ReactDom from 'react-dom'; +import { createComputed, createState, LogCodeManager } 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; +}); + +const CountView = () => { + const count = useAgile(COUNT); + return

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

; +}; + +const ComputedCountView = () => { + const computedCount = useAgile(COMPUTED_COUNT); + return

{computedCount}

; +}; + +const App = () => { + return ( +
+ + +
+ ); +}; + +export default function (target: HTMLElement) { + ReactDom.render(, target); +} diff --git a/benchmark/benchmarks/react/computed/bench/agilets/hardCoded.tsx b/benchmark/benchmarks/react/computed/bench/agilets/hardCoded.tsx new file mode 100644 index 00000000..59cc57a8 --- /dev/null +++ b/benchmark/benchmarks/react/computed/bench/agilets/hardCoded.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import ReactDom from 'react-dom'; +import { createComputed, createState, LogCodeManager } 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; + }, + { autodetect: false, computedDeps: [COUNT] } +); + +const CountView = () => { + const count = useAgile(COUNT); + return

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

; +}; + +const ComputedCountView = () => { + const computedCount = useAgile(COMPUTED_COUNT); + return

{computedCount}

; +}; + +const App = () => { + return ( +
+ + +
+ ); +}; + +export default function (target: HTMLElement) { + ReactDom.render(, target); +} diff --git a/benchmark/benchmarks/react/computed/bench/jotai.tsx b/benchmark/benchmarks/react/computed/bench/jotai.tsx new file mode 100644 index 00000000..6de63920 --- /dev/null +++ b/benchmark/benchmarks/react/computed/bench/jotai.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import ReactDom from 'react-dom'; +import { atom, useAtom } from 'jotai'; + +const countAtom = atom(0); +const computedCountAtom = atom((get) => get(countAtom) * 5); + +const CountView = () => { + const [count, setCount] = useAtom(countAtom); + return

setCount((v) => v + 1)}>{count}

; +}; + +const ComputedCountView = () => { + const [computedCount] = useAtom(computedCountAtom); + return

{computedCount}

; +}; + +const App = () => { + return ( +
+ + +
+ ); +}; + +export default function (target: HTMLElement) { + ReactDom.render(, target); +} diff --git a/benchmark/benchmarks/react/computed/bench/nanostores.tsx b/benchmark/benchmarks/react/computed/bench/nanostores.tsx new file mode 100644 index 00000000..5ac7c0d2 --- /dev/null +++ b/benchmark/benchmarks/react/computed/bench/nanostores.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import ReactDom from 'react-dom'; +import { createDerived, createStore, getValue } from 'nanostores'; +import { useStore } from 'nanostores/react'; + +const countStore = createStore(() => { + countStore.set(0); +}); +const computedStore = createDerived(countStore, (count) => { + return count * 5; +}); + +const CountView = () => { + const count = useStore(countStore); + return ( +

countStore.set(getValue(countStore) + 1)}>{count}

+ ); +}; + +const ComputedCountView = () => { + const computedCount = useStore(computedStore); + return

{computedCount}

; +}; + +const App = () => { + return ( +
+ + +
+ ); +}; + +export default function (target: HTMLElement) { + ReactDom.render(, target); +} diff --git a/benchmark/benchmarks/react/computed/bench/recoil.tsx b/benchmark/benchmarks/react/computed/bench/recoil.tsx new file mode 100644 index 00000000..8b275a0e --- /dev/null +++ b/benchmark/benchmarks/react/computed/bench/recoil.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import ReactDom from 'react-dom'; +import { + atom, + RecoilRoot, + useRecoilState, + selector, + useRecoilValue, +} from 'recoil'; +import { useAtom } from 'jotai'; + +const countState = atom({ + key: 'countState', + default: 0, +}); +const computedCountState = selector({ + key: 'computedCountState', + get: ({ get }) => { + return get(countState) * 5; + }, +}); + +const CountView = () => { + const [count, setCount] = useRecoilState(countState); + return

setCount((v) => v + 1)}>{count}

; +}; + +const ComputedCountView = () => { + const computedCount = useRecoilValue(computedCountState); + return

{computedCount}

; +}; + +const App = () => { + return ( +
+ + +
+ ); +}; + +export default function (target: HTMLElement) { + ReactDom.render( + + + , + target + ); +} diff --git a/benchmark/benchmarks/react/computed/index.ts b/benchmark/benchmarks/react/computed/index.ts new file mode 100644 index 00000000..e70b9c70 --- /dev/null +++ b/benchmark/benchmarks/react/computed/index.ts @@ -0,0 +1,94 @@ +import ReactDOM from 'react-dom'; +import Benchmark, { Suite, Options } from 'benchmark'; +import { + cycleLog, + CycleResultInterface, + endBenchmarkLog, + getCycleResult, + startBenchmarkLog, +} from '../../../benchmarkManager'; + +// Files to run the Benchmark on +import agileAutoTracking from './bench/agilets/autoTracking'; +import agileHardCoded from './bench/agilets/hardCoded'; +import jotai from './bench/jotai'; +import nanostores from './bench/nanostores'; +import recoil from './bench/recoil'; + +// @ts-ignore +// Benchmark.js requires an instance of itself globally +window.Benchmark = Benchmark; + +// Create new Benchmark Test Suite +const suite = new Suite('Computed'); + +// Retrieve the Element to render the Benchmark Test Suite in +const target = document.getElementById('bench')!; + +// Increment Element +let increment: HTMLHeadingElement; + +// Configures a single Benchmark Test +function configTest(renderElement: (target: HTMLElement) => void): Options { + return { + fn() { + // Execute increment action + increment.click(); + }, + onStart() { + // Render React Component in the target Element + renderElement(target); + + // Retrieve Element to execute the increment action on + increment = target.querySelector('h1')!; + }, + onComplete() { + // Set 'output' in the Benchmark itself to print it later + (this as any).output = parseInt( + (target.querySelector('h1') as any)?.innerText, + 10 + ); + (this as any).computedOutput = parseInt( + (target.querySelector('p') as any)?.innerText, + 10 + ); + + // Unmount React Component + ReactDOM.unmountComponentAtNode(target); + target.innerHTML = ''; + }, + }; +} + +const results: CycleResultInterface[] = []; + +// Add Tests to the Benchmark Test Suite +suite + .add('Agile Auto Tracking', configTest(agileAutoTracking)) + .add('Agile Hard Coded', configTest(agileHardCoded)) + .add('Jotai', configTest(jotai)) + .add('Nano Stores', configTest(nanostores)) + .add('Recoil', configTest(recoil)) + + // Add Listener + .on('start', function (this: any) { + startBenchmarkLog(this.name); + }) + .on('cycle', (event: any) => { + const cycleResult = getCycleResult(event); + cycleLog( + cycleResult, + `[Count: ${event.target.output}, ComputedCount: ${event.target.computedOutput}]` + ); + results.push(cycleResult); + }) + .on('complete', function (this: any) { + endBenchmarkLog(this.name, results, this.filter('fastest').map('name')); + + // @ts-ignore + // Notify server that the Benchmark Test Suite has ended + window.TEST.ended = true; + }) + + // Run Benchmark Test Suite + .run({ async: true }); diff --git a/benchmark/benchmarks/react/counter/bench/agilets.tsx b/benchmark/benchmarks/react/counter/bench/agilets.tsx new file mode 100644 index 00000000..309b2f2a --- /dev/null +++ b/benchmark/benchmarks/react/counter/bench/agilets.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import ReactDom from 'react-dom'; +import { createState, LogCodeManager } from '@agile-ts/core'; +import { useAgile } from '@agile-ts/react'; + +LogCodeManager.getLogger().isActive = false; +const COUNT = createState(0); + +const App = () => { + const count = useAgile(COUNT, undefined); + return

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

; +}; + +export default function (target: HTMLElement) { + ReactDom.render(, target); +} diff --git a/benchmark/benchmarks/react/counter/bench/hookstate.tsx b/benchmark/benchmarks/react/counter/bench/hookstate.tsx new file mode 100644 index 00000000..e3f12268 --- /dev/null +++ b/benchmark/benchmarks/react/counter/bench/hookstate.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import ReactDom from 'react-dom'; +import { createState, useHookstate } from '@hookstate/core'; + +const counter = createState(0); + +const App = () => { + const state = useHookstate(counter); + return

state.set((v) => v + 1)}>{state.get()}

; +}; + +export default function (target: HTMLElement) { + ReactDom.render(, target); +} diff --git a/benchmark/benchmarks/react/counter/bench/jotai.tsx b/benchmark/benchmarks/react/counter/bench/jotai.tsx new file mode 100644 index 00000000..cb256aba --- /dev/null +++ b/benchmark/benchmarks/react/counter/bench/jotai.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import ReactDom from 'react-dom'; +import { atom, useAtom } from 'jotai'; + +const countAtom = atom(0); + +const App = () => { + const [count, setCount] = useAtom(countAtom); + return

setCount((v) => v + 1)}>{count}

; +}; + +export default function (target: HTMLElement) { + ReactDom.render(, target); +} diff --git a/benchmark/benchmarks/react/counter/bench/mobx.tsx b/benchmark/benchmarks/react/counter/bench/mobx.tsx new file mode 100644 index 00000000..c597bf9a --- /dev/null +++ b/benchmark/benchmarks/react/counter/bench/mobx.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import ReactDom from 'react-dom'; +import { action, observable } from 'mobx'; +import { observer } from 'mobx-react'; + +const appState = observable({ + count: 0, + increment: action(function () { + appState.count += 1; + }), +}); + +const App = observer(() => { + return

{appState.count}

; +}); + +export default function (target: HTMLElement) { + ReactDom.render(, target); +} diff --git a/benchmark/benchmarks/react/counter/bench/nanostores.tsx b/benchmark/benchmarks/react/counter/bench/nanostores.tsx new file mode 100644 index 00000000..9b377c7b --- /dev/null +++ b/benchmark/benchmarks/react/counter/bench/nanostores.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import ReactDom from 'react-dom'; +import { createStore, getValue } from 'nanostores'; +import { useStore } from 'nanostores/react'; + +const countStore = createStore(() => { + countStore.set(0); +}); + +const App = () => { + const count = useStore(countStore); + return ( +

countStore.set(getValue(countStore) + 1)}>{count}

+ ); +}; + +export default function (target: HTMLElement) { + ReactDom.render(, target); +} diff --git a/benchmark/benchmarks/react/counter/bench/pulsejs.tsx b/benchmark/benchmarks/react/counter/bench/pulsejs.tsx new file mode 100644 index 00000000..f34e2d38 --- /dev/null +++ b/benchmark/benchmarks/react/counter/bench/pulsejs.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import ReactDom from 'react-dom'; +import { state } from '@pulsejs/core'; +import { usePulse } from '@pulsejs/react'; + +const COUNT = state(0); + +const App = () => { + const count = usePulse(COUNT); + return

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

; +}; + +export default function (target: HTMLElement) { + ReactDom.render(, target); +} diff --git a/benchmark/benchmarks/react/counter/bench/recoil.tsx b/benchmark/benchmarks/react/counter/bench/recoil.tsx new file mode 100644 index 00000000..d857e23d --- /dev/null +++ b/benchmark/benchmarks/react/counter/bench/recoil.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import ReactDom from 'react-dom'; +import { atom, RecoilRoot, useRecoilState } from 'recoil'; + +const counterState = atom({ + key: 'counterState', + default: 0, +}); + +const App = () => { + const [count, setCount] = useRecoilState(counterState); + return

setCount((v) => v + 1)}>{count}

; +}; + +export default function (target: HTMLElement) { + ReactDom.render( + + + , + target + ); +} diff --git a/benchmark/benchmarks/react/counter/bench/redux-toolkit.tsx b/benchmark/benchmarks/react/counter/bench/redux-toolkit.tsx new file mode 100644 index 00000000..1fbbedc6 --- /dev/null +++ b/benchmark/benchmarks/react/counter/bench/redux-toolkit.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import ReactDom from 'react-dom'; +import { configureStore, createSlice } from '@reduxjs/toolkit'; +import { Provider, useDispatch, useSelector } from 'react-redux'; + +const counterSlice = createSlice({ + name: 'counter', + initialState: { + count: 0, + }, + reducers: { + increment: (state) => { + state.count += 1; + }, + }, +}); + +const store = configureStore({ + reducer: { + counter: counterSlice.reducer, + }, +}); + +const App = () => { + const count = useSelector((state: any) => state.counter.count); + const dispatch = useDispatch(); + + return ( +

dispatch(counterSlice.actions.increment())}>{count}

+ ); +}; + +export default function (target: HTMLElement) { + ReactDom.render( + + + , + target + ); +} diff --git a/benchmark/benchmarks/react/counter/bench/redux.tsx b/benchmark/benchmarks/react/counter/bench/redux.tsx new file mode 100644 index 00000000..72b5c604 --- /dev/null +++ b/benchmark/benchmarks/react/counter/bench/redux.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import ReactDom from 'react-dom'; +import { combineReducers, createStore } from 'redux'; +import { Provider, useDispatch, useSelector } from 'react-redux'; + +const increment = () => { + return { + type: 'INCREMENT', + }; +}; + +const counter = (state = 0, action: any) => { + switch (action.type) { + case 'INCREMENT': + return state + 1; + default: + return state; + } +}; + +const rootReducer = combineReducers({ + counter, +}); + +const store = createStore(rootReducer); + +const App = () => { + const count = useSelector((state: any) => state.counter); + const dispatch = useDispatch(); + + return

dispatch(increment())}>{count}

; +}; + +export default function (target: HTMLElement) { + ReactDom.render( + + + , + target + ); +} diff --git a/benchmark/benchmarks/react/counter/bench/valtio.tsx b/benchmark/benchmarks/react/counter/bench/valtio.tsx new file mode 100644 index 00000000..682a3211 --- /dev/null +++ b/benchmark/benchmarks/react/counter/bench/valtio.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import ReactDom from 'react-dom'; +import { proxy, useSnapshot } from 'valtio'; + +const state = proxy({ count: 0 }); + +function App() { + const snapshot = useSnapshot(state, { sync: true }); + return

state.count++}>{snapshot.count}

; +} + +export default function (target: HTMLElement) { + ReactDom.render(, target); +} diff --git a/benchmark/benchmarks/react/counter/bench/zustand.tsx b/benchmark/benchmarks/react/counter/bench/zustand.tsx new file mode 100644 index 00000000..ac73a2b5 --- /dev/null +++ b/benchmark/benchmarks/react/counter/bench/zustand.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import ReactDom from 'react-dom'; +import create from 'zustand'; + +const useStore = create((set) => ({ + count: 0, + increment: () => set((state) => ({ count: state.count + 1 })), +})); + +const App = () => { + const count = useStore((state) => state.count); + const increment = useStore((state) => state.increment); + + return

{count}

; +}; + +export default function (target: HTMLElement) { + ReactDom.render(, target); +} diff --git a/benchmark/benchmarks/react/counter/index.ts b/benchmark/benchmarks/react/counter/index.ts new file mode 100644 index 00000000..067a85de --- /dev/null +++ b/benchmark/benchmarks/react/counter/index.ts @@ -0,0 +1,96 @@ +import ReactDOM from 'react-dom'; +import Benchmark, { Suite, Options } from 'benchmark'; +import { + cycleLog, + CycleResultInterface, + endBenchmarkLog, + getCycleResult, + startBenchmarkLog, +} from '../../../benchmarkManager'; + +// Files to run the Benchmark on +import agilets from './bench/agilets'; +import hookstate from './bench/hookstate'; +import jotai from './bench/jotai'; +import mobx from './bench/mobx'; +import nanostores from './bench/nanostores'; +import pulsejs from './bench/pulsejs'; +import recoil from './bench/recoil'; +import redux from './bench/redux'; +import reduxToolkit from './bench/redux-toolkit'; +import valtio from './bench/valtio'; +import zustand from './bench/zustand'; + +// @ts-ignore +// Benchmark.js requires an instance of itself globally +window.Benchmark = Benchmark; + +// Create new Benchmark Test Suite +const suite = new Suite('Count'); + +// Retrieve the Element to render the Benchmark Test Suite in +const target = document.getElementById('bench')!; + +// Increment Element +let increment: HTMLHeadingElement; + +// Configures a single Benchmark Test +function configTest(renderElement: (target: HTMLElement) => void): Options { + return { + fn() { + // Execute increment action + increment.click(); + }, + onStart() { + // Render React Component in the target Element + renderElement(target); + + // Retrieve Element to execute the increment action on + increment = target.querySelector('h1')!; + }, + onComplete() { + // Set 'output' in the Benchmark itself to print it later + (this as any).output = parseInt(target.innerText, 10); + + // Unmount React Component + ReactDOM.unmountComponentAtNode(target); + target.innerHTML = ''; + }, + }; +} + +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 Listener + .on('start', function (this: any) { + startBenchmarkLog(this.name); + }) + .on('cycle', (event: any) => { + const cycleResult = getCycleResult(event); + cycleLog(cycleResult, `[Count: ${event.target.output}]`); + results.push(cycleResult); + }) + .on('complete', function (this: any) { + endBenchmarkLog(this.name, results, this.filter('fastest').map('name')); + + // @ts-ignore + // Notify server that the Benchmark Test Suite has ended + window.TEST.ended = true; + }) + + // Run Benchmark Test Suite + .run({ async: true }); diff --git a/benchmark/benchmarks/typescript/defineConfig/bench/referencer.ts b/benchmark/benchmarks/typescript/defineConfig/bench/referencer.ts new file mode 100644 index 00000000..ba6f36b6 --- /dev/null +++ b/benchmark/benchmarks/typescript/defineConfig/bench/referencer.ts @@ -0,0 +1,16 @@ +export function defineConfig( + config: any, + defaults: any, + overwriteUndefinedProperties?: boolean +): void { + if (overwriteUndefinedProperties === undefined) + overwriteUndefinedProperties = true; + + for (const defaultKey in defaults) { + if ( + !Object.prototype.hasOwnProperty.call(config, defaultKey) || + (overwriteUndefinedProperties && config[defaultKey] === undefined) + ) + config[defaultKey] = defaults[defaultKey]; + } +} diff --git a/benchmark/benchmarks/typescript/defineConfig/bench/spreader.ts b/benchmark/benchmarks/typescript/defineConfig/bench/spreader.ts new file mode 100644 index 00000000..7fe3c013 --- /dev/null +++ b/benchmark/benchmarks/typescript/defineConfig/bench/spreader.ts @@ -0,0 +1,17 @@ +export function defineConfig( + config: any, + defaults: any, + overwriteUndefinedProperties?: boolean +): any { + if (overwriteUndefinedProperties === undefined) + overwriteUndefinedProperties = true; + + if (overwriteUndefinedProperties) { + const finalConfig = { ...defaults, ...config }; + for (const key in finalConfig) + if (finalConfig[key] === undefined) finalConfig[key] = defaults[key]; + return finalConfig; + } + + return { ...defaults, ...config }; +} diff --git a/benchmark/benchmarks/typescript/defineConfig/index.ts b/benchmark/benchmarks/typescript/defineConfig/index.ts new file mode 100644 index 00000000..f38b2034 --- /dev/null +++ b/benchmark/benchmarks/typescript/defineConfig/index.ts @@ -0,0 +1,85 @@ +import Benchmark, { Suite } from 'benchmark'; +import { + cycleLog, + CycleResultInterface, + endBenchmarkLog, + getCycleResult, + startBenchmarkLog, +} from '../../../benchmarkManager'; + +// Files to run the Benchmark on +import * as referencer from './bench/referencer'; +import * as spreader from './bench/spreader'; + +interface ConfigInterface { + x1?: boolean; + x2?: string; + x3?: number; + x4?: boolean; + x5?: string; +} + +const defaultConfig: ConfigInterface = { x1: true, x2: undefined }; + +// @ts-ignore +// Benchmark.js requires an instance of itself globally +window.Benchmark = Benchmark; + +// Create new Benchmark Test Suite +const suite = new Suite('define config'); + +const results: CycleResultInterface[] = []; + +// Add Tests to the Benchmark Test Suite +suite + .add('Primitiver', function () { + let config = defaultConfig; + config = { + x1: false, + x2: 'jeff', + x3: 10, + x4: false, + x5: 'hans', + ...config, + }; + }) + .add('Referencer', function () { + const config = defaultConfig; + referencer.defineConfig(config, { + x1: false, + x2: 'jeff', + x3: 10, + x4: false, + x5: 'hans', + }); + }) + .add('Spreader', function () { + let config = defaultConfig; + config = spreader.defineConfig(config, { + x1: false, + x2: 'jeff', + x3: 10, + x4: false, + x5: 'hans', + }); + }) + + // Add Listener + .on('start', function (this: any) { + startBenchmarkLog(this.name); + }) + .on('cycle', (event: any) => { + const cycleResult = getCycleResult(event); + cycleLog(cycleResult); + results.push(cycleResult); + }) + .on('complete', function (this: any) { + endBenchmarkLog(this.name, results, this.filter('fastest').map('name')); + + // @ts-ignore + // Notify server that the Benchmark Test Suite has ended + window.TEST.ended = true; + }) + + // Run Benchmark Test Suite + .run({ async: true }); diff --git a/benchmark/lodash.ts b/benchmark/lodash.ts new file mode 100644 index 00000000..104b3b1f --- /dev/null +++ b/benchmark/lodash.ts @@ -0,0 +1,4 @@ +import _ from 'lodash'; + +// Benchmark.js requires lodash globally +window._ = _; diff --git a/benchmark/package.json b/benchmark/package.json new file mode 100644 index 00000000..d7cf2247 --- /dev/null +++ b/benchmark/package.json @@ -0,0 +1,59 @@ +{ + "name": "benchmark", + "version": "0.1.0", + "private": true, + "author": "BennoDev", + "license": "MIT", + "homepage": "https://agile-ts.org/", + "description": "Benchmark Tests", + "scripts": { + "test": "node -r esbuild-register run.ts", + "test:counter": "yarn test ./benchmarks/react/counter", + "test:1000fields": "yarn test ./benchmarks/react/1000fields", + "test:computed": "yarn test ./benchmarks/react/computed", + "test:defineConfig": "yarn test ./benchmarks/typescript/defineConfig", + "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" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/agile-ts/agile.git" + }, + "dependencies": { + "@agile-ts/core": "file:.yalc/@agile-ts/core", + "@agile-ts/react": "file:.yalc/@agile-ts/react", + "@hookstate/core": "^3.0.8", + "@pulsejs/core": "^4.0.0-beta.3", + "@pulsejs/react": "^4.0.0-beta.3", + "@reduxjs/toolkit": "^1.6.0", + "benchmark": "^2.1.4", + "chalk": "^4.1.1", + "colorette": "^1.2.2", + "dotenv": "^10.0.0", + "esbuild": "^0.12.14", + "esbuild-register": "^2.6.0", + "jotai": "^1.1.2", + "lodash": "^4.17.21", + "mobx": "^6.3.2", + "mobx-react": "^7.2.0", + "nanostores": "^0.3.3", + "playwright": "^1.12.3", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-redux": "^7.2.4", + "recoil": "^0.3.1", + "redux": "^4.1.0", + "typescript": "^4.3.5", + "valtio": "^1.0.6", + "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" + }, + "bugs": { + "url": "https://github.com/agile-ts/agile/issues" + } +} diff --git a/benchmark/public/index.html b/benchmark/public/index.html new file mode 100644 index 00000000..93fc27b9 --- /dev/null +++ b/benchmark/public/index.html @@ -0,0 +1,16 @@ + + + + + Benchmark + + + +
+ + + diff --git a/benchmark/run.ts b/benchmark/run.ts new file mode 100644 index 00000000..2bd03642 --- /dev/null +++ b/benchmark/run.ts @@ -0,0 +1,111 @@ +import dotenv from 'dotenv'; +import esbuild from 'esbuild'; +import playwright from 'playwright'; +import chalk from 'chalk'; + +// Loads environment variables from the '.env' file +dotenv.config(); + +// 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]; +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')); + + // Bundle Benchmark Test Suite + // and launch the server on which the Test Suite is executed + const server = await esbuild.serve( + { + servedir: 'public', + port: 3003, + host: '127.0.0.1', // localhost + }, + { + 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: true, // 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 + } + ); + const serverUrl = `http://${server.host}:${server.port}`; + + console.log( + `${chalk.blue('[i]')} ${chalk.gray( + `Server is running at port: ${chalk.blueBright.bold(server.port)}` + )}` + ); + + // Launch Chrome as browser to run the Benchmark Test Suite in + const browser = await playwright.chromium.launch(); + const context = await browser.newContext(); + const page = await context.newPage(); + + // Option to open and test the Benchmark Test Suite in the browser manually + if (process.env.MANUAL_BENCHMARK === 'true') { + console.log( + `${chalk.blue('[i]')} ${chalk.gray( + `Benchmark is running at ${chalk.blueBright.bold(serverUrl)}` + )}` + ); + + await server.wait; + } + + console.log('\n'); + + // Setup 'pageerror' listener to throw occurring errors in the local console + // https://playwright.dev/docs/api/class-page/#page-event-page-error + page.on('pageerror', (error) => { + throw error; + }); + + // Setup 'console' listener to transfer the browser logs into the local console + // https://playwright.dev/docs/api/class-page/#page-event-console + page.on('console', (...message) => { + const stringMessages = message.map((m) => m.text()); + const colorMessage = stringMessages[0]; + stringMessages.shift(); // Remove 'colorMessage' (first argument) from 'stringMessages' array + + // Parse color message to work in chalck + // https://stackoverflow.com/questions/56526522/gulp-chalk-pass-string-template-through-method + const parsedColorMessage = [colorMessage]; + // @ts-ignore + parsedColorMessage.raw = [colorMessage]; + + console.log(chalk(parsedColorMessage), ...stringMessages); + }); + + // Open the url the server is running on + await page.goto(serverUrl); + + // Wait for tests to be executed (indicator is when 'window.TESTS.ended' is set to true) + // https://playwright.dev/docs/api/class-frame#frame-wait-for-function + await page.waitForFunction( + // @ts-ignore + () => window.TEST?.ended, + undefined, + { + timeout: 0, + polling: 100, + } + ); + + // Close browser and stop server + await browser.close(); + server.stop(); +}; + +// Execute the Benchmark +startBenchmark(); diff --git a/benchmark/tsconfig.json b/benchmark/tsconfig.json new file mode 100644 index 00000000..b36827f2 --- /dev/null +++ b/benchmark/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react" + } +} diff --git a/benchmark/yarn.lock b/benchmark/yarn.lock new file mode 100644 index 00000000..16d56ebc --- /dev/null +++ b/benchmark/yarn.lock @@ -0,0 +1,624 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@agile-ts/core@file:.yalc/@agile-ts/core": + version "0.1.0" + dependencies: + "@agile-ts/utils" "^0.0.5" + +"@agile-ts/react@file:.yalc/@agile-ts/react": + version "0.1.0" + +"@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== + +"@babel/runtime@^7.12.1", "@babel/runtime@^7.9.2": + version "7.14.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.6.tgz#535203bc0892efc7dec60bdc27b2ecf6e409062d" + integrity sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg== + dependencies: + regenerator-runtime "^0.13.4" + +"@hookstate/core@^3.0.8": + version "3.0.8" + resolved "https://registry.yarnpkg.com/@hookstate/core/-/core-3.0.8.tgz#d6838153d6d43c2f35cfca475c31248192564e62" + integrity sha512-blQagGIVIbNoUiNCRrvaXqFmUe7WGMY35ok/LENfl2pcIsLBjkreYIZiaSFi83tkycwq7ZOmcQz/R1nvLKhH8w== + +"@pulsejs/core@^4.0.0-beta.3": + version "4.0.0-beta.3" + resolved "https://registry.yarnpkg.com/@pulsejs/core/-/core-4.0.0-beta.3.tgz#a19c983093dbc516ee3dcda4dee92917c77fd4cc" + integrity sha512-nYSckFFTPt8/8wZpEaFqKsSc1xd+eT7t3TSB3acy6yEQjbkQ1T2CL5tyNGL4tzT3Bher6LHkipDy0tMi5PsPUg== + +"@pulsejs/react@^4.0.0-beta.3": + version "4.0.0-beta.3" + resolved "https://registry.yarnpkg.com/@pulsejs/react/-/react-4.0.0-beta.3.tgz#d25bf1e969b8749d0f098f6e2ac066364f8c1fe1" + integrity sha512-QuO/UoBTL+5ObCTje3nvrMqrifPujWtbHpVMJMnWnZGrL88hiSivaWGvqzaU0BsIE3R6WQHZTJNDvBR8sXFIEQ== + +"@reduxjs/toolkit@^1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.6.0.tgz#0a17c6941c57341f8b31e982352b495ab69d5add" + integrity sha512-eGL50G+Vj5AG5uD0lineb6rRtbs96M8+hxbcwkHpZ8LQcmt0Bm33WyBSnj5AweLkjQ7ZP+KFRDHiLMznljRQ3A== + dependencies: + immer "^9.0.1" + redux "^4.1.0" + redux-thunk "^2.3.0" + reselect "^4.0.0" + +"@types/benchmark@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@types/benchmark/-/benchmark-2.1.0.tgz#157e2ef22311d3140fb33e82a938a1beb26e78e0" + integrity sha512-wxT2/LZn4z0NvSfZirxmBx686CU7EXp299KHkIk79acXpQtgeYHrslFzDacPGXifC0Pe3CEaLup07bgY1PnuQw== + +"@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/node@*", "@types/node@^16.0.0": + version "16.0.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.0.0.tgz#067a6c49dc7a5c2412a505628e26902ae967bf6f" + integrity sha512-TmCW5HoZ2o2/z2EYi109jLqIaPIi9y/lc2LmDCWzuCi35bcaQ+OtUh6nwBiFK7SOu25FAU5+YKdqFZUwtqGSdg== + +"@types/prop-types@*": + version "15.7.3" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" + integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== + +"@types/react-dom@^17.0.8": + version "17.0.8" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.8.tgz#3180de6d79bf53762001ad854e3ce49f36dd71fc" + integrity sha512-0ohAiJAx1DAUEcY9UopnfwCE9sSMDGnY/oXjWMax6g3RpzmTt2GMyMVAXcbn0mo8XAff0SbQJl2/SBU+hjSZ1A== + dependencies: + "@types/react" "*" + +"@types/react-redux@^7.1.16": + version "7.1.16" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.16.tgz#0fbd04c2500c12105494c83d4a3e45c084e3cb21" + integrity sha512-f/FKzIrZwZk7YEO9E1yoxIuDNRiDducxkFlkw/GNMGEnK9n4K8wJzlJBghpSuOVDgEUHoDkDF7Gi9lHNQR4siw== + dependencies: + "@types/hoist-non-react-statics" "^3.3.0" + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + redux "^4.0.0" + +"@types/react@*", "@types/react@^17.0.13": + version "17.0.13" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.13.tgz#6b7c9a8f2868586ad87d941c02337c6888fb874f" + integrity sha512-D/G3PiuqTfE3IMNjLn/DCp6umjVCSvtZTPdtAFy5+Ved6CsdRvivfKeCzw79W4AatShtU4nGqgvOv5Gro534vQ== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + +"@types/scheduler@*": + version "0.16.1" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275" + integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA== + +"@types/yauzl@^2.9.1": + version "2.9.2" + resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.9.2.tgz#c48e5d56aff1444409e39fa164b0b4d4552a7b7a" + integrity sha512-8uALY5LTvSuHgloDVUvWP3pIauILm+8/0pDMokuDYIoNsOkSwd5AiHBTSEJjKTDcZr5z8UpgOWZkxBF4iJftoA== + dependencies: + "@types/node" "*" + +agent-base@6: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + +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== + dependencies: + color-convert "^2.0.1" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +benchmark@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/benchmark/-/benchmark-2.1.4.tgz#09f3de31c916425d498cc2ee565a0ebf3c2a5629" + integrity sha1-CfPeMckWQl1JjMLuVloOvzwqVik= + dependencies: + lodash "^4.17.4" + platform "^1.3.3" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +buffer-crc32@~0.2.3: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= + +chalk@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad" + integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +colorette@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" + integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== + +commander@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" + integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +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== + +debug@4, debug@^4.1.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" + integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== + dependencies: + ms "2.1.2" + +dotenv@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81" + integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== + +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" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +esbuild-register@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/esbuild-register/-/esbuild-register-2.6.0.tgz#9f19a54c82be751dd87673d6a66d7b9e1cdd8498" + integrity sha512-2u4AtnCXP5nivtIxZryiZOUcEQkOzFS7UhAqibUEmaTAThJ48gDLYTBF/Fsz+5r0hbV1jrFE6PQvPDUrKZNt/Q== + dependencies: + esbuild "^0.12.8" + jsonc-parser "^3.0.0" + +esbuild@^0.12.14, esbuild@^0.12.8: + version "0.12.14" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.12.14.tgz#43157dbd0b36d939247d4eb4909a4886ac40f82e" + integrity sha512-z8p+6FGiplR7a3pPonXREbm+8IeXjBGvDpVidZmGB/AJMsJSfGCU+n7KOMCazA9AwvagadRWBhiKorC0w9WJvw== + +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" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + +extract-zip@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" + integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== + dependencies: + debug "^4.1.1" + get-stream "^5.1.0" + yauzl "^2.10.0" + optionalDependencies: + "@types/yauzl" "^2.9.1" + +fd-slicer@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" + integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4= + dependencies: + pend "~1.2.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +get-stream@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" + integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== + dependencies: + pump "^3.0.0" + +glob@^7.1.3: + version "7.1.7" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" + integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +graceful-fs@^4.2.4: + version "4.2.6" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" + integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== + +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= + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +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" + +https-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" + integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== + dependencies: + agent-base "6" + debug "4" + +immer@^9.0.1: + version "9.0.3" + resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.3.tgz#146e2ba8b84d4b1b15378143c2345559915097f4" + integrity sha512-mONgeNSMuyjIe0lkQPa9YhdmTv8P19IeHV0biYhcXhbd5dhdB9HSK93zBpyKjp6wersSUgT5QyU0skmejUVP2A== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +jotai@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/jotai/-/jotai-1.1.2.tgz#3f211e0c03c74e95ea6fd7a69c1d2b65731009bf" + integrity sha512-dni4wtgYGG+s9YbOJN7lcfrrhxiD6bH1SN00Pnl0F2htgOXmjxqkGlFzw02OK0Rw35wGNzBfDTJVtbGD9wHOhg== + +jpeg-js@^0.4.2: + version "0.4.3" + resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.3.tgz#6158e09f1983ad773813704be80680550eff977b" + integrity sha512-ru1HWKek8octvUHFHvE5ZzQ1yAsJmIvRdGWvSoKV52XKyuyYA437QWDttXT8eZXDSbuMpHlLzPDZUPd6idIz+Q== + +"js-tokens@^3.0.0 || ^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +jsonc-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.0.0.tgz#abdd785701c7e7eaca8a9ec8cf070ca51a745a22" + integrity sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA== + +lodash@^4.17.21, lodash@^4.17.4: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +loose-envify@^1.1.0, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +mime@^2.4.6: + version "2.5.2" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe" + integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg== + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +mobx-react-lite@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-3.2.0.tgz#331d7365a6b053378dfe9c087315b4e41c5df69f" + integrity sha512-q5+UHIqYCOpBoFm/PElDuOhbcatvTllgRp3M1s+Hp5j0Z6XNgDbgqxawJ0ZAUEyKM8X1zs70PCuhAIzX1f4Q/g== + +mobx-react@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/mobx-react/-/mobx-react-7.2.0.tgz#241e925e963bb83a31d269f65f9f379e37ecbaeb" + integrity sha512-KHUjZ3HBmZlNnPd1M82jcdVsQRDlfym38zJhZEs33VxyVQTvL77hODCArq6+C1P1k/6erEeo2R7rpE7ZeOL7dg== + dependencies: + mobx-react-lite "^3.2.0" + +mobx@^6.3.2: + version "6.3.2" + resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.3.2.tgz#125590961f702a572c139ab69392bea416d2e51b" + integrity sha512-xGPM9dIE1qkK9Nrhevp0gzpsmELKU4MFUJRORW/jqxVFIHHWIoQrjDjL8vkwoJYY3C2CeVJqgvl38hgKTalTWg== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +nanostores@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/nanostores/-/nanostores-0.3.3.tgz#3f6a858d3e2700c70c7942301380550f66a8e9ee" + integrity sha512-+MemxV/HzzTPJQCvzEmwfIFMAIcDEiod37A5F1ERyKQqtm6hbEfuNokfmJfecYM2gjunlPdPHA5lQ9cwHHSYNg== + +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= + +platform@^1.3.3: + version "1.3.6" + resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.6.tgz#48b4ce983164b209c2d45a107adb31f473a6e7a7" + integrity sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg== + +playwright@^1.12.3: + version "1.12.3" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.12.3.tgz#113afa2cba10fb56e9a5b307377343e32a155a99" + integrity sha512-eyhHvZV7dMAUltqjQsgJ9CjZM8dznzN1+rcfCI6W6lfQ7IlPvTFGLuKOCcI4ETbjfbxqaS5FKIkb1WDDzq2Nww== + dependencies: + commander "^6.1.0" + debug "^4.1.1" + extract-zip "^2.0.1" + https-proxy-agent "^5.0.0" + jpeg-js "^0.4.2" + mime "^2.4.6" + pngjs "^5.0.0" + progress "^2.0.3" + proper-lockfile "^4.1.1" + proxy-from-env "^1.1.0" + rimraf "^3.0.2" + stack-utils "^2.0.3" + ws "^7.4.6" + yazl "^2.5.1" + +pngjs@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb" + integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw== + +progress@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + +prop-types@^15.7.2: + version "15.7.2" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" + integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.8.1" + +proper-lockfile@^4.1.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/proper-lockfile/-/proper-lockfile-4.1.2.tgz#c8b9de2af6b2f1601067f98e01ac66baa223141f" + integrity sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA== + dependencies: + graceful-fs "^4.2.4" + retry "^0.12.0" + signal-exit "^3.0.2" + +proxy-compare@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/proxy-compare/-/proxy-compare-2.0.0.tgz#36f41114a25fcf359037308d12529183a9dc182c" + integrity sha512-xhJF1+vPCnu93QYva3Weii5ho1AeX5dsR/P5O7pzy9QLxeOgMSQNC8zDo0bGg9vtn61Pu5Qn+5w/Y8OSU5k+8g== + +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +react-dom@^17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" + integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + scheduler "^0.20.2" + +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== + +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@^17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" + integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + +recoil@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/recoil/-/recoil-0.3.1.tgz#40ef544160d19d76e25de8929d7e512eace13b90" + integrity sha512-KNA3DRqgxX4rRC8E7fc6uIw7BACmMPuraIYy+ejhE8tsw7w32CetMm8w7AMZa34wzanKKkev3vl3H7Z4s0QSiA== + dependencies: + hamt_plus "1.0.2" + +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.0" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.0.tgz#eb049679f2f523c379f1aff345c8612f294c88d4" + integrity sha512-uI2dQN43zqLWCt6B/BMGRMY6db7TTY4qeHHfGeKb3EOhmOKjU3KdWvNLJyqaHRksv/ErdNH7cFZWg9jXtewy4g== + dependencies: + "@babel/runtime" "^7.9.2" + +regenerator-runtime@^0.13.4: + version "0.13.7" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" + integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== + +reselect@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7" + integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA== + +retry@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" + integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +scheduler@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" + integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + +signal-exit@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" + integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== + +stack-utils@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.3.tgz#cd5f030126ff116b78ccb3c027fe302713b61277" + integrity sha512-gL//fkxfWUsIlFL2Tl42Cl6+HFALEaB1FU76I/Fy+oZjRreP7OPMXFlGbxM7NQsI0ZpUfw76sHnv0WNYuTb7Iw== + dependencies: + escape-string-regexp "^2.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" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +typescript@^4.3.5: + version "4.3.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4" + integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA== + +valtio@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/valtio/-/valtio-1.0.6.tgz#b316deea5537d254a141e2e5af1692d9eae2f60f" + integrity sha512-ylCis9IkcE7b92XjMb3ebdJgLvJEFJ2NjfuD01QNr98pVOhRa5WsW4LSykFgbO4W7ftrZtO8jN4svZL0XlD77w== + dependencies: + proxy-compare "2.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +ws@^7.4.6: + version "7.5.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.1.tgz#44fc000d87edb1d9c53e51fbc69a0ac1f6871d66" + integrity sha512-2c6faOUH/nhoQN6abwMloF7Iyl0ZS2E9HGtsiLrWn0zOOMWlhtDmdf/uihDt6jnuCxgtwGBNy6Onsoy2s2O2Ow== + +yauzl@^2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" + integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= + dependencies: + buffer-crc32 "~0.2.3" + fd-slicer "~1.1.0" + +yazl@^2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/yazl/-/yazl-2.5.1.tgz#a3d65d3dd659a5b0937850e8609f22fffa2b5c35" + integrity sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw== + dependencies: + buffer-crc32 "~0.2.3" + +zustand@^3.5.5: + version "3.5.5" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-3.5.5.tgz#628458ad70621ddc2a17dbee49be963e5c0dccb5" + integrity sha512-iTiJoxzYFtiD7DhscgwK2P4Kft1JcZEI2U7mG8IxiOFM4KpBAiJZfFop3r/3wbCuyltXI6ph1Fx90e4j/S43XA== diff --git a/examples/react/develop/functional-component-ts/package.json b/examples/react/develop/functional-component-ts/package.json index 08b5324c..4972a343 100644 --- a/examples/react/develop/functional-component-ts/package.json +++ b/examples/react/develop/functional-component-ts/package.json @@ -5,10 +5,11 @@ "dependencies": { "@agile-ts/api": "file:.yalc/@agile-ts/api", "@agile-ts/core": "file:.yalc/@agile-ts/core", - "@agile-ts/multieditor": "file:.yalc/@agile-ts/multieditor", - "@agile-ts/react": "file:.yalc/@agile-ts/react", "@agile-ts/event": "file:.yalc/@agile-ts/event", + "@agile-ts/logger": "file:.yalc/@agile-ts/logger", + "@agile-ts/multieditor": "file:.yalc/@agile-ts/multieditor", "@agile-ts/proxytree": "file:.yalc/@agile-ts/proxytree", + "@agile-ts/react": "file:.yalc/@agile-ts/react", "react": "^16.13.1", "react-dom": "^16.13.1", "react-router-dom": "^5.2.0", @@ -30,7 +31,8 @@ "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject", - "install:agile": "yalc add @agile-ts/core @agile-ts/react @agile-ts/api @agile-ts/multieditor @agile-ts/event @agile-ts/proxytree & yarn install" + "install:dev:agile": "yalc add @agile-ts/core @agile-ts/react @agile-ts/api @agile-ts/multieditor @agile-ts/event @agile-ts/proxytree @agile-ts/logger & yarn install", + "install:prod:agile": "yarn add @agile-ts/core @agile-ts/react @agile-ts/api @agile-ts/multieditor @agile-ts/event @agile-ts/proxytree @agile-ts/logger & yarn install" }, "eslintConfig": { "extends": "react-app" 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 cf448f46..c98da0af 100644 --- a/examples/react/develop/functional-component-ts/src/core/index.ts +++ b/examples/react/develop/functional-component-ts/src/core/index.ts @@ -1,10 +1,11 @@ -import { Agile, clone, Item, Logger } from '@agile-ts/core'; +import { Agile, clone, Item } from '@agile-ts/core'; import Event from '@agile-ts/event'; +import { assignSharedAgileLoggerConfig, Logger } from '@agile-ts/logger'; export const myStorage: any = {}; +assignSharedAgileLoggerConfig({ level: Logger.level.DEBUG }); export const App = new Agile({ - logConfig: { level: Logger.level.DEBUG }, localStorage: true, }); diff --git a/examples/react/develop/functional-component-ts/yarn.lock b/examples/react/develop/functional-component-ts/yarn.lock index 21b151e8..b44f5283 100644 --- a/examples/react/develop/functional-component-ts/yarn.lock +++ b/examples/react/develop/functional-component-ts/yarn.lock @@ -3,46 +3,36 @@ "@agile-ts/api@file:.yalc/@agile-ts/api": - version "0.0.18" + version "0.0.19" dependencies: - "@agile-ts/utils" "^0.0.4" + "@agile-ts/utils" "^0.0.5" "@agile-ts/core@file:.yalc/@agile-ts/core": - version "0.0.17" + version "0.1.0" dependencies: - "@agile-ts/logger" "^0.0.4" - "@agile-ts/utils" "^0.0.4" + "@agile-ts/utils" "^0.0.5" "@agile-ts/event@file:.yalc/@agile-ts/event": - version "0.0.7" + version "0.0.8" -"@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== +"@agile-ts/logger@file:.yalc/@agile-ts/logger": + version "0.0.5" dependencies: - "@agile-ts/utils" "^0.0.4" + "@agile-ts/utils" "^0.0.5" "@agile-ts/multieditor@file:.yalc/@agile-ts/multieditor": - version "0.0.17" - -"@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== + version "0.0.18" "@agile-ts/proxytree@file:.yalc/@agile-ts/proxytree": - version "0.0.3" + version "0.0.4" "@agile-ts/react@file:.yalc/@agile-ts/react": - version "0.0.18" - dependencies: - "@agile-ts/proxytree" "^0.0.3" + version "0.1.0" -"@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.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== "@babel/code-frame@7.8.3": version "7.8.3" diff --git a/examples/react/release/boxes/package.json b/examples/react/release/boxes/package.json index 44579fee..8fdd52f6 100644 --- a/examples/react/release/boxes/package.json +++ b/examples/react/release/boxes/package.json @@ -3,10 +3,8 @@ "version": "0.1.0", "private": true, "dependencies": { - "@agile-ts/api": "file:.yalc/@agile-ts/api", "@agile-ts/core": "file:.yalc/@agile-ts/core", - "@agile-ts/event": "file:.yalc/@agile-ts/event", - "@agile-ts/multieditor": "file:.yalc/@agile-ts/multieditor", + "@agile-ts/logger": "file:.yalc/@agile-ts/logger", "@agile-ts/proxytree": "file:.yalc/@agile-ts/proxytree", "@agile-ts/react": "file:.yalc/@agile-ts/react", "@chakra-ui/react": "^1.6.3", @@ -35,7 +33,8 @@ "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject", - "install:agile": "yalc add @agile-ts/core @agile-ts/react @agile-ts/api @agile-ts/multieditor @agile-ts/event @agile-ts/proxytree & yarn install" + "install:dev:agile": "yalc add @agile-ts/core @agile-ts/react @agile-ts/proxytree @agile-ts/logger & yarn install", + "install:prod:agile": "yarn add @agile-ts/core @agile-ts/react @agile-ts/proxytree @agile-ts/logger & yarn install" }, "eslintConfig": { "extends": [ diff --git a/examples/react/release/boxes/src/api.ts b/examples/react/release/boxes/src/api.ts index 17d0a2a6..8e01fedc 100644 --- a/examples/react/release/boxes/src/api.ts +++ b/examples/react/release/boxes/src/api.ts @@ -1,26 +1,26 @@ -import queryString, {ParsedUrlQueryInput} from 'querystring' +import queryString, { ParsedUrlQueryInput } from 'querystring'; type RequestOptions = { - queryParams?: ParsedUrlQueryInput - method?: 'GET' | 'POST' - body?: object | string -} + queryParams?: ParsedUrlQueryInput; + method?: 'GET' | 'POST'; + body?: object | string; +}; export const apiUrl = (lambda: string, queryParams?: ParsedUrlQueryInput) => { - let url = `https://f10adraov8.execute-api.us-east-1.amazonaws.com/dev/${lambda}` - if (queryParams) url += '?' + queryString.stringify(queryParams) + let url = `https://f10adraov8.execute-api.us-east-1.amazonaws.com/dev/${lambda}`; + if (queryParams) url += '?' + queryString.stringify(queryParams); - return url -} + return url; +}; export const callApi = (lambda: string, options?: RequestOptions) => { - const {queryParams, body, method} = options || {} - const url = apiUrl(lambda, queryParams) + const { queryParams, body, method } = options || {}; + const url = apiUrl(lambda, queryParams); - let bodyString = body - if (typeof bodyString === 'object') { - bodyString = JSON.stringify(body) - } + let bodyString = body; + if (typeof bodyString === 'object') { + bodyString = JSON.stringify(body); + } - return fetch(url, {body: bodyString, method}).then((res) => res.json()) -} + return fetch(url, { body: bodyString, method }).then((res) => res.json()); +}; diff --git a/examples/react/release/boxes/src/components/EditProperties/index.tsx b/examples/react/release/boxes/src/components/EditProperties/index.tsx index 62c3fd51..3729f759 100644 --- a/examples/react/release/boxes/src/components/EditProperties/index.tsx +++ b/examples/react/release/boxes/src/components/EditProperties/index.tsx @@ -11,16 +11,17 @@ import _ from 'lodash'; import { ImageInfo, ImageInfoFallback } from './components/ImageInfo'; import { useProxy, useSelector } from '@agile-ts/react'; import core from '../../core'; +import { ElementImageInterface } from '../../core/entities/ui/ui.interfaces'; export const EditProperties = () => { const selectedElementId = useSelector( core.ui.SELECTED_ELEMENT, (value) => value?.id - ); + ) as number | string; const selectedElementImage = useSelector( core.ui.SELECTED_ELEMENT, (value) => value?.image - ); + ) as ElementImageInterface; // TODO useProxy doesn't work here as expected because the selected Elements // doesn't exist on the creation of the Subscription Container @@ -135,7 +136,8 @@ const Property = ({ id: number | string; }) => { const ELEMENT = core.ui.ELEMENTS.getItem(id); - const element = useProxy(ELEMENT, { componentId: 'Property' }); + const element = useProxy(ELEMENT, { componentId: 'Property', deps: [id] }); + return ( = ( setIsLoading(true); core.ui.getImageDimensions(element.image.src).then((response) => { setIsLoading(false); - ELEMENT?.patch({ - style: { ...{ size: response }, ...ELEMENT?.value.style }, - }); + if (ELEMENT != null) { + ELEMENT.nextStateValue.style.size = response; + ELEMENT?.ingest(); + } }); } else { setIsLoading(false); diff --git a/examples/react/release/boxes/src/components/Rectangle/index.tsx b/examples/react/release/boxes/src/components/Rectangle/index.tsx index c4a34aee..5dee9586 100644 --- a/examples/react/release/boxes/src/components/Rectangle/index.tsx +++ b/examples/react/release/boxes/src/components/Rectangle/index.tsx @@ -3,7 +3,7 @@ import { Drag } from '../actionComponents/Drag'; import { Resize } from '../actionComponents/Resize'; import { RectangleContainer } from './components/RectangleContainer'; import { RectangleInner } from './components/RectangleInner'; -import { useAgile, useProxy } from '@agile-ts/react'; +import { useAgile } from '@agile-ts/react'; import core from '../../core'; import { SELECTED_ELEMENT } from '../../core/entities/ui/ui.controller'; import { ElementStyleInterface } from '../../core/entities/ui/ui.interfaces'; @@ -24,7 +24,7 @@ export const Rectangle: React.FC = (props) => { { componentId: 'Rectangle', } - ); + ) as string | number; if (element == null) return null; diff --git a/examples/react/release/boxes/src/components/actionComponents/Resize/components/Handle.tsx b/examples/react/release/boxes/src/components/actionComponents/Resize/components/Handle.tsx index dbe0a795..3b86fe96 100644 --- a/examples/react/release/boxes/src/components/actionComponents/Resize/components/Handle.tsx +++ b/examples/react/release/boxes/src/components/actionComponents/Resize/components/Handle.tsx @@ -10,13 +10,14 @@ type Position = { bottom?: number | string; }; -interface HandlePropsInterface { +export interface HandlePropsInterface { placement: ResizeHandle; visible: boolean; + innerRef?: any; } export const Handle: React.FC = (props) => { - const { placement, visible } = props; + const { placement, visible, innerRef } = props; const size = 10; const position: Position = {}; @@ -34,6 +35,7 @@ export const Handle: React.FC = (props) => { return ( = (props) => { lockAspectRatio, } = props; + // https://github.com/react-grid-layout/react-resizable#custom-react-component + const WrappedHandle = React.forwardRef((props: HandlePropsInterface, ref) => ( + + )); + return ( = (props) => { }} resizeHandles={handlePlacements} handle={(placement) => ( -
- -
+ )} lockAspectRatio={lockAspectRatio}>
{children}
diff --git a/examples/react/release/boxes/src/core/app.ts b/examples/react/release/boxes/src/core/app.ts index 8cf510e6..89d7df2e 100644 --- a/examples/react/release/boxes/src/core/app.ts +++ b/examples/react/release/boxes/src/core/app.ts @@ -1,6 +1,6 @@ -import { Agile, Logger } from '@agile-ts/core'; -import reactIntegration from '@agile-ts/react'; +import { Agile, assignSharedAgileInstance } from '@agile-ts/core'; +import { assignSharedAgileLoggerConfig, Logger } from '@agile-ts/logger'; -export const App = new Agile({ - logConfig: { level: Logger.level.WARN }, -}).integrate(reactIntegration); +export const App = new Agile(); +assignSharedAgileInstance(App); +assignSharedAgileLoggerConfig({ level: Logger.level.WARN }); diff --git a/examples/react/release/boxes/yarn.lock b/examples/react/release/boxes/yarn.lock index bd82c231..7fff41aa 100644 --- a/examples/react/release/boxes/yarn.lock +++ b/examples/react/release/boxes/yarn.lock @@ -2,42 +2,26 @@ # yarn lockfile v1 -"@agile-ts/api@file:.yalc/@agile-ts/api": - version "0.0.18" - dependencies: - "@agile-ts/utils" "^0.0.4" - "@agile-ts/core@file:.yalc/@agile-ts/core": - version "0.0.17" + version "0.1.0" dependencies: - "@agile-ts/logger" "^0.0.4" - "@agile-ts/utils" "^0.0.4" + "@agile-ts/utils" "^0.0.5" -"@agile-ts/event@file:.yalc/@agile-ts/event": - version "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== +"@agile-ts/logger@file:.yalc/@agile-ts/logger": + version "0.0.5" dependencies: - "@agile-ts/utils" "^0.0.4" + "@agile-ts/utils" "^0.0.5" -"@agile-ts/multieditor@file:.yalc/@agile-ts/multieditor": - version "0.0.17" - -"@agile-ts/proxytree@^0.0.3", "@agile-ts/proxytree@file:.yalc/@agile-ts/proxytree": - version "0.0.3" +"@agile-ts/proxytree@file:.yalc/@agile-ts/proxytree": + version "0.0.4" "@agile-ts/react@file:.yalc/@agile-ts/react": - version "0.0.18" - dependencies: - "@agile-ts/proxytree" "^0.0.3" + version "0.1.0" -"@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.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== "@babel/code-frame@7.10.4": version "7.10.4" diff --git a/examples/vue/develop/my-project/babel.config.js b/examples/vue/develop/my-project/babel.config.js index e9558405..078c0056 100644 --- a/examples/vue/develop/my-project/babel.config.js +++ b/examples/vue/develop/my-project/babel.config.js @@ -1,5 +1,3 @@ module.exports = { - presets: [ - '@vue/cli-plugin-babel/preset' - ] -} + presets: ['@vue/cli-plugin-babel/preset'], +}; diff --git a/examples/vue/develop/my-project/package.json b/examples/vue/develop/my-project/package.json index 47b1f72d..ce01a6b2 100644 --- a/examples/vue/develop/my-project/package.json +++ b/examples/vue/develop/my-project/package.json @@ -6,10 +6,12 @@ "serve": "vue-cli-service serve", "build": "vue-cli-service build", "lint": "vue-cli-service lint", - "install:agile": "yalc add @agile-ts/core @agile-ts/vue & yarn install" + "install:dev:agile": "yalc add @agile-ts/core @agile-ts/vue @agile-ts/logger & yarn install", + "install:prod:agile": "yarn add @agile-ts/core @agile-ts/vue @agile-ts/logger & yarn install" }, "dependencies": { "@agile-ts/core": "file:.yalc/@agile-ts/core", + "@agile-ts/logger": "file:.yalc/@agile-ts/logger", "@agile-ts/vue": "file:.yalc/@agile-ts/vue", "core-js": "^3.6.5", "vue": "^2.6.11" diff --git a/examples/vue/develop/my-project/src/core.js b/examples/vue/develop/my-project/src/core.js index a8999d9f..0512ec9b 100644 --- a/examples/vue/develop/my-project/src/core.js +++ b/examples/vue/develop/my-project/src/core.js @@ -1,19 +1,22 @@ -import { Agile, globalBind, Logger } from '@agile-ts/core'; -import vueIntegration from '@agile-ts/vue'; +import { Agile, assignSharedAgileInstance, globalBind } from '@agile-ts/core'; +import { Logger, assignSharedAgileLoggerConfig } from '@agile-ts/logger'; +import '@agile-ts/vue'; + +assignSharedAgileLoggerConfig({ level: Logger.level.DEBUG }); // Create Agile Instance -export const App = new Agile({ - logConfig: { level: Logger.level.DEBUG }, -}).integrate(vueIntegration); +export const App = new Agile({ localStorage: true }); +assignSharedAgileInstance(App); // console.debug('hi'); // Doesn't work here idk why // Create State export const MY_STATE = App.createState('World', { key: 'my-state', -}).computeValue((v) => { - return `Hello ${v}`; -}); +}) + .computeValue((v) => { + return `Hello ${v}`; + }); export const MY_COMPUTED = App.createComputed( async () => { diff --git a/examples/vue/develop/my-project/yarn.lock b/examples/vue/develop/my-project/yarn.lock index 04c336c0..5997fe57 100644 --- a/examples/vue/develop/my-project/yarn.lock +++ b/examples/vue/develop/my-project/yarn.lock @@ -3,25 +3,22 @@ "@agile-ts/core@file:.yalc/@agile-ts/core": - version "0.0.17" + version "0.1.0" dependencies: - "@agile-ts/logger" "^0.0.4" - "@agile-ts/utils" "^0.0.4" + "@agile-ts/utils" "^0.0.5" -"@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== +"@agile-ts/logger@file:.yalc/@agile-ts/logger": + version "0.0.5" dependencies: - "@agile-ts/utils" "^0.0.4" + "@agile-ts/utils" "^0.0.5" -"@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.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/vue@file:.yalc/@agile-ts/vue": - version "0.0.5" + version "0.1.0" "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13": version "7.12.13" diff --git a/jest.base.config.js b/jest.base.config.js index 33c9a7b1..9c42b9e0 100644 --- a/jest.base.config.js +++ b/jest.base.config.js @@ -15,5 +15,6 @@ module.exports = { 'ts-jest': { tsconfig: '/packages/tsconfig.default.json', }, + __DEV__: true, }, }; diff --git a/package.json b/package.json index b4ed3355..2e7e2bce 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,5 @@ { + "name": "agile", "private": true, "author": "BennoDev", "license": "MIT", @@ -73,6 +74,5 @@ }, "workspaces": [ "packages/*" - ], - "name": "agile" + ] } diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 9c7880ec..e8de6610 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -1,4 +1,4 @@ -import { clone, copy, defineConfig, isValidObject } from '@agile-ts/utils'; +import { clone, copy, isValidObject } from '@agile-ts/utils'; export default class API { public config: ApiConfig; @@ -113,7 +113,7 @@ export default class API { // Configure request Options const config = copy(this.config); - config.options = defineConfig(options, config.options || {}); + config.options = { ...config.options, ...options }; config.options.method = method; if (!config.options.headers) config.options.headers = {}; diff --git a/packages/core/.size-limit.js b/packages/core/.size-limit.js index 1b8247f6..b3971b86 100644 --- a/packages/core/.size-limit.js +++ b/packages/core/.size-limit.js @@ -1,6 +1,6 @@ module.exports = [ { path: 'dist/*', - limit: '35 kB', + limit: '20 kB', }, ]; diff --git a/packages/core/package.json b/packages/core/package.json index 1463bafd..4d10af6e 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -44,9 +44,16 @@ "@agile-ts/utils": "file:../utils" }, "dependencies": { - "@agile-ts/utils": "^0.0.5", + "@agile-ts/utils": "^0.0.5" + }, + "peerDependencies": { "@agile-ts/logger": "^0.0.5" }, + "peerDependenciesMeta": { + "@agile-ts/logger": { + "optional": true + } + }, "publishConfig": { "access": "public" }, diff --git a/packages/core/src/agile.ts b/packages/core/src/agile.ts index adf0419c..85c03544 100644 --- a/packages/core/src/agile.ts +++ b/packages/core/src/agile.ts @@ -13,20 +13,25 @@ import { Storages, CreateStorageConfigInterface, RegisterConfigInterface, - defineConfig, - Logger, - CreateLoggerConfigInterface, StateConfigInterface, flatMerge, LogCodeManager, DependableAgileInstancesType, CreateComputedConfigInterface, ComputeFunctionType, + createStorage, + createState, + createCollection, + createComputed, + IntegrationsConfigInterface, } from './internal'; export class Agile { public config: AgileConfigInterface; + // Key/Name identifier of Agile Instance + public key?: AgileKey; + // Queues and executes incoming Observer-based Jobs public runtime: Runtime; // Manages and simplifies the subscription to UI-Components @@ -34,18 +39,8 @@ export class Agile { // Handles the permanent persistence of Agile Classes public storages: Storages; - // Integrations (UI-Frameworks) that are integrated into AgileTs + // Integrations (UI-Frameworks) that are integrated into the Agile Instance public integrations: Integrations; - // External added Integrations that are to integrate into AgileTs when it is instantiated - static initialIntegrations: Integration[] = []; - - // Static AgileTs Logger with the default config - // (-> is overwritten by the last created Agile Instance) - static logger = new Logger({ - prefix: 'Agile', - active: true, - level: Logger.level.WARN, - }); // Identifier used to bind an Agile Instance globally static globalKey = '__agile__'; @@ -77,39 +72,37 @@ export class Agile { * @param config - Configuration object */ constructor(config: CreateAgileConfigInterface = {}) { - config = defineConfig(config, { - localStorage: true, + config = { + localStorage: false, waitForMount: true, - logConfig: {}, bindGlobal: false, - }); - config.logConfig = defineConfig(config.logConfig, { - prefix: 'Agile', - active: true, - level: Logger.level.WARN, - canUseCustomStyles: true, - allowedTags: ['runtime', 'storage', 'subscription', 'multieditor'], - }); + autoIntegrate: true, + bucket: true, + ...config, + }; this.config = { waitForMount: config.waitForMount as any, + bucket: config.bucket as any, }; - this.integrations = new Integrations(this); + this.key = config.key; + this.integrations = new Integrations(this, { + autoIntegrate: config.autoIntegrate, + }); this.runtime = new Runtime(this); this.subController = new SubController(this); this.storages = new Storages(this, { localStorage: config.localStorage, }); - // Assign customized Logger config to the static Logger - Agile.logger = new Logger(config.logConfig); - - LogCodeManager.log('10:00:00', [], this, Agile.logger); + LogCodeManager.log('10:00:00', [], this); // Create a global instance of the Agile Instance. // Why? 'getAgileInstance()' returns the global Agile Instance // if it couldn't find any Agile Instance in the specified Instance. if (config.bindGlobal) - if (!globalBind(Agile.globalKey, this)) LogCodeManager.log('10:02:00'); + if (!globalBind(Agile.globalKey, this)) { + LogCodeManager.log('10:02:00'); + } } /** @@ -128,7 +121,7 @@ export class Agile { * @param config - Configuration object */ public createStorage(config: CreateStorageConfigInterface): Storage { - return new Storage(config); + return createStorage(config); } /** @@ -150,7 +143,10 @@ export class Agile { initialValue: ValueType, config: StateConfigInterface = {} ): State { - return new State(this, initialValue, config); + return createState(initialValue, { + ...config, + ...{ agileInstance: this }, + }); } /** @@ -174,7 +170,7 @@ export class Agile { public createCollection( config?: CollectionConfig ): Collection { - return new Collection(this, config); + return createCollection(config, this); } /** @@ -232,12 +228,14 @@ export class Agile { if (Array.isArray(configOrDeps)) { _config = flatMerge(_config, { computedDeps: configOrDeps, + agileInstance: this, }); } else { - if (configOrDeps) _config = configOrDeps; + if (configOrDeps) + _config = { ...configOrDeps, ...{ agileInstance: this } }; } - return new Computed(this, computeFunction, _config); + return createComputed(computeFunction, _config); } /** @@ -303,18 +301,10 @@ export class Agile { } } -export interface CreateAgileConfigInterface { - /** - * Configures the logging behaviour of AgileTs. - * @default { - prefix: 'Agile', - active: true, - level: Logger.level.WARN, - canUseCustomStyles: true, - allowedTags: ['runtime', 'storage', 'subscription', 'multieditor'], - } - */ - logConfig?: CreateLoggerConfigInterface; +export type AgileKey = string | number; + +export interface CreateAgileConfigInterface + extends IntegrationsConfigInterface { /** * Whether the Subscription Container shouldn't be ready * until the UI-Component it represents has been mounted. @@ -323,7 +313,7 @@ export interface CreateAgileConfigInterface { waitForMount?: boolean; /** * Whether the Local Storage should be registered as a Agile Storage by default. - * @default true + * @default false */ localStorage?: boolean; /** @@ -332,6 +322,20 @@ export interface CreateAgileConfigInterface { * @default false */ bindGlobal?: boolean; + /** + * Key/Name identifier of the Agile Instance. + * @default undefined + */ + key?: AgileKey; + /** + * Whether to put render events into "The bucket" of the browser, + * where all events are first put in wait for the UI thread + * to be done with whatever it's doing. + * + * [Learn more..](https://stackoverflow.com/questions/9083594/call-settimeout-without-delay) + * @default true + */ + bucket?: boolean; } export interface AgileConfigInterface { @@ -341,4 +345,13 @@ export interface AgileConfigInterface { * @default true */ waitForMount: boolean; + /** + * Whether to put render events into "The bucket" of the browser, + * where all events are first put in wait for the UI thread + * to be done with whatever it's doing. + * + * [Learn more..](https://stackoverflow.com/questions/9083594/call-settimeout-without-delay) + * @default true + */ + bucket: boolean; } diff --git a/packages/core/src/collection/collection.persistent.ts b/packages/core/src/collection/collection.persistent.ts index 86b049a3..0289620b 100644 --- a/packages/core/src/collection/collection.persistent.ts +++ b/packages/core/src/collection/collection.persistent.ts @@ -3,7 +3,6 @@ import { CollectionKey, CreatePersistentConfigInterface, DefaultItem, - defineConfig, Group, GroupKey, ItemKey, @@ -37,11 +36,12 @@ export class CollectionPersistent< super(collection.agileInstance(), { instantiate: false, }); - config = defineConfig(config, { + config = { instantiate: true, storageKeys: [], - defaultStorageKey: null, - }); + defaultStorageKey: null as any, + ...config, + }; this.collection = () => collection; this.instantiatePersistent({ key: config.key, @@ -145,7 +145,7 @@ export class CollectionPersistent< // that it was loaded completely and exists at all dummyItem?.persist(itemStorageKey, { loadValue: false, - defaultStorageKey: this.config.defaultStorageKey || undefined, + defaultStorageKey: this.config.defaultStorageKey as any, storageKeys: this.storageKeys, followCollectionPersistKeyPattern: false, // Because of the dynamic 'storageItemKey', the key is already formatted above }); diff --git a/packages/core/src/collection/group/group.observer.ts b/packages/core/src/collection/group/group.observer.ts index d298f653..c9da7654 100644 --- a/packages/core/src/collection/group/group.observer.ts +++ b/packages/core/src/collection/group/group.observer.ts @@ -3,7 +3,6 @@ import { Group, CreateObserverConfigInterface, copy, - defineConfig, equal, generateId, RuntimeJob, @@ -68,7 +67,7 @@ export class GroupObserver extends Observer { config: GroupIngestConfigInterface = {} ): void { const group = this.group(); - config = defineConfig(config, { + config = { perform: true, background: false, sideEffects: { @@ -77,7 +76,8 @@ export class GroupObserver extends Observer { }, force: false, maxTriesToUpdate: 3, - }); + ...config, + }; // Force overwriting the Group value if it is a placeholder. // After assigning a value to the Group, the Group is supposed to be no placeholder anymore. diff --git a/packages/core/src/collection/group/index.ts b/packages/core/src/collection/group/index.ts index af25f737..03f1d413 100644 --- a/packages/core/src/collection/group/index.ts +++ b/packages/core/src/collection/group/index.ts @@ -3,7 +3,6 @@ import { Collection, DefaultItem, ItemKey, - defineConfig, normalizeArray, Item, copy, @@ -184,10 +183,11 @@ export class Group< const notExistingItemKeysInCollection: Array = []; const existingItemKeys: Array = []; let newGroupValue = copy(this.nextStateValue); - config = defineConfig(config, { + config = { method: 'push', overwrite: false, - }); + ...config, + }; // Add itemKeys to Group _itemKeys.forEach((itemKey) => { @@ -304,12 +304,13 @@ export class Group< key = keyOrConfig as PersistentKey; } - _config = defineConfig(_config, { + _config = { loadValue: true, followCollectionPersistKeyPattern: true, storageKeys: [], - defaultStorageKey: null, - }); + defaultStorageKey: null as any, + ..._config, + }; // Create storageItemKey based on Collection key/name identifier if (_config.followCollectionPersistKeyPattern) { diff --git a/packages/core/src/collection/index.ts b/packages/core/src/collection/index.ts index 32272973..14d707cb 100644 --- a/packages/core/src/collection/index.ts +++ b/packages/core/src/collection/index.ts @@ -7,7 +7,6 @@ import { SelectorKey, StorageKey, GroupConfigInterface, - defineConfig, isValidObject, normalizeArray, copy, @@ -72,12 +71,13 @@ export class Collection< constructor(agileInstance: Agile, config: CollectionConfig = {}) { this.agileInstance = () => agileInstance; let _config = typeof config === 'function' ? config(this) : config; - _config = defineConfig(_config, { + _config = { primaryKey: 'id', groups: {}, selectors: {}, defaultGroupKey: 'default', - }); + ..._config, + }; this._key = _config.key; this.config = { defaultGroupKey: _config.defaultGroupKey as any, @@ -304,12 +304,13 @@ export class Collection< const _groupKeys = normalizeArray(groupKeys); const defaultGroupKey = this.config.defaultGroupKey; const primaryKey = this.config.primaryKey; - config = defineConfig(config, { + config = { method: 'push', background: false, patch: false, select: false, - }); + ...config, + }; // Add default groupKey, since all Items are added to the default Group if (!_groupKeys.includes(defaultGroupKey)) _groupKeys.push(defaultGroupKey); @@ -373,10 +374,11 @@ export class Collection< ): Item | undefined { const item = this.getItem(itemKey, { notExisting: true }); const primaryKey = this.config.primaryKey; - config = defineConfig(config, { + config = { patch: true, background: false, - }); + ...config, + }; // Check if the given conditions are suitable for a update action if (item == null) { @@ -405,9 +407,10 @@ export class Collection< let patchConfig: { addNewProperties?: boolean } = typeof config.patch === 'object' ? config.patch : {}; - patchConfig = defineConfig(patchConfig, { + patchConfig = { addNewProperties: true, - }); + ...patchConfig, + }; item.patch(changes as any, { background: config.background, @@ -495,9 +498,10 @@ export class Collection< groupKey: GroupKey | undefined | null, config: HasConfigInterface = {} ): Group | undefined { - config = defineConfig(config, { + config = { notExisting: false, - }); + ...config, + }; // Retrieve Group const group = groupKey ? this.groups[groupKey] : undefined; @@ -668,9 +672,10 @@ export class Collection< selectorKey: SelectorKey | undefined | null, config: HasConfigInterface = {} ): Selector | undefined { - config = defineConfig(config, { + config = { notExisting: false, - }); + ...config, + }; // Get Selector const selector = selectorKey ? this.selectors[selectorKey] : undefined; @@ -775,9 +780,10 @@ export class Collection< itemKey: ItemKey | undefined | null, config: HasConfigInterface = {} ): Item | undefined { - config = defineConfig(config, { + config = { notExisting: false, - }); + ...config, + }; // Get Item const item = itemKey != null ? this.data[itemKey] : undefined; @@ -879,9 +885,10 @@ export class Collection< * @param config - Configuration object */ public getAllItems(config: HasConfigInterface = {}): Array> { - config = defineConfig(config, { + config = { notExisting: false, - }); + ...config, + }; const defaultGroup = this.getDefaultGroup(); let items: Array> = []; @@ -962,11 +969,12 @@ export class Collection< key = keyOrConfig as StorageKey; } - _config = defineConfig(_config, { + _config = { loadValue: true, storageKeys: [], - defaultStorageKey: null, - }); + defaultStorageKey: null as any, + ..._config, + }; // Check if Collection is already persisted if (this.persistent != null && this.isPersisted) return this; @@ -1105,9 +1113,10 @@ export class Collection< config: UpdateItemKeyConfigInterface = {} ): boolean { const item = this.getItem(oldItemKey, { notExisting: true }); - config = defineConfig(config, { + config = { background: false, - }); + ...config, + }; if (item == null || oldItemKey === newItemKey) return false; @@ -1277,10 +1286,11 @@ export class Collection< itemKeys: ItemKey | Array, config: RemoveItemsConfigInterface = {} ): this { - config = defineConfig(config, { + config = { notExisting: false, removeSelector: false, - }); + ...config, + }; const _itemKeys = normalizeArray(itemKeys); _itemKeys.forEach((itemKey) => { @@ -1339,10 +1349,11 @@ export class Collection< data: DataType, config: AssignDataConfigInterface = {} ): boolean { - config = defineConfig(config, { + config = { patch: false, background: false, - }); + ...config, + }; const _data = copy(data); // Copy data object to get rid of reference const primaryKey = this.config.primaryKey; @@ -1397,10 +1408,11 @@ export class Collection< item: Item, config: AssignItemConfigInterface = {} ): boolean { - config = defineConfig(config, { + config = { overwrite: false, background: false, - }); + ...config, + }; const primaryKey = this.config.primaryKey; let itemKey = item._value[primaryKey]; let increaseCollectionSize = true; @@ -1457,13 +1469,14 @@ export class Collection< itemKey: ItemKey, config: RebuildGroupsThatIncludeItemKeyConfigInterface = {} ): void { - config = defineConfig(config, { + config = { background: false, sideEffects: { enabled: true, exclude: [], }, - }); + ...config, + }; // Rebuild Groups that include itemKey for (const groupKey in this.groups) { diff --git a/packages/core/src/collection/item.ts b/packages/core/src/collection/item.ts index 5375b143..7797e042 100644 --- a/packages/core/src/collection/item.ts +++ b/packages/core/src/collection/item.ts @@ -3,7 +3,6 @@ import { Collection, StateKey, StateRuntimeJobConfigInterface, - defineConfig, SelectorKey, PersistentKey, isValidObject, @@ -63,7 +62,7 @@ export class Item extends State< config: StateRuntimeJobConfigInterface = {} ): this { super.setKey(value); - config = defineConfig(config, { + config = { sideEffects: { enabled: true, exclude: [], @@ -72,7 +71,8 @@ export class Item extends State< force: false, storage: true, overwrite: false, - }); + ...config, + }; if (value == null) return this; // Update 'rebuildGroupsThatIncludeItemKey' side effect to the new itemKey @@ -129,12 +129,13 @@ export class Item extends State< key = keyOrConfig as PersistentKey; } - _config = defineConfig(_config, { + _config = { loadValue: true, followCollectionPersistKeyPattern: true, storageKeys: [], - defaultStorageKey: null, - }); + defaultStorageKey: null as any, + ..._config, + }; // Create storageItemKey based on Collection key/name identifier if (_config.followCollectionPersistKeyPattern) { diff --git a/packages/core/src/collection/selector.ts b/packages/core/src/collection/selector.ts index 5e484e9d..7937dd66 100644 --- a/packages/core/src/collection/selector.ts +++ b/packages/core/src/collection/selector.ts @@ -1,7 +1,6 @@ import { Collection, DefaultItem, - defineConfig, Item, ItemKey, State, @@ -41,9 +40,10 @@ export class Selector< itemKey: ItemKey | null, config: SelectorConfigInterface = {} ) { - config = defineConfig(config, { + config = { isPlaceholder: false, - }); + ...config, + }; super(collection.agileInstance(), null, config); this.collection = () => collection; this._item = null; @@ -116,7 +116,7 @@ export class Selector< itemKey: ItemKey | null, config: StateRuntimeJobConfigInterface = {} ): this { - config = defineConfig(config, { + config = { background: false, sideEffects: { enabled: true, @@ -125,7 +125,8 @@ export class Selector< force: false, overwrite: this._item?.isPlaceholder ?? false, storage: true, - }); + ...config, + }; // Don't select Item if Collection is not correctly instantiated yet // (because only after a successful instantiation the Collection @@ -227,10 +228,7 @@ export class Selector< item.selectedBy.delete(this._key as any); item.removeSideEffect(Selector.rebuildSelectorSideEffectKey); item.removeSideEffect(Selector.rebuildItemSideEffectKey); - if ( - item.isPlaceholder && - this._itemKey != null - ) + if (item.isPlaceholder && this._itemKey != null) delete this.collection().data[this._itemKey]; } diff --git a/packages/core/src/computed/index.ts b/packages/core/src/computed/index.ts index b166deb1..efc19b5e 100644 --- a/packages/core/src/computed/index.ts +++ b/packages/core/src/computed/index.ts @@ -1,12 +1,10 @@ import { State, Agile, - defineConfig, Observer, StateConfigInterface, ComputedTracker, Collection, - extractObservers, StateIngestConfigInterface, removeProperties, LogCodeManager, @@ -52,10 +50,11 @@ export class Computed extends State< key: config.key, dependents: config.dependents, }); - config = defineConfig(config, { + config = { computedDeps: [], autodetect: !isAsyncFunction(computeFunction), - }); + ...config, + }; this.agileInstance = () => agileInstance; this.computeFunction = computeFunction; this.config = { @@ -87,9 +86,10 @@ export class Computed extends State< * @param config - Configuration object */ public recompute(config: RecomputeConfigInterface = {}): this { - config = defineConfig(config, { + config = { autodetect: false, - }); + ...config, + }; this.compute({ autodetect: config.autodetect }).then((result) => { this.observers['value'].ingestValue( result, @@ -120,9 +120,10 @@ export class Computed extends State< deps: Array = [], config: RecomputeConfigInterface = {} ): this { - config = defineConfig(config, { + config = { autodetect: this.config.autodetect, - }); + ...config, + }; // Make this Observer no longer depend on the old dep Observers this.deps.forEach((observer) => { @@ -160,9 +161,10 @@ export class Computed extends State< public async compute( config: ComputeConfigInterface = {} ): Promise { - config = defineConfig(config, { + config = { autodetect: this.config.autodetect, - }); + ...config, + }; // Start auto tracking of Observers on which the computeFunction might depend if (config.autodetect) ComputedTracker.track(); diff --git a/packages/core/src/integrations/index.ts b/packages/core/src/integrations/index.ts index 038166db..a7421b18 100644 --- a/packages/core/src/integrations/index.ts +++ b/packages/core/src/integrations/index.ts @@ -1,5 +1,9 @@ import { Agile, Integration, LogCodeManager } from '../internal'; +const onRegisterInitialIntegrationCallbacks: (( + integration: Integration +) => void)[] = []; + export class Integrations { // Agile Instance the Integrations belongs to public agileInstance: () => Agile; @@ -7,6 +11,42 @@ export class Integrations { // 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 @@ -14,14 +54,21 @@ export class Integrations { * * @internal * @param agileInstance - Instance of Agile the Integrations belongs to. + * @param config - Configuration object */ - constructor(agileInstance: Agile) { + constructor(agileInstance: Agile, config: IntegrationsConfigInterface = {}) { + config = { + autoIntegrate: true, + ...config, + }; this.agileInstance = () => agileInstance; - // Integrate initial Integrations which were statically set externally - Agile.initialIntegrations.forEach((integration) => - this.integrate(integration) - ); + if (config.autoIntegrate) { + // Setup listener to be notified when an external registered Integration was added + Integrations.onRegisterInitialIntegration((integration) => { + this.integrate(integration); + }); + } } /** @@ -33,8 +80,12 @@ export class Integrations { */ public async integrate(integration: Integration): Promise { // Check if Integration is valid - if (!integration._key) { - LogCodeManager.log('18:03:00', [integration._key], integration); + if (integration._key == null) { + LogCodeManager.log( + '18:03:00', + [integration._key, this.agileInstance().key], + integration + ); return false; } @@ -47,7 +98,11 @@ export class Integrations { this.integrations.add(integration); integration.integrated = true; - LogCodeManager.log('18:00:00', [integration._key], integration); + LogCodeManager.log( + '18:00:00', + [integration._key, this.agileInstance().key], + integration + ); return true; } @@ -84,3 +139,14 @@ export class Integrations { 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/internal.ts b/packages/core/src/internal.ts index 6e403e5a..b9356568 100644 --- a/packages/core/src/internal.ts +++ b/packages/core/src/internal.ts @@ -4,17 +4,20 @@ // !! All internal Agile modules must be imported from here!! -// Logger -export * from '@agile-ts/logger'; -export * from './logCodeManager'; - // 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'; @@ -47,6 +50,5 @@ export * from './collection/item'; export * from './collection/selector'; export * from './collection/collection.persistent'; -// Integrations -export * from './integrations'; -export * from './integrations/integration'; +// Shared +export * from './shared'; diff --git a/packages/core/src/logCodeManager.ts b/packages/core/src/logCodeManager.ts index 36a1eba0..f7a6788b 100644 --- a/packages/core/src/logCodeManager.ts +++ b/packages/core/src/logCodeManager.ts @@ -1,4 +1,10 @@ -import { Agile } from './agile'; +// 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. @@ -33,7 +39,7 @@ const logCodeMessages = { // Storages '11:02:00': - "The 'Local Storage' is not available in your current environment." + + "The 'Local Storage' is not available in your current environment. " + "To use the '.persist()' functionality, please provide a custom Storage!", '11:02:01': 'The first allocated Storage for AgileTs must be set as the default Storage!', @@ -59,6 +65,7 @@ const logCodeMessages = { "The Storage with the key/name '${1}' doesn't exists!`", // Storage + '13:00:00': "Registered new Storage '${0}'.", '13:01:00': "GET value at key '${1}' from Storage '${0}'.", '13:01:01': "SET value at key '${1}' in Storage '${0}'.", '13:01:02': "REMOVE value at key '${1}' from Storage '${0}'.", @@ -96,10 +103,10 @@ const logCodeMessages = { "The 'perform()' method isn't set in Observer but need to be set! Observer is no stand alone class.", // Integrations - '18:00:00': "Integrated '${0}' into AgileTs", + '18:00:00': "Integrated '${0}' into AgileTs '${1}'", '18:02:00': "Can't call the 'update()' method on a not ready Integration '${0}'!", - '18:03:00': "Failed to integrate Framework '${0}'!", + '18:03:00': "Failed to integrate Framework '${0}' into AgileTs '${1}'!", // Computed '19:03:00': @@ -180,9 +187,9 @@ function getLog>( ): string { let result = logCodeMessages[logCode] ?? `'${logCode}' is a unknown logCode!`; - for (const i in replacers) { - // https://stackoverflow.com/questions/41438656/why-do-i-get-cannot-read-property-tostring-of-undefined - result = result.split('${' + i + '}').join(replacers[i] + ''); + // Replace '${x}' with the specified replacer instances + for (let i = 0; i < replacers.length; i++) { + result = result.replace('${' + i + '}', replacers[i]); } return result; @@ -190,7 +197,7 @@ function getLog>( /** * Logs the log message according to the specified log code - * with the Agile Logger. + * with the Agile Logger if installed or the normal console. * * @internal * @param logCode - Log code of the message to be returned. @@ -203,9 +210,52 @@ function log>( replacers: any[] = [], ...data: any[] ): void { - const codes = logCode.split(':'); - if (codes.length === 3) - Agile.logger[logCodeTypes[codes[1]]](getLog(logCode, replacers), ...data); + const logger = LogCodeManager.getLogger(); + if (!logger?.isActive) return; + const logType = logCodeTypes[logCode.substr(3, 2)]; + if (typeof logType !== 'string') return; + + // Handle logging without Logger + if (logger == null) { + if (logType === 'error' || logType === 'warn') + console[logType](getLog(logCode, replacers)); + return; + } + + // Handle logging with Logger + logger[logType](getLog(logCode, replacers), ...data); +} + +/** + * Logs the log message according to the specified log code + * with the Agile Logger if installed and the provided tags are active. + * + * @internal + * @param tags - Tags to be active to log the logCode. + * @param logCode - Log code of the message to be returned. + * @param replacers - Instances that replace these '${x}' placeholders based on the index + * For example: 'replacers[0]' replaces '${0}', 'replacers[1]' replaces '${1}', .. + * @param data - Data to be attached to the end of the log message. + */ +function logIfTags>( + tags: string[], + logCode: T, + replacers: any[] = [], + ...data: any[] +): void { + const logger = LogCodeManager.getLogger(); + if (!logger?.isActive) return; + const logType = logCodeTypes[logCode.substr(3, 2)]; + if (typeof logType !== 'string') return; + + // Handle logging without Logger + if (logger == null) { + // Log nothing since if a log has a tag it is probably not so important + return; + } + + // Handle logging with Logger + logger.if.tag(tags)[logType](getLog(logCode, replacers), ...data); } /** @@ -219,6 +269,12 @@ export const LogCodeManager = { 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, }; export type LogCodesArrayType = { diff --git a/packages/core/src/runtime/index.ts b/packages/core/src/runtime/index.ts index f5578699..f5222a3b 100644 --- a/packages/core/src/runtime/index.ts +++ b/packages/core/src/runtime/index.ts @@ -4,7 +4,6 @@ import { RuntimeJob, CallbackSubscriptionContainer, ComponentSubscriptionContainer, - defineConfig, notEqual, LogCodeManager, } from '../internal'; @@ -28,6 +27,9 @@ export class Runtime { // 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.) @@ -64,16 +66,15 @@ export class Runtime { * @param config - Configuration object */ public ingest(job: RuntimeJob, config: IngestConfigInterface = {}): void { - config = defineConfig(config, { + config = { perform: !this.isPerformingJobs, - }); + ...config, + }; // Add specified Job to the queue this.jobQueue.push(job); - Agile.logger.if - .tag(['runtime']) - .info(LogCodeManager.getLog('16:01:00', [job._key]), job); + LogCodeManager.logIfTags(['runtime'], '16:01:00', [job._key], job); // Run first Job from the queue if (config.perform) { @@ -110,9 +111,7 @@ export class Runtime { if (job.rerender) this.jobsToRerender.push(job); this.currentJob = null; - Agile.logger.if - .tag(['runtime']) - .info(LogCodeManager.getLog('16:01:01', [job._key]), job); + 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) @@ -123,10 +122,18 @@ export class Runtime { } else { this.isPerformingJobs = false; if (this.jobsToRerender.length > 0) { - // https://stackoverflow.com/questions/9083594/call-settimeout-without-delay - setTimeout(() => { - this.updateSubscribers(); - }); + 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(); } } } @@ -174,9 +181,13 @@ export class Runtime { public extractToUpdateSubscriptionContainer( jobs: Array ): Array { + // https://medium.com/@bretcameron/how-to-make-your-code-faster-using-javascript-sets-b432457a4a77 const subscriptionsToUpdate = new Set(); - jobs.forEach((job) => { + // 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; @@ -228,7 +239,7 @@ export class Runtime { job.subscriptionContainersToUpdate.delete(subscriptionContainer); }); - }); + } return Array.from(subscriptionsToUpdate); } @@ -245,7 +256,11 @@ export class Runtime { public updateSubscriptionContainer( subscriptionsToUpdate: Array ): void { - subscriptionsToUpdate.forEach((subscriptionContainer) => { + // 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(); @@ -258,11 +273,14 @@ export class Runtime { ); subscriptionContainer.updatedSubscribers.clear(); - }); + } - Agile.logger.if - .tag(['runtime']) - .info(LogCodeManager.getLog('16:01:02'), subscriptionsToUpdate); + LogCodeManager.logIfTags( + ['runtime'], + '16:01:02', + [], + subscriptionsToUpdate + ); } /** diff --git a/packages/core/src/runtime/observer.ts b/packages/core/src/runtime/observer.ts index bce9e5fe..4897a059 100644 --- a/packages/core/src/runtime/observer.ts +++ b/packages/core/src/runtime/observer.ts @@ -3,7 +3,6 @@ import { StateKey, RuntimeJob, SubscriptionContainer, - defineConfig, IngestConfigInterface, CreateRuntimeJobConfigInterface, LogCodeManager, @@ -60,10 +59,11 @@ export class Observer { agileInstance: Agile, config: CreateObserverConfigInterface = {} ) { - config = defineConfig(config, { + config = { dependents: [], subs: [], - }); + ...config, + }; this.agileInstance = () => agileInstance; this._key = config.key; this.value = config.value; @@ -104,7 +104,7 @@ export class Observer { * @param config - Configuration object */ public ingest(config: ObserverIngestConfigInterface = {}): void { - config = defineConfig(config, { + config = { perform: true, background: false, sideEffects: { @@ -112,7 +112,8 @@ export class Observer { exclude: [], }, force: false, - }); + ...config, + }; // Create Runtime-Job const job = new RuntimeJob(this, { diff --git a/packages/core/src/runtime/runtime.job.ts b/packages/core/src/runtime/runtime.job.ts index d3eea3f2..84fbb5f1 100644 --- a/packages/core/src/runtime/runtime.job.ts +++ b/packages/core/src/runtime/runtime.job.ts @@ -1,4 +1,4 @@ -import { Observer, defineConfig, SubscriptionContainer } from '../internal'; +import { Observer, SubscriptionContainer } from '../internal'; export class RuntimeJob { public config: RuntimeJobConfigInterface; @@ -32,7 +32,7 @@ export class RuntimeJob { observer: ObserverType, config: CreateRuntimeJobConfigInterface = {} ) { - config = defineConfig(config, { + config = { background: false, sideEffects: { enabled: true, @@ -40,7 +40,8 @@ export class RuntimeJob { }, force: false, maxTriesToUpdate: 3, - }); + ...config, + }; this.config = { background: config.background, force: config.force, diff --git a/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts b/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts index 4c0dcb17..34aca13e 100644 --- a/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts +++ b/packages/core/src/runtime/subscription/container/SubscriptionContainer.ts @@ -1,9 +1,4 @@ -import { - defineConfig, - generateId, - isValidObject, - Observer, -} from '../../../internal'; +import { generateId, isValidObject, Observer } from '../../../internal'; export class SubscriptionContainer { /** @@ -110,11 +105,10 @@ export class SubscriptionContainer { subs: Array | { [key: string]: Observer }, config: SubscriptionContainerConfigInterface = {} ) { - config = defineConfig(config, { - proxyWeakMap: new WeakMap(), - selectorWeakMap: new WeakMap(), + config = { key: generateId(), - }); + ...config, + }; this.subscribers = new Set(); this.key = config.key; diff --git a/packages/core/src/runtime/subscription/sub.controller.ts b/packages/core/src/runtime/subscription/sub.controller.ts index d738d5b5..3f6d22ea 100644 --- a/packages/core/src/runtime/subscription/sub.controller.ts +++ b/packages/core/src/runtime/subscription/sub.controller.ts @@ -6,7 +6,6 @@ import { CallbackSubscriptionContainer, isFunction, SubscriptionContainerConfigInterface, - defineConfig, removeProperties, LogCodeManager, } from '../../internal'; @@ -113,9 +112,10 @@ export class SubController { subscriptionContainer: SubscriptionContainer; props: { [key: string]: Observer['value'] }; } { - config = defineConfig(config, { + config = { waitForMount: this.agileInstance().config.waitForMount, - }); + ...config, + }; // Create Subscription Container based on specified 'integrationInstance' const subscriptionContainer = isFunction(integrationInstance) @@ -165,9 +165,12 @@ export class SubController { if (subscriptionInstance instanceof CallbackSubscriptionContainer) { unsub(subscriptionInstance); this.callbackSubs.delete(subscriptionInstance); - Agile.logger.if - .tag(['runtime', 'subscription']) - .info(LogCodeManager.getLog('15:01:00'), subscriptionInstance); + LogCodeManager.logIfTags( + ['subscription'], + '15:01:00', + [], + subscriptionInstance + ); return; } @@ -175,9 +178,12 @@ export class SubController { if (subscriptionInstance instanceof ComponentSubscriptionContainer) { unsub(subscriptionInstance); this.componentSubs.delete(subscriptionInstance); - Agile.logger.if - .tag(['runtime', 'subscription']) - .info(LogCodeManager.getLog('15:01:01'), subscriptionInstance); + LogCodeManager.logIfTags( + ['subscription'], + '15:01:01', + [], + subscriptionInstance + ); return; } @@ -191,9 +197,12 @@ export class SubController { (subContainer) => { unsub(subContainer as ComponentSubscriptionContainer); this.componentSubs.delete(subContainer); - Agile.logger.if - .tag(['runtime', 'subscription']) - .info(LogCodeManager.getLog('15:01:01'), subscriptionInstance); + LogCodeManager.logIfTags( + ['subscription'], + '15:01:01', + [], + subscriptionInstance + ); } ); return; @@ -241,9 +250,12 @@ export class SubController { componentSubscriptionContainer, ]; - Agile.logger.if - .tag(['runtime', 'subscription']) - .info(LogCodeManager.getLog('15:01:02'), componentSubscriptionContainer); + LogCodeManager.logIfTags( + ['subscription'], + '15:01:02', + [], + componentSubscriptionContainer + ); return componentSubscriptionContainer; } @@ -270,9 +282,12 @@ export class SubController { this.callbackSubs.add(callbackSubscriptionContainer); callbackSubscriptionContainer.ready = true; - Agile.logger.if - .tag(['runtime', 'subscription']) - .info(LogCodeManager.getLog('15:01:03'), callbackSubscriptionContainer); + LogCodeManager.logIfTags( + ['subscription'], + '15:01:03', + [], + callbackSubscriptionContainer + ); return callbackSubscriptionContainer; } diff --git a/packages/core/src/shared.ts b/packages/core/src/shared.ts new file mode 100644 index 00000000..79d792d3 --- /dev/null +++ b/packages/core/src/shared.ts @@ -0,0 +1,199 @@ +import { + Agile, + Collection, + CollectionConfig, + Computed, + ComputeFunctionType, + CreateComputedConfigInterface, + CreateStorageConfigInterface, + DefaultItem, + DependableAgileInstancesType, + flatMerge, + removeProperties, + runsOnServer, + State, + StateConfigInterface, + Storage, +} from './internal'; + +/** + * Shared Agile Instance that is used when no Agile Instance was specified. + */ +let sharedAgileInstance = new Agile({ + key: 'shared', + localStorage: !runsOnServer(), +}); +export { sharedAgileInstance as shared }; + +/** + * Assigns the specified Agile Instance as the shared Agile Instance. + * + * @param agileInstance - Agile Instance to become the new shared Agile Instance. + */ +// https://stackoverflow.com/questions/32558514/javascript-es6-export-const-vs-export-let +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 = { + agileInstance: sharedAgileInstance, + ...config, + }; + 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 = flatMerge(_config, { + computedDeps: configOrDeps, + }); + } else { + if (configOrDeps) _config = configOrDeps; + } + + _config = { + agileInstance: sharedAgileInstance, + ..._config, + }; + + return new Computed( + _config.agileInstance as any, + computeFunction, + removeProperties(_config, ['agileInstance']) + ); +} + +export interface CreateAgileSubInstanceInterface { + /** + * Instance of Agile the Instance belongs to. + * @default sharedAgileInstance + */ + 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 b70a3a70..6c3cf779 100644 --- a/packages/core/src/state/index.ts +++ b/packages/core/src/state/index.ts @@ -2,7 +2,6 @@ import { Agile, StorageKey, copy, - defineConfig, flatMerge, isValidObject, StateObserver, @@ -84,10 +83,11 @@ export class State { initialValue: ValueType, config: StateConfigInterface = {} ) { - config = defineConfig(config, { + config = { dependents: [], isPlaceholder: false, - }); + ...config, + }; this.agileInstance = () => agileInstance; this._key = config.key; this.observers['value'] = new StateObserver(this, { @@ -196,9 +196,10 @@ export class State { value: ValueType | ((value: ValueType) => ValueType), config: StateIngestConfigInterface = {} ): this { - config = defineConfig(config, { + config = { force: false, - }); + ...config, + }; const _value = isFunction(value) ? (value as any)(copy(this._value)) : value; @@ -299,9 +300,10 @@ export class State { targetWithChanges: Object, config: PatchConfigInterface = {} ): this { - config = defineConfig(config, { + config = { addNewProperties: true, - }); + ...config, + }; // Check if the given conditions are suitable for a patch action if (!isValidObject(this.nextStateValue, true)) { @@ -467,11 +469,12 @@ export class State { key = keyOrConfig as PersistentKey; } - _config = defineConfig(_config, { + _config = { loadValue: true, storageKeys: [], - defaultStorageKey: null, - }); + defaultStorageKey: null as any, + ..._config, + }; // Check if State is already persisted if (this.persistent != null && this.isPersisted) return this; @@ -697,9 +700,10 @@ export class State { callback: SideEffectFunctionType, config: AddSideEffectConfigInterface = {} ): this { - config = defineConfig(config, { + config = { weight: 10, - }); + ...config, + }; if (!isFunction(callback)) { LogCodeManager.log('00:03:01', ['Side Effect Callback', 'function']); return this; diff --git a/packages/core/src/state/state.observer.ts b/packages/core/src/state/state.observer.ts index 85598d79..2b874757 100644 --- a/packages/core/src/state/state.observer.ts +++ b/packages/core/src/state/state.observer.ts @@ -3,7 +3,6 @@ import { State, Computed, copy, - defineConfig, equal, notEqual, isFunction, @@ -81,7 +80,7 @@ export class StateObserver extends Observer { config: StateIngestConfigInterface = {} ): void { const state = this.state(); - config = defineConfig(config, { + config = { perform: true, background: false, sideEffects: { @@ -92,7 +91,8 @@ export class StateObserver extends Observer { storage: true, overwrite: false, maxTriesToUpdate: 3, - }); + ...config, + }; // Force overwriting the State value if it is a placeholder. // After assigning a value to the State, the State is supposed to be no placeholder anymore. diff --git a/packages/core/src/state/state.persistent.ts b/packages/core/src/state/state.persistent.ts index 03029ff0..4b544388 100644 --- a/packages/core/src/state/state.persistent.ts +++ b/packages/core/src/state/state.persistent.ts @@ -1,6 +1,5 @@ import { CreatePersistentConfigInterface, - defineConfig, Persistent, PersistentKey, State, @@ -26,11 +25,12 @@ export class StatePersistent extends Persistent { super(state.agileInstance(), { instantiate: false, }); - config = defineConfig(config, { + config = { instantiate: true, storageKeys: [], - defaultStorageKey: null, - }); + defaultStorageKey: null as any, + ...config, + }; this.state = () => state; this.instantiatePersistent({ key: config.key, diff --git a/packages/core/src/state/state.runtime.job.ts b/packages/core/src/state/state.runtime.job.ts index 16e1628a..3faf958d 100644 --- a/packages/core/src/state/state.runtime.job.ts +++ b/packages/core/src/state/state.runtime.job.ts @@ -1,5 +1,4 @@ import { - defineConfig, RuntimeJob, RuntimeJobConfigInterface, RuntimeJobKey, @@ -25,7 +24,7 @@ export class StateRuntimeJob extends RuntimeJob { config: CreateStateRuntimeJobConfigInterface = {} ) { super(observer, config); - config = defineConfig(config, { + config = { background: false, sideEffects: { enabled: true, @@ -35,7 +34,8 @@ export class StateRuntimeJob extends RuntimeJob { storage: true, overwrite: false, maxTriesToUpdate: 3, - }); + ...config, + }; this.config = { background: config.background, diff --git a/packages/core/src/storages/index.ts b/packages/core/src/storages/index.ts index 552a96b7..75eddab5 100644 --- a/packages/core/src/storages/index.ts +++ b/packages/core/src/storages/index.ts @@ -1,7 +1,6 @@ import { Agile, Storage, - defineConfig, Persistent, StorageKey, StorageItemKey, @@ -34,10 +33,11 @@ export class Storages { config: CreateStoragesConfigInterface = {} ) { this.agileInstance = () => agileInstance; - config = defineConfig(config, { + config = { localStorage: false, - defaultStorageKey: null, - }); + defaultStorageKey: null as any, + ...config, + }; this.config = { defaultStorageKey: config.defaultStorageKey as any }; if (config.localStorage) this.instantiateLocalStorage(); } @@ -115,6 +115,8 @@ export class Storages { } }); + LogCodeManager.log('13:00:00', [storage.key], storage); + return true; } diff --git a/packages/core/src/storages/persistent.ts b/packages/core/src/storages/persistent.ts index a335d982..abeaab17 100644 --- a/packages/core/src/storages/persistent.ts +++ b/packages/core/src/storages/persistent.ts @@ -1,10 +1,4 @@ -import { - Agile, - copy, - defineConfig, - LogCodeManager, - StorageKey, -} from '../internal'; +import { Agile, copy, LogCodeManager, StorageKey } from '../internal'; export class Persistent { // Agile Instance the Persistent belongs to @@ -44,11 +38,12 @@ export class Persistent { ) { this.agileInstance = () => agileInstance; this._key = Persistent.placeHolderKey; - config = defineConfig(config, { + config = { instantiate: true, storageKeys: [], - defaultStorageKey: null, - }); + defaultStorageKey: null as any, + ...config, + }; this.agileInstance().storages.persistentInstances.add(this); this.config = { defaultStorageKey: config.defaultStorageKey as any }; diff --git a/packages/core/src/storages/storage.ts b/packages/core/src/storages/storage.ts index 894a2a6d..01100e4e 100644 --- a/packages/core/src/storages/storage.ts +++ b/packages/core/src/storages/storage.ts @@ -1,9 +1,7 @@ import { isJsonString, - defineConfig, isAsyncFunction, isFunction, - Agile, LogCodeManager, } from '../internal'; @@ -28,10 +26,11 @@ export class Storage { * @param config - Configuration object */ constructor(config: CreateStorageConfigInterface) { - config = defineConfig(config, { + config = { prefix: 'agile', async: false, - }); + ...config, + }; this.key = config.key; this.methods = config.methods; this.config = { @@ -89,12 +88,12 @@ export class Storage { const res = this.methods.get(this.getStorageKey(key)); const _res = isJsonString(res) ? JSON.parse(res) : res; - Agile.logger.if - .tag(['storage']) - .info( - LogCodeManager.getLog('13:01:00', [this.key, this.getStorageKey(key)]), - _res - ); + LogCodeManager.logIfTags( + ['storage'], + '13:01:00', + [this.key, this.getStorageKey(key)], + _res + ); return _res; } @@ -122,15 +121,12 @@ export class Storage { .then((res: any) => { const _res = isJsonString(res) ? JSON.parse(res) : res; - Agile.logger.if - .tag(['storage']) - .info( - LogCodeManager.getLog('13:01:00', [ - this.key, - this.getStorageKey(key), - ]), - _res - ); + LogCodeManager.logIfTags( + ['storage'], + '13:01:00', + [this.key, this.getStorageKey(key)], + _res + ); resolve(_res); }) @@ -149,12 +145,10 @@ export class Storage { public set(key: StorageItemKey, value: any): void { if (!this.ready || !this.methods.set) return; - Agile.logger.if - .tag(['storage']) - .info( - LogCodeManager.getLog('13:01:01', [this.key, this.getStorageKey(key)]), - value - ); + LogCodeManager.logIfTags(['storage'], '13:01:01', [ + this.key, + this.getStorageKey(key), + ]); this.methods.set(this.getStorageKey(key), JSON.stringify(value)); } @@ -169,11 +163,10 @@ export class Storage { public remove(key: StorageItemKey): void { if (!this.ready || !this.methods.remove) return; - Agile.logger.if - .tag(['storage']) - .info( - LogCodeManager.getLog('13:01:02', [this.key, this.getStorageKey(key)]) - ); + LogCodeManager.logIfTags(['storage'], '13:01:02', [ + this.key, + this.getStorageKey(key), + ]); this.methods.remove(this.getStorageKey(key)); } diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index 56af1728..8748f28e 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -5,6 +5,7 @@ import { normalizeArray, isFunction, LogCodeManager, + shared, } from './internal'; /** @@ -25,6 +26,11 @@ export function getAgileInstance(instance: any): Agile | undefined { if (_agileInstance) return _agileInstance; } + // Try to get shared Agile Instance + if (shared instanceof Agile) { + return shared; + } + // Return global bound Agile Instance return globalThis[Agile.globalKey]; } catch (e) { @@ -254,3 +260,16 @@ export function globalBind( } return false; } + +/** + * Returns a boolean indicating whether AgileTs is currently running on a server. + * + * @public + */ +export const runsOnServer = (): boolean => { + return !( + typeof window !== 'undefined' && + typeof window.document !== 'undefined' && + typeof window.document.createElement !== 'undefined' + ); +}; diff --git a/packages/core/tests/helper/logMock.ts b/packages/core/tests/helper/logMock.ts index 49c4c4dc..b431beb8 100644 --- a/packages/core/tests/helper/logMock.ts +++ b/packages/core/tests/helper/logMock.ts @@ -22,7 +22,7 @@ const logTypes = { }; function mockLogs(mockArg?: LogTypes[]): void { - const _mockArg = mockArg ?? ['warn', 'error']; + const _mockArg = mockArg ?? ['warn', 'error', 'log']; mockConsole(_mockArg); } diff --git a/packages/core/tests/integration/collection.persistent.integration.test.ts b/packages/core/tests/integration/collection.persistent.integration.test.ts index 203fbfd4..e1d151ee 100644 --- a/packages/core/tests/integration/collection.persistent.integration.test.ts +++ b/packages/core/tests/integration/collection.persistent.integration.test.ts @@ -17,16 +17,7 @@ describe('Collection Persist Function Tests', () => { delete myStorage[key]; }), }; - - // Define Agile with Storage - const App = new Agile({ localStorage: false }); - App.registerStorage( - App.createStorage({ - key: 'testStorage', - prefix: 'test', - methods: storageMethods, - }) - ); + let App: Agile; interface User { id: number; @@ -36,6 +27,15 @@ describe('Collection Persist Function Tests', () => { beforeEach(() => { LogMock.mockLogs(); jest.clearAllMocks(); + + App = new Agile({ localStorage: false }); + App.registerStorage( + App.createStorage({ + key: 'testStorage', + prefix: 'test', + methods: storageMethods, + }) + ); }); describe('Collection', () => { diff --git a/packages/core/tests/unit/agile.test.ts b/packages/core/tests/unit/agile.test.ts index f1bdec49..2d0dc29e 100644 --- a/packages/core/tests/unit/agile.test.ts +++ b/packages/core/tests/unit/agile.test.ts @@ -7,37 +7,52 @@ import { Storage, Computed, Collection, - Logger, Storages, } from '../../src'; import testIntegration from '../helper/test.integration'; import { LogMock } from '../helper/logMock'; +import * as Shared from '../../src/shared'; -jest.mock('../../src/runtime/index'); -jest.mock('../../src/runtime/subscription/sub.controller'); -jest.mock('../../src/storages/index'); -jest.mock('../../src/integrations/index'); -jest.mock('../../src/storages/storage'); -jest.mock('../../src/collection/index'); -jest.mock('../../src/computed/index'); -/* Can't mock Logger because I somehow can't overwrite a static get method -jest.mock("../../src/logger/index", () => { - return class { - static get level() { +// https://github.com/facebook/jest/issues/5023 +jest.mock('../../src/runtime', () => { + return { + // https://jestjs.io/docs/mock-function-api#mockfnmockimplementationfn + Runtime: jest.fn().mockImplementation(() => { return { - TRACE: 1, - DEBUG: 2, - LOG: 5, - TABLE: 5, - INFO: 10, - WARN: 20, - ERROR: 50, + ingest: jest.fn(), }; - } + }), + }; +}); +jest.mock('../../src/runtime/subscription/sub.controller', () => { + return { + SubController: jest.fn(), + }; +}); +jest.mock('../../src/storages', () => { + return { + Storages: jest.fn(), + }; +}); + +// https://gist.github.com/virgs/d9c50e878fc69832c01f8085f2953f12 +// https://medium.com/@masonlgoetz/mock-static-class-methods-in-jest-1ceda967b47f +jest.mock('../../src/integrations', () => { + const mockedInstances = { + // https://jestjs.io/docs/mock-function-api#mockfnmockimplementationfn + Integrations: jest.fn().mockImplementation(() => { + return { + integrate: jest.fn(), + hasIntegration: jest.fn(), + }; + }), }; + // @ts-ignore + mockedInstances.Integrations.onRegisteredExternalIntegration = jest.fn(); + // @ts-ignore + mockedInstances.Integrations.initialIntegrations = []; + return mockedInstances; }); - */ -// jest.mock("../../src/state/index"); // Can't mock State because mocks get instantiated before everything else -> I got the good old not loaded Object error https://github.com/kentcdodds/how-jest-mocking-works describe('Agile Tests', () => { const RuntimeMock = Runtime as jest.MockedClass; @@ -50,131 +65,114 @@ describe('Agile Tests', () => { >; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); + // Clear specified mocks RuntimeMock.mockClear(); SubControllerMock.mockClear(); StoragesMock.mockClear(); IntegrationsMock.mockClear(); - // Reset Global This + // Reset globalThis globalThis[Agile.globalKey] = undefined; + + jest.spyOn(Agile.prototype, 'integrate'); + + jest.clearAllMocks(); }); it('should instantiate Agile (default config)', () => { const agile = new Agile(); - // Check if Agile properties got instantiated properly expect(agile.config).toStrictEqual({ waitForMount: true, + bucket: true, }); - expect(IntegrationsMock).toHaveBeenCalledWith(agile); - expect(agile.integrations).toBeInstanceOf(Integrations); + expect(agile.key).toBeUndefined(); + expect(IntegrationsMock).toHaveBeenCalledWith(agile, { + autoIntegrate: true, + }); + // expect(agile.integrations).toBeInstanceOf(Integrations); // Because 'Integrations' is completely overwritten with a mock (mockImplementation) expect(RuntimeMock).toHaveBeenCalledWith(agile); - expect(agile.runtime).toBeInstanceOf(Runtime); + // expect(agile.runtime).toBeInstanceOf(Runtime); // Because 'Runtime' is completely overwritten with a mock (mockImplementation) expect(SubControllerMock).toHaveBeenCalledWith(agile); expect(agile.subController).toBeInstanceOf(SubController); expect(StoragesMock).toHaveBeenCalledWith(agile, { - localStorage: true, + localStorage: false, }); expect(agile.storages).toBeInstanceOf(Storages); - // Check if Static Logger has correct config - expect(Agile.logger.config).toStrictEqual({ - prefix: 'Agile', - level: Logger.level.WARN, - canUseCustomStyles: true, - timestamp: false, - }); - expect(Agile.logger.allowedTags).toStrictEqual([ - 'runtime', - 'storage', - 'subscription', - 'multieditor', - ]); - expect(Agile.logger.isActive).toBeTruthy(); - - // Check if global Agile Instance got created + // Check if Agile Instance got bound globally expect(globalThis[Agile.globalKey]).toBeUndefined(); }); - it('should instantiate Agile with (specific config)', () => { + it('should instantiate Agile (specific config)', () => { const agile = new Agile({ waitForMount: false, - localStorage: false, - logConfig: { - level: Logger.level.DEBUG, - active: false, - prefix: 'Jeff', - timestamp: true, - }, + bucket: false, + localStorage: true, bindGlobal: true, + key: 'jeff', + autoIntegrate: false, }); - // Check if Agile properties got instantiated properly expect(agile.config).toStrictEqual({ waitForMount: false, + bucket: false, + }); + expect(agile.key).toBe('jeff'); + expect(IntegrationsMock).toHaveBeenCalledWith(agile, { + autoIntegrate: false, }); - expect(IntegrationsMock).toHaveBeenCalledWith(agile); - expect(agile.integrations).toBeInstanceOf(Integrations); + // expect(agile.integrations).toBeInstanceOf(Integrations); // Because 'Integrations' is completely overwritten with a mock (mockImplementation) expect(RuntimeMock).toHaveBeenCalledWith(agile); - expect(agile.runtime).toBeInstanceOf(Runtime); + // expect(agile.runtime).toBeInstanceOf(Runtime); // Because 'Runtime' is completely overwritten with a mock (mockImplementation) expect(SubControllerMock).toHaveBeenCalledWith(agile); expect(agile.subController).toBeInstanceOf(SubController); expect(StoragesMock).toHaveBeenCalledWith(agile, { - localStorage: false, + localStorage: true, }); expect(agile.storages).toBeInstanceOf(Storages); - // Check if Static Logger has correct config - expect(Agile.logger.config).toStrictEqual({ - prefix: 'Jeff', - level: Logger.level.DEBUG, - canUseCustomStyles: true, - timestamp: true, - }); - expect(Agile.logger.allowedTags).toStrictEqual([ - 'runtime', - 'storage', - 'subscription', - 'multieditor', - ]); - expect(Agile.logger.isActive).toBeFalsy(); - - // Check if global Agile Instance got created + // Check if Agile Instance got bound globally expect(globalThis[Agile.globalKey]).toBe(agile); }); - it('should instantiate second Agile Instance and print warning if config.bindGlobal is set both times to true', () => { - const agile1 = new Agile({ - bindGlobal: true, - }); + it( + 'should instantiate second Agile Instance ' + + 'and print warning when an attempt is made to set the second Agile Instance globally ' + + 'although the previously defined Agile Instance is already globally set', + () => { + const agile1 = new Agile({ + bindGlobal: true, + }); - const agile2 = new Agile({ - bindGlobal: true, - }); + const agile2 = new Agile({ + bindGlobal: true, + }); - expect(globalThis[Agile.globalKey]).toBe(agile1); - LogMock.hasLoggedCode('10:02:00'); - }); + expect(agile1).toBeInstanceOf(Agile); + expect(agile2).toBeInstanceOf(Agile); + + expect(globalThis[Agile.globalKey]).toBe(agile1); + LogMock.hasLoggedCode('10:02:00'); + } + ); describe('Agile Function Tests', () => { let agile: Agile; beforeEach(() => { agile = new Agile(); - jest.clearAllMocks(); // Because creating Agile executes some mocks + jest.clearAllMocks(); // Because creating the Agile Instance calls some mocks }); describe('createStorage function tests', () => { - const StorageMock = Storage as jest.MockedClass; - beforeEach(() => { - StorageMock.mockClear(); + jest.spyOn(Shared, 'createStorage'); }); - it('should create Storage', () => { + it('should call createStorage', () => { const storageConfig = { prefix: 'test', methods: { @@ -190,31 +188,36 @@ describe('Agile Tests', () => { }, key: 'myTestStorage', }; - const storage = agile.createStorage(storageConfig); - expect(storage).toBeInstanceOf(Storage); - expect(StorageMock).toHaveBeenCalledWith(storageConfig); + const response = agile.createStorage(storageConfig); + + expect(response).toBeInstanceOf(Storage); + expect(Shared.createStorage).toHaveBeenCalledWith(storageConfig); }); }); - describe('state function tests', () => { - it('should create State', () => { - const state = agile.createState('testValue', { - key: 'myCoolState', - }); + 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(state).toBeInstanceOf(State); + expect(response).toBeInstanceOf(State); + expect(Shared.createState).toHaveBeenCalledWith('jeff', { + key: 'jeffState', + agileInstance: agile, + }); }); }); describe('createCollection function tests', () => { - const CollectionMock = Collection as jest.MockedClass; - beforeEach(() => { - CollectionMock.mockClear(); + jest.spyOn(Shared, 'createCollection'); }); - it('should create Collection', () => { + it('should call createCollection with the Agile Instance it was called on', () => { const collectionConfig = { selectors: ['test', 'test1'], groups: ['test2', 'test10'], @@ -222,48 +225,53 @@ describe('Agile Tests', () => { key: 'myCoolCollection', }; - const collection = agile.createCollection(collectionConfig); + const response = agile.createCollection(collectionConfig); - expect(collection).toBeInstanceOf(Collection); - expect(CollectionMock).toHaveBeenCalledWith(agile, collectionConfig); + expect(response).toBeInstanceOf(Collection); + expect(Shared.createCollection).toHaveBeenCalledWith( + collectionConfig, + agile + ); }); }); describe('createComputed function tests', () => { - const ComputedMock = Computed as jest.MockedClass; const computedFunction = () => { - // console.log("Hello Jeff"); + // empty }; beforeEach(() => { - ComputedMock.mockClear(); + jest.spyOn(Shared, 'createComputed'); }); - it('should create Computed', () => { - const computed = agile.createComputed(computedFunction, [ + it('should call createComputed with the Agile Instance it was called on (default config)', () => { + const response = agile.createComputed(computedFunction, [ 'dummyDep' as any, ]); - expect(computed).toBeInstanceOf(Computed); - expect(ComputedMock).toHaveBeenCalledWith(agile, computedFunction, { + expect(response).toBeInstanceOf(Computed); + expect(Shared.createComputed).toHaveBeenCalledWith(computedFunction, { computedDeps: ['dummyDep' as any], + agileInstance: agile, }); }); - it('should create Computed with config', () => { - const computed = agile.createComputed(computedFunction, { + 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, - }); + }; - expect(computed).toBeInstanceOf(Computed); - expect(ComputedMock).toHaveBeenCalledWith(agile, computedFunction, { - 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, + }, }); }); }); @@ -280,6 +288,10 @@ describe('Agile Tests', () => { }); describe('registerStorage function tests', () => { + beforeEach(() => { + agile.storages.register = jest.fn(); + }); + it('should register provided Storage', () => { const dummyStorage = new Storage({ prefix: 'test', @@ -317,6 +329,10 @@ describe('Agile Tests', () => { }); describe('hasStorage function tests', () => { + beforeEach(() => { + agile.storages.hasStorage = jest.fn(); + }); + it('should check if Agile has any registered Storage', () => { agile.hasStorage(); diff --git a/packages/core/tests/unit/collection/collection.persistent.test.ts b/packages/core/tests/unit/collection/collection.persistent.test.ts index 3ebbc97c..0c746a27 100644 --- a/packages/core/tests/unit/collection/collection.persistent.test.ts +++ b/packages/core/tests/unit/collection/collection.persistent.test.ts @@ -20,7 +20,6 @@ describe('CollectionPersistent Tests', () => { let dummyCollection: Collection; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); @@ -30,6 +29,8 @@ describe('CollectionPersistent Tests', () => { jest.spyOn(CollectionPersistent.prototype, 'instantiatePersistent'); jest.spyOn(CollectionPersistent.prototype, 'initialLoading'); + + jest.clearAllMocks(); }); it('should create CollectionPersistent and should call initialLoading if Persistent is ready (default config)', () => { diff --git a/packages/core/tests/unit/collection/collection.test.ts b/packages/core/tests/unit/collection/collection.test.ts index 47db19a4..e5b0e686 100644 --- a/packages/core/tests/unit/collection/collection.test.ts +++ b/packages/core/tests/unit/collection/collection.test.ts @@ -22,7 +22,6 @@ describe('Collection Tests', () => { let dummyAgile: Agile; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); @@ -30,6 +29,8 @@ describe('Collection Tests', () => { jest.spyOn(Collection.prototype, 'initSelectors'); jest.spyOn(Collection.prototype, 'initGroups'); jest.spyOn(Collection.prototype, 'collect'); + + jest.clearAllMocks(); }); it('should create Collection (default config)', () => { diff --git a/packages/core/tests/unit/collection/group/group.observer.test.ts b/packages/core/tests/unit/collection/group/group.observer.test.ts index f24e14d6..a295262e 100644 --- a/packages/core/tests/unit/collection/group/group.observer.test.ts +++ b/packages/core/tests/unit/collection/group/group.observer.test.ts @@ -24,7 +24,6 @@ describe('GroupObserver Tests', () => { let dummyItem2: Item; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); @@ -40,6 +39,8 @@ describe('GroupObserver Tests', () => { id: 'dummyItem2Key', name: 'jeff', }); + + jest.clearAllMocks(); }); it('should create Group Observer (default config)', () => { diff --git a/packages/core/tests/unit/collection/group/group.test.ts b/packages/core/tests/unit/collection/group/group.test.ts index 7aaceb4f..f9d48a74 100644 --- a/packages/core/tests/unit/collection/group/group.test.ts +++ b/packages/core/tests/unit/collection/group/group.test.ts @@ -21,7 +21,6 @@ describe('Group Tests', () => { let dummyCollection: Collection; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); @@ -31,6 +30,8 @@ describe('Group Tests', () => { jest.spyOn(Group.prototype, 'rebuild'); jest.spyOn(Group.prototype, 'addSideEffect'); + + jest.clearAllMocks(); }); it('should create Group with no initialItems (default config)', () => { diff --git a/packages/core/tests/unit/collection/item.test.ts b/packages/core/tests/unit/collection/item.test.ts index b50b620a..374352b8 100644 --- a/packages/core/tests/unit/collection/item.test.ts +++ b/packages/core/tests/unit/collection/item.test.ts @@ -18,13 +18,14 @@ describe('Item Tests', () => { let dummyCollection: Collection; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); dummyCollection = new Collection(dummyAgile); jest.spyOn(Item.prototype, 'addRebuildGroupThatIncludeItemKeySideEffect'); + + jest.clearAllMocks(); }); it('should create Item (default config)', () => { diff --git a/packages/core/tests/unit/collection/selector.test.ts b/packages/core/tests/unit/collection/selector.test.ts index 092a0d4c..11080f9f 100644 --- a/packages/core/tests/unit/collection/selector.test.ts +++ b/packages/core/tests/unit/collection/selector.test.ts @@ -11,13 +11,14 @@ describe('Selector Tests', () => { let dummyCollection: Collection; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); dummyCollection = new Collection(dummyAgile); jest.spyOn(Selector.prototype, 'select'); + + jest.clearAllMocks(); }); it('should create Selector and call initial select (default config)', () => { diff --git a/packages/core/tests/unit/computed/computed.test.ts b/packages/core/tests/unit/computed/computed.test.ts index 7f4c58fe..ef88decc 100644 --- a/packages/core/tests/unit/computed/computed.test.ts +++ b/packages/core/tests/unit/computed/computed.test.ts @@ -14,13 +14,14 @@ describe('Computed Tests', () => { let dummyAgile: Agile; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); jest.spyOn(Computed.prototype, 'recompute'); jest.spyOn(Utils, 'extractRelevantObservers'); + + jest.clearAllMocks(); }); it('should create Computed with a not async compute method (default config)', () => { diff --git a/packages/core/tests/unit/computed/computed.tracker.test.ts b/packages/core/tests/unit/computed/computed.tracker.test.ts index 81483956..20e09dc1 100644 --- a/packages/core/tests/unit/computed/computed.tracker.test.ts +++ b/packages/core/tests/unit/computed/computed.tracker.test.ts @@ -5,7 +5,6 @@ describe('ComputedTracker Tests', () => { let dummyAgile: Agile; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); @@ -13,6 +12,8 @@ describe('ComputedTracker Tests', () => { // Reset ComputedTracker (because it works static) ComputedTracker.isTracking = false; ComputedTracker.trackedObservers = new Set(); + + jest.clearAllMocks(); }); describe('ComputedTracker Function Tests', () => { diff --git a/packages/core/tests/unit/integrations/integration.test.ts b/packages/core/tests/unit/integrations/integration.test.ts index f9cac640..a73cc5c9 100644 --- a/packages/core/tests/unit/integrations/integration.test.ts +++ b/packages/core/tests/unit/integrations/integration.test.ts @@ -3,8 +3,8 @@ import { LogMock } from '../../helper/logMock'; describe('Integration Tests', () => { beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); + jest.clearAllMocks(); }); it('should create Integration', () => { diff --git a/packages/core/tests/unit/integrations/integrations.test.ts b/packages/core/tests/unit/integrations/integrations.test.ts index d4baf660..a7598d61 100644 --- a/packages/core/tests/unit/integrations/integrations.test.ts +++ b/packages/core/tests/unit/integrations/integrations.test.ts @@ -3,56 +3,124 @@ import { LogMock } from '../../helper/logMock'; describe('Integrations Tests', () => { let dummyAgile: Agile; + let dummyIntegration1: Integration; + let dummyIntegration2: Integration; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); - Agile.initialIntegrations = []; + dummyIntegration1 = new Integration({ + key: 'dummyIntegration1', + }); + dummyIntegration2 = new Integration({ + key: 'dummyIntegration2', + }); - jest.spyOn(Integrations.prototype, 'integrate'); - }); + Integrations.initialIntegrations = []; - it('should create Integrations', () => { - const integrations = new Integrations(dummyAgile); + jest.spyOn(Integrations.prototype, 'integrate'); + jest.spyOn(Integrations, 'onRegisterInitialIntegration'); - expect(integrations.integrations.size).toBe(0); + jest.clearAllMocks(); }); - it('should create Integrations and integrate Agile initialIntegrations', async () => { - const dummyIntegration1 = new Integration({ - key: 'initialIntegration1', - }); - const dummyIntegration2 = new Integration({ - key: 'initialIntegration2', - }); - Agile.initialIntegrations.push(dummyIntegration1); - Agile.initialIntegrations.push(dummyIntegration2); + it('should create Integrations with the before specified initial Integrations (default config)', () => { + Integrations.initialIntegrations = [dummyIntegration1, dummyIntegration2]; const integrations = new Integrations(dummyAgile); - expect(integrations.integrations.size).toBe(2); - expect(integrations.integrations.has(dummyIntegration1)).toBeTruthy(); - expect(integrations.integrations.has(dummyIntegration2)).toBeTruthy(); + expect(Array.from(integrations.integrations)).toStrictEqual([ + dummyIntegration1, + dummyIntegration2, + ]); + expect(Integrations.onRegisterInitialIntegration).toHaveBeenCalledWith( + expect.any(Function) + ); + expect(integrations.integrate).toHaveBeenCalledTimes(2); expect(integrations.integrate).toHaveBeenCalledWith(dummyIntegration1); expect(integrations.integrate).toHaveBeenCalledWith(dummyIntegration2); }); + it('should create Integrations without the before specified initial Integrations (autoIntegrate = false)', () => { + Integrations.initialIntegrations = [dummyIntegration1, dummyIntegration2]; + + const integrations = new Integrations(dummyAgile, { autoIntegrate: false }); + + expect(Array.from(integrations.integrations)).toStrictEqual([]); + + expect(Integrations.onRegisterInitialIntegration).not.toHaveBeenCalled(); + expect(integrations.integrate).not.toHaveBeenCalled(); + }); + describe('Integrations Function Tests', () => { let integrations: Integrations; - let dummyIntegration1: Integration; - let dummyIntegration2: Integration; beforeEach(() => { integrations = new Integrations(dummyAgile); - dummyIntegration1 = new Integration({ - key: 'dummyIntegration1', + }); + + describe('onRegisterInitialIntegration function tests', () => { + let callback; + beforeEach(() => { + callback = jest.fn(); }); - dummyIntegration2 = new Integration({ - key: 'dummyIntegration2', + + it( + 'should register specified onRegisterInitialIntegration callback ' + + 'and call it for each tracked initial Integrations', + () => { + Integrations.initialIntegrations = [ + dummyIntegration1, + dummyIntegration2, + ]; + + Integrations.onRegisterInitialIntegration(callback); + + expect(callback).toHaveBeenCalledTimes(2); + expect(callback).toHaveBeenCalledWith(dummyIntegration1); + expect(callback).toHaveBeenCalledWith(dummyIntegration2); + } + ); + }); + + describe('addInitialIntegration function tests', () => { + const callback1 = jest.fn(); + const callback2 = jest.fn(); + + beforeEach(() => { + Integrations.onRegisterInitialIntegration(callback1); + Integrations.onRegisterInitialIntegration(callback2); }); + + it( + 'should add valid Integration to the initialIntegrations array ' + + 'and fire the onRegisterInitialIntegration callbacks', + () => { + Integrations.addInitialIntegration(dummyIntegration1); + + expect(callback1).toHaveBeenCalledTimes(1); + expect(callback1).toHaveBeenCalledWith(dummyIntegration1); + expect(callback2).toHaveBeenCalledTimes(1); + expect(callback2).toHaveBeenCalledWith(dummyIntegration1); + expect(Integrations.initialIntegrations).toStrictEqual([ + dummyIntegration1, + ]); + } + ); + + it( + "shouldn't add invalid Integration to the initialIntegrations array " + + "and shouldn't fire the onRegisterInitialIntegration callbacks", + () => { + Integrations.addInitialIntegration(undefined as any); + + expect(callback1).not.toHaveBeenCalled(); + expect(callback2).not.toHaveBeenCalled(); + expect(Integrations.initialIntegrations).toStrictEqual([]); + } + ); }); describe('integrate function tests', () => { @@ -101,7 +169,7 @@ describe('Integrations Tests', () => { LogMock.hasLoggedCode( '18:03:00', - [dummyIntegration1._key], + [dummyIntegration1._key, dummyAgile.key], dummyIntegration1 ); }); diff --git a/packages/core/tests/unit/runtime/observer.test.ts b/packages/core/tests/unit/runtime/observer.test.ts index 59889d40..5810c138 100644 --- a/packages/core/tests/unit/runtime/observer.test.ts +++ b/packages/core/tests/unit/runtime/observer.test.ts @@ -15,7 +15,6 @@ describe('Observer Tests', () => { let dummySubscription2: SubscriptionContainer; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile(); @@ -26,6 +25,8 @@ describe('Observer Tests', () => { jest.spyOn(dummySubscription1, 'addSubscription'); jest.spyOn(dummySubscription2, 'addSubscription'); + + jest.clearAllMocks(); }); it('should create Observer (default config)', () => { diff --git a/packages/core/tests/unit/runtime/runtime.job.test.ts b/packages/core/tests/unit/runtime/runtime.job.test.ts index 4793df21..b01c3765 100644 --- a/packages/core/tests/unit/runtime/runtime.job.test.ts +++ b/packages/core/tests/unit/runtime/runtime.job.test.ts @@ -7,7 +7,6 @@ describe('RuntimeJob Tests', () => { let dummyObserver: Observer; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); @@ -15,6 +14,8 @@ describe('RuntimeJob Tests', () => { key: 'myIntegration', }); dummyObserver = new Observer(dummyAgile); + + jest.clearAllMocks(); }); it( diff --git a/packages/core/tests/unit/runtime/runtime.test.ts b/packages/core/tests/unit/runtime/runtime.test.ts index 2906607d..5bcc3014 100644 --- a/packages/core/tests/unit/runtime/runtime.test.ts +++ b/packages/core/tests/unit/runtime/runtime.test.ts @@ -9,15 +9,17 @@ import { } from '../../../src'; import * as Utils from '@agile-ts/utils'; import { LogMock } from '../../helper/logMock'; +import waitForExpect from 'wait-for-expect'; describe('Runtime Tests', () => { let dummyAgile: Agile; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); + + jest.clearAllMocks(); }); it('should create Runtime', () => { @@ -29,6 +31,7 @@ describe('Runtime Tests', () => { expect(runtime.jobsToRerender).toStrictEqual([]); expect(Array.from(runtime.notReadyJobsToRerender)).toStrictEqual([]); expect(runtime.isPerformingJobs).toBeFalsy(); + expect(runtime.bucketTimeout).toBeNull(); }); describe('Runtime Function Tests', () => { @@ -110,8 +113,11 @@ describe('Runtime Tests', () => { it( "should perform specified Job and all remaining Jobs in the 'jobQueue' " + - "and call 'updateSubscribers' if at least one performed Job needs to rerender", + "and call 'updateSubscribers' in a setTimeout (bucket) " + + 'if at least one performed Job needs to rerender (config.bucket = true)', async () => { + runtime.agileInstance().config.bucket = true; + runtime.bucketTimeout = null; runtime.jobQueue.push(dummyJob2); runtime.jobQueue.push(dummyJob3); @@ -127,14 +133,51 @@ describe('Runtime Tests', () => { expect(runtime.isPerformingJobs).toBeFalsy(); // because Jobs were performed expect(runtime.jobQueue).toStrictEqual([]); expect(runtime.jobsToRerender).toStrictEqual([dummyJob1, dummyJob2]); + expect(runtime.bucketTimeout).not.toBeNull(); + + // Because 'updateSubscribers' is called in a timeout + await waitForExpect(() => { + expect(runtime.updateSubscribers).toHaveBeenCalledTimes(1); + expect(runtime.bucketTimeout).toBeNull(); + }); + } + ); + + it( + "should perform specified Job and all remaining Jobs in the 'jobQueue' " + + "and call 'updateSubscribers' " + + 'if at least one performed Job needs to rerender (config.bucket = false)', + async () => { + runtime.agileInstance().config.bucket = false; + runtime.bucketTimeout = null; + runtime.jobQueue.push(dummyJob2); + runtime.jobQueue.push(dummyJob3); + + runtime.perform(dummyJob1); - // Sleep 5ms because updateSubscribers is called in a timeout - await new Promise((resolve) => setTimeout(resolve, 5)); + expect(runtime.bucketTimeout).toBeNull(); expect(runtime.updateSubscribers).toHaveBeenCalledTimes(1); } ); + it( + "should perform specified Job and all remaining Jobs in the 'jobQueue' " + + "and shouldn't call 'updateSubscribers' although at least one performed Job needs to rerender" + + 'if a bucket timeout is already active (config.bucket = true)', + async () => { + runtime.agileInstance().config.bucket = true; + runtime.bucketTimeout = 'notNull' as any; + runtime.jobQueue.push(dummyJob2); + runtime.jobQueue.push(dummyJob3); + + runtime.perform(dummyJob1); + + expect(runtime.bucketTimeout).toBe('notNull'); + expect(runtime.updateSubscribers).not.toHaveBeenCalled(); + } + ); + it('should perform specified Job and ingest its dependents into the runtime', async () => { dummyJob1.observer.dependents.add(dummyObserver2); dummyJob1.observer.dependents.add(dummyObserver1); @@ -150,7 +193,8 @@ describe('Runtime Tests', () => { it( "should perform specified Job and all remaining Jobs in the 'jobQueue' " + - "and shouldn't call 'updateSubscribes' if no performed Job needs to rerender", + "and shouldn't call 'updateSubscribes' " + + 'if no performed Job needs to rerender', async () => { dummyJob1.rerender = false; runtime.jobQueue.push(dummyJob3); @@ -165,9 +209,7 @@ describe('Runtime Tests', () => { expect(runtime.isPerformingJobs).toBeFalsy(); // because Jobs were performed expect(runtime.jobQueue).toStrictEqual([]); expect(runtime.jobsToRerender).toStrictEqual([]); - - // Sleep 5ms because updateSubscribers is called in a timeout - await new Promise((resolve) => setTimeout(resolve, 5)); + expect(runtime.bucketTimeout).toBeNull(); expect(runtime.updateSubscribers).not.toHaveBeenCalled(); } diff --git a/packages/core/tests/unit/runtime/subscription/container/CallbackSubscriptionContainer.test.ts b/packages/core/tests/unit/runtime/subscription/container/CallbackSubscriptionContainer.test.ts index e8bea19e..7f342dc9 100644 --- a/packages/core/tests/unit/runtime/subscription/container/CallbackSubscriptionContainer.test.ts +++ b/packages/core/tests/unit/runtime/subscription/container/CallbackSubscriptionContainer.test.ts @@ -15,7 +15,6 @@ describe('CallbackSubscriptionContainer Tests', () => { let dummyProxyWeakMap: ProxyWeakMapType; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile(); @@ -23,6 +22,8 @@ describe('CallbackSubscriptionContainer Tests', () => { dummyObserver2 = new Observer(dummyAgile, { key: 'dummyObserver2' }); dummySelectorWeakMap = new WeakMap(); dummyProxyWeakMap = new WeakMap(); + + jest.clearAllMocks(); }); it('should create CallbackSubscriptionContainer', () => { diff --git a/packages/core/tests/unit/runtime/subscription/container/ComponentSubscriptionContainer.test.ts b/packages/core/tests/unit/runtime/subscription/container/ComponentSubscriptionContainer.test.ts index 4401cf88..9efe4262 100644 --- a/packages/core/tests/unit/runtime/subscription/container/ComponentSubscriptionContainer.test.ts +++ b/packages/core/tests/unit/runtime/subscription/container/ComponentSubscriptionContainer.test.ts @@ -15,7 +15,6 @@ describe('ComponentSubscriptionContainer Tests', () => { let dummyProxyWeakMap: ProxyWeakMapType; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile(); @@ -23,6 +22,8 @@ describe('ComponentSubscriptionContainer Tests', () => { dummyObserver2 = new Observer(dummyAgile, { key: 'dummyObserver2' }); dummySelectorWeakMap = new WeakMap(); dummyProxyWeakMap = new WeakMap(); + + jest.clearAllMocks(); }); it('should create ComponentSubscriptionContainer', () => { diff --git a/packages/core/tests/unit/runtime/subscription/container/SubscriptionContainer.test.ts b/packages/core/tests/unit/runtime/subscription/container/SubscriptionContainer.test.ts index c3aa012d..ce983e95 100644 --- a/packages/core/tests/unit/runtime/subscription/container/SubscriptionContainer.test.ts +++ b/packages/core/tests/unit/runtime/subscription/container/SubscriptionContainer.test.ts @@ -16,7 +16,6 @@ describe('SubscriptionContainer Tests', () => { let dummyProxyWeakMap: ProxyWeakMapType; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile(); @@ -24,6 +23,8 @@ describe('SubscriptionContainer Tests', () => { dummyObserver2 = new Observer(dummyAgile, { key: 'dummyObserver2' }); dummySelectorWeakMap = new WeakMap(); dummyProxyWeakMap = new WeakMap(); + + jest.clearAllMocks(); }); it('should create SubscriptionContainer with passed subs array (default config)', () => { diff --git a/packages/core/tests/unit/runtime/subscription/sub.controller.test.ts b/packages/core/tests/unit/runtime/subscription/sub.controller.test.ts index e52e81a1..6d73d851 100644 --- a/packages/core/tests/unit/runtime/subscription/sub.controller.test.ts +++ b/packages/core/tests/unit/runtime/subscription/sub.controller.test.ts @@ -12,10 +12,11 @@ describe('SubController Tests', () => { let dummyAgile: Agile; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); + + jest.clearAllMocks(); }); it('should create SubController', () => { diff --git a/packages/core/tests/unit/shared.test.ts b/packages/core/tests/unit/shared.test.ts new file mode 100644 index 00000000..fc81e6e8 --- /dev/null +++ b/packages/core/tests/unit/shared.test.ts @@ -0,0 +1,212 @@ +import { + Agile, + Collection, + Computed, + shared, + State, + Storage, + createStorage, + createState, + createCollection, + createComputed, + assignSharedAgileInstance, +} from '../../src'; +import { LogMock } from '../helper/logMock'; + +jest.mock('../../src/storages/storage'); +jest.mock('../../src/collection'); +jest.mock('../../src/computed'); + +// https://github.com/facebook/jest/issues/5023 +jest.mock('../../src/state', () => { + return { + State: jest.fn(), + }; +}); + +describe('Shared Tests', () => { + let sharedAgileInstance: Agile; + + beforeEach(() => { + LogMock.mockLogs(); + + sharedAgileInstance = new Agile(); + assignSharedAgileInstance(sharedAgileInstance); + + jest.clearAllMocks(); + }); + + describe('assignSharedAgileInstance function tests', () => { + it('should assign the specified Agile Instance as new shared Agile Instance', () => { + const newAgileInstance = new Agile({ key: 'notShared' }); + + assignSharedAgileInstance(newAgileInstance); + + expect(shared).toBe(newAgileInstance); + }); + }); + + describe('createStorage function tests', () => { + const StorageMock = Storage as jest.MockedClass; + + beforeEach(() => { + StorageMock.mockClear(); + }); + + it('should create Storage', () => { + const storageConfig = { + prefix: 'test', + methods: { + get: () => { + /* empty function */ + }, + set: () => { + /* empty function */ + }, + remove: () => { + /* empty function */ + }, + }, + key: 'myTestStorage', + }; + + const storage = createStorage(storageConfig); + + expect(storage).toBeInstanceOf(Storage); + expect(StorageMock).toHaveBeenCalledWith(storageConfig); + }); + }); + + describe('createState function tests', () => { + const StateMock = State as jest.MockedClass; + + it('should create State with the shared Agile Instance', () => { + const state = createState('testValue', { + key: 'myCoolState', + }); + + expect(state).toBeInstanceOf(State); + expect(StateMock).toHaveBeenCalledWith(sharedAgileInstance, 'testValue', { + key: 'myCoolState', + }); + }); + + it('should create State with a specified Agile Instance', () => { + const agile = new Agile(); + + const state = createState('testValue', { + key: 'myCoolState', + agileInstance: agile, + }); + + expect(state).toBeInstanceOf(State); + expect(StateMock).toHaveBeenCalledWith(agile, 'testValue', { + key: 'myCoolState', + }); + }); + }); + + describe('createCollection function tests', () => { + const CollectionMock = Collection as jest.MockedClass; + + beforeEach(() => { + CollectionMock.mockClear(); + }); + + it('should create Collection with the shared Agile Instance', () => { + const collectionConfig = { + selectors: ['test', 'test1'], + groups: ['test2', 'test10'], + defaultGroupKey: 'frank', + key: 'myCoolCollection', + }; + + const collection = createCollection(collectionConfig); + + expect(collection).toBeInstanceOf(Collection); + expect(CollectionMock).toHaveBeenCalledWith( + sharedAgileInstance, + collectionConfig + ); + }); + + it('should create Collection with a specified Agile Instance', () => { + const agile = new Agile(); + const collectionConfig = { + selectors: ['test', 'test1'], + groups: ['test2', 'test10'], + defaultGroupKey: 'frank', + key: 'myCoolCollection', + }; + + const collection = createCollection(collectionConfig, agile); + + expect(collection).toBeInstanceOf(Collection); + expect(CollectionMock).toHaveBeenCalledWith(agile, collectionConfig); + }); + }); + + describe('createComputed function tests', () => { + const ComputedMock = Computed as jest.MockedClass; + const computedFunction = () => { + // empty + }; + + beforeEach(() => { + ComputedMock.mockClear(); + }); + + it('should create Computed with the shared Agile Instance (default config)', () => { + const response = createComputed(computedFunction, ['dummyDep' as any]); + + expect(response).toBeInstanceOf(Computed); + expect(ComputedMock).toHaveBeenCalledWith( + sharedAgileInstance, + computedFunction, + { + computedDeps: ['dummyDep' as any], + } + ); + }); + + it('should create Computed with the shared Agile Instance (specific config)', () => { + const computedConfig = { + key: 'jeff', + isPlaceholder: false, + computedDeps: ['dummyDep' as any], + autodetect: true, + }; + + const response = createComputed(computedFunction, computedConfig); + + expect(response).toBeInstanceOf(Computed); + expect(ComputedMock).toHaveBeenCalledWith( + sharedAgileInstance, + computedFunction, + computedConfig + ); + }); + + it('should create Computed with a specified Agile Instance (specific config)', () => { + const agile = new Agile(); + const computedConfig = { + key: 'jeff', + isPlaceholder: false, + computedDeps: ['dummyDep' as any], + autodetect: true, + }; + + const response = createComputed(computedFunction, { + ...computedConfig, + ...{ agileInstance: agile }, + }); + + expect(response).toBeInstanceOf(Computed); + expect(ComputedMock).toHaveBeenCalledWith( + agile, + computedFunction, + computedConfig + ); + }); + }); +}); diff --git a/packages/core/tests/unit/state/state.observer.test.ts b/packages/core/tests/unit/state/state.observer.test.ts index 32a99109..035ec4a5 100644 --- a/packages/core/tests/unit/state/state.observer.test.ts +++ b/packages/core/tests/unit/state/state.observer.test.ts @@ -17,11 +17,12 @@ describe('StateObserver Tests', () => { let dummyState: State; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); dummyState = new State(dummyAgile, 'dummyValue', { key: 'dummyState' }); + + jest.clearAllMocks(); }); it('should create State Observer (default config)', () => { diff --git a/packages/core/tests/unit/state/state.persistent.test.ts b/packages/core/tests/unit/state/state.persistent.test.ts index 1fc7d061..65603523 100644 --- a/packages/core/tests/unit/state/state.persistent.test.ts +++ b/packages/core/tests/unit/state/state.persistent.test.ts @@ -12,7 +12,6 @@ describe('StatePersistent Tests', () => { let dummyState: State; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); @@ -20,6 +19,8 @@ describe('StatePersistent Tests', () => { jest.spyOn(StatePersistent.prototype, 'instantiatePersistent'); jest.spyOn(StatePersistent.prototype, 'initialLoading'); + + jest.clearAllMocks(); }); it("should create StatePersistent and shouldn't call initialLoading if Persistent isn't ready (default config)", () => { diff --git a/packages/core/tests/unit/state/state.runtime.job.test.ts b/packages/core/tests/unit/state/state.runtime.job.test.ts index 58906f56..85df6e56 100644 --- a/packages/core/tests/unit/state/state.runtime.job.test.ts +++ b/packages/core/tests/unit/state/state.runtime.job.test.ts @@ -16,7 +16,6 @@ describe('RuntimeJob Tests', () => { let dummyObserver: StateObserver; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); @@ -25,6 +24,8 @@ describe('RuntimeJob Tests', () => { }); dummyState = new State(dummyAgile, 'dummyValue'); dummyObserver = new StateObserver(dummyState); + + jest.clearAllMocks(); }); it( diff --git a/packages/core/tests/unit/state/state.test.ts b/packages/core/tests/unit/state/state.test.ts index 3bfe253f..78b3afe9 100644 --- a/packages/core/tests/unit/state/state.test.ts +++ b/packages/core/tests/unit/state/state.test.ts @@ -15,12 +15,13 @@ describe('State Tests', () => { let dummyAgile: Agile; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); jest.spyOn(State.prototype, 'set'); + + jest.clearAllMocks(); }); it('should create State and should call initial set (default config)', () => { diff --git a/packages/core/tests/unit/storages/persistent.test.ts b/packages/core/tests/unit/storages/persistent.test.ts index 79ca7ca2..b038fff3 100644 --- a/packages/core/tests/unit/storages/persistent.test.ts +++ b/packages/core/tests/unit/storages/persistent.test.ts @@ -5,12 +5,13 @@ describe('Persistent Tests', () => { let dummyAgile: Agile; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); jest.spyOn(Persistent.prototype, 'instantiatePersistent'); + + jest.clearAllMocks(); }); it('should create Persistent (default config)', () => { diff --git a/packages/core/tests/unit/storages/storage.test.ts b/packages/core/tests/unit/storages/storage.test.ts index 1d1442c8..bfe220c0 100644 --- a/packages/core/tests/unit/storages/storage.test.ts +++ b/packages/core/tests/unit/storages/storage.test.ts @@ -5,7 +5,6 @@ describe('Storage Tests', () => { let dummyStorageMethods; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyStorageMethods = { @@ -16,6 +15,8 @@ describe('Storage Tests', () => { // https://codewithhugo.com/jest-stub-mock-spy-set-clear/ jest.spyOn(Storage.prototype, 'validate'); + + jest.clearAllMocks(); }); it('should create not async Storage with normal Storage Methods (default config)', () => { diff --git a/packages/core/tests/unit/storages/storages.test.ts b/packages/core/tests/unit/storages/storages.test.ts index efdb57a3..139a7e29 100644 --- a/packages/core/tests/unit/storages/storages.test.ts +++ b/packages/core/tests/unit/storages/storages.test.ts @@ -5,12 +5,13 @@ describe('Storages Tests', () => { let dummyAgile; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); jest.spyOn(Storages.prototype, 'instantiateLocalStorage'); + + jest.clearAllMocks(); }); it('should create Storages (default config)', () => { diff --git a/packages/core/tests/unit/utils.test.ts b/packages/core/tests/unit/utils.test.ts index db4bd7e3..96decb42 100644 --- a/packages/core/tests/unit/utils.test.ts +++ b/packages/core/tests/unit/utils.test.ts @@ -1,12 +1,11 @@ import { - globalBind, - getAgileInstance, Agile, State, Observer, Collection, StateObserver, GroupObserver, + assignSharedAgileInstance, } from '../../src'; import * as Utils from '../../src/utils'; import { LogMock } from '../helper/logMock'; @@ -15,47 +14,68 @@ describe('Utils Tests', () => { let dummyAgile: Agile; beforeEach(() => { - jest.clearAllMocks(); LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); // @ts-ignore | Reset globalThis globalThis = {}; + + jest.clearAllMocks(); }); describe('getAgileInstance function tests', () => { beforeEach(() => { + assignSharedAgileInstance(dummyAgile); globalThis[Agile.globalKey] = dummyAgile; }); - it('should get agileInstance from State', () => { + it('should return Agile Instance from State', () => { const dummyState = new State(dummyAgile, 'dummyValue'); - expect(getAgileInstance(dummyState)).toBe(dummyAgile); + expect(Utils.getAgileInstance(dummyState)).toBe(dummyAgile); }); - it('should get agileInstance from Collection', () => { + it('should return Agile Instance from Collection', () => { const dummyCollection = new Collection(dummyAgile); - expect(getAgileInstance(dummyCollection)).toBe(dummyAgile); + expect(Utils.getAgileInstance(dummyCollection)).toBe(dummyAgile); }); - it('should get agileInstance from Observer', () => { + it('should return Agile Instance from Observer', () => { const dummyObserver = new Observer(dummyAgile); - expect(getAgileInstance(dummyObserver)).toBe(dummyAgile); + expect(Utils.getAgileInstance(dummyObserver)).toBe(dummyAgile); }); - it('should get agileInstance from globalThis if passed instance holds no agileInstance', () => { - expect(getAgileInstance('weiredInstance')).toBe(dummyAgile); - }); - - it('should print error if something went wrong', () => { + it( + 'should return shared Agile Instance ' + + 'if specified Instance contains no valid Agile Instance', + () => { + expect(Utils.getAgileInstance('weiredInstance')).toBe(dummyAgile); + } + ); + + it( + 'should return globally bound Agile Instance' + + 'if specified Instance contains no valid Agile Instance' + + 'and no shared Agile Instance is specified', + () => { + // Destroy shared Agile Instance + assignSharedAgileInstance(undefined as any); + + expect(Utils.getAgileInstance('weiredInstance')).toBe(dummyAgile); + } + ); + + it('should print error if no Agile Instance could be retrieved', () => { // @ts-ignore | Destroy globalThis globalThis = undefined; - const response = getAgileInstance('weiredInstance'); + // Destroy shared Agile Instance + assignSharedAgileInstance(undefined as any); + + const response = Utils.getAgileInstance('weiredInstance'); expect(response).toBeUndefined(); LogMock.hasLoggedCode('20:03:00', [], 'weiredInstance'); @@ -360,23 +380,23 @@ describe('Utils Tests', () => { }); it('should bind Instance globally at the specified key (default config)', () => { - globalBind(dummyKey, 'dummyInstance'); + Utils.globalBind(dummyKey, 'dummyInstance'); expect(globalThis[dummyKey]).toBe('dummyInstance'); }); it("shouldn't overwrite already globally bound Instance at the same key (default config)", () => { - globalBind(dummyKey, 'I am first!'); + Utils.globalBind(dummyKey, 'I am first!'); - globalBind(dummyKey, 'dummyInstance'); + Utils.globalBind(dummyKey, 'dummyInstance'); expect(globalThis[dummyKey]).toBe('I am first!'); }); it('should overwrite already globally bound Instance at the same key (overwrite = true)', () => { - globalBind(dummyKey, 'I am first!'); + Utils.globalBind(dummyKey, 'I am first!'); - globalBind(dummyKey, 'dummyInstance', true); + Utils.globalBind(dummyKey, 'dummyInstance', true); expect(globalThis[dummyKey]).toBe('dummyInstance'); }); @@ -385,9 +405,27 @@ describe('Utils Tests', () => { // @ts-ignore | Destroy globalThis globalThis = undefined; - globalBind(dummyKey, 'dummyInstance'); + Utils.globalBind(dummyKey, 'dummyInstance'); LogMock.hasLoggedCode('20:03:01', [dummyKey]); }); }); + + describe('runsOnServer function tests', () => { + it("should return 'false' if the current environment isn't a server", () => { + global.window = { + document: { + createElement: 'isSet' as any, + } as any, + } as any; + + expect(Utils.runsOnServer()).toBeFalsy(); + }); + + it("should return 'true' if the current environment is a server", () => { + global.window = undefined as any; + + expect(Utils.runsOnServer()).toBeTruthy(); + }); + }); }); diff --git a/packages/cra-template-agile-typescript/README.md b/packages/cra-template-agile-typescript/README.md index 7ff1b678..4b973574 100644 --- a/packages/cra-template-agile-typescript/README.md +++ b/packages/cra-template-agile-typescript/README.md @@ -24,4 +24,4 @@ npx create-react-app my-app --template agile-typescript ## Credits -https://github.com/facebook/create-react-app/blob/master/packages/cra-template-typescript \ No newline at end of file +https://github.com/facebook/create-react-app/blob/master/packages/cra-template-typescript diff --git a/packages/event/README.md b/packages/event/README.md index 18503089..5b52052c 100644 --- a/packages/event/README.md +++ b/packages/event/README.md @@ -16,8 +16,7 @@ ## ⏰ Short Example ```ts -const App = new Agile(); -const MY_EVENT = new Event(App); +const MY_EVENT = createEvent(); MY_EVENT.on((data) => {console.log("hello there " + data.name)}); // Print 'hello there jeff' if Event gets triggered MY_EVENT.trigger({name: "jeff"}); // Trigger Event ``` @@ -39,4 +38,4 @@ _Other Versions aren't supported anymore_ ## 📄 Documentation -The Agile Event Docs are located [here](https://agile-ts.org/docs/) \ No newline at end of file +The Agile Event Docs are located [here](https://agile-ts.org/docs/) diff --git a/packages/event/src/event.ts b/packages/event/src/event.ts index 3e4a1c55..8291b1a6 100644 --- a/packages/event/src/event.ts +++ b/packages/event/src/event.ts @@ -1,8 +1,8 @@ import { Agile, - defineConfig, generateId, isFunction, + LogCodeManager, Observer, } from '@agile-ts/core'; import { EventObserver, EventJob } from './internal'; @@ -33,14 +33,15 @@ export class Event { */ constructor(agileInstance: Agile, config: CreateEventConfigInterface = {}) { this.agileInstance = () => agileInstance; - config = defineConfig(config, { + config = { enabled: true, rerender: false, maxUses: undefined, delay: undefined, overlap: false, dependents: [], - }); + ...config, + }; this._key = config.key; this.observer = new EventObserver(this, { key: config.key, @@ -122,7 +123,7 @@ export class Event { // Check if Callback is a Function if (!isFunction(_callback)) { - Agile.logger.error( + LogCodeManager.getLogger()?.error( 'A Event Callback Function has to be typeof Function!' ); return this; @@ -130,7 +131,7 @@ export class Event { // Check if Callback Function already exists if (this.callbacks[key]) { - Agile.logger.error( + LogCodeManager.getLogger()?.error( `Event Callback Function with the key/name '${key}' already exists!` ); return this; diff --git a/packages/event/src/hooks/useEvent.ts b/packages/event/src/hooks/useEvent.ts index 905d6413..5a1d3168 100644 --- a/packages/event/src/hooks/useEvent.ts +++ b/packages/event/src/hooks/useEvent.ts @@ -2,6 +2,7 @@ import React from 'react'; import { Agile, getAgileInstance, + LogCodeManager, SubscriptionContainerKeyType, } from '@agile-ts/core'; import { Event, EventCallbackFunction } from '../internal'; @@ -22,7 +23,10 @@ export function useEvent>( // Get Agile Instance if (!agileInstance) agileInstance = getAgileInstance(event); if (!agileInstance || !agileInstance.subController) { - Agile.logger.error('Failed to subscribe Component with deps', event); + LogCodeManager.getLogger()?.error( + 'Failed to subscribe Component with deps', + event + ); return; } diff --git a/packages/event/src/internal.ts b/packages/event/src/internal.ts index e63a1ffb..695b0fbb 100644 --- a/packages/event/src/internal.ts +++ b/packages/event/src/internal.ts @@ -4,6 +4,10 @@ // !! All internal Agile Editor modules must be imported from here!! +// Event export * from './event.job'; export * from './event.observer'; export * from './event'; + +// Shared +export * from './shared'; diff --git a/packages/event/src/shared.ts b/packages/event/src/shared.ts new file mode 100644 index 00000000..7aadc8a8 --- /dev/null +++ b/packages/event/src/shared.ts @@ -0,0 +1,27 @@ +import { + CreateAgileSubInstanceInterface, + removeProperties, + shared, +} from '@agile-ts/core'; +import { + Event, + CreateEventConfigInterface, + DefaultEventPayload, +} from './internal'; + +export function createEvent( + config: CreateEventConfigInterfaceWithAgile = {} +): Event { + config = { + agileInstance: shared, + ...config, + }; + return new Event( + config.agileInstance as any, + removeProperties(config, ['agileInstance']) + ); +} + +export interface CreateEventConfigInterfaceWithAgile + extends CreateEventConfigInterface, + CreateAgileSubInstanceInterface {} diff --git a/packages/event/tests/unit/event.job.test.ts b/packages/event/tests/unit/event.job.test.ts index 7c475015..90397943 100644 --- a/packages/event/tests/unit/event.job.test.ts +++ b/packages/event/tests/unit/event.job.test.ts @@ -1,10 +1,10 @@ import { EventJob } from '../../src'; -import mockConsole from 'jest-mock-console'; +import { LogMock } from '../../../core/tests/helper/logMock'; describe('EventJob Tests', () => { beforeEach(() => { + LogMock.mockLogs(); jest.clearAllMocks(); - mockConsole(['error', 'warn']); }); it('should create EventJob (without keys)', () => { diff --git a/packages/event/tests/unit/event.observer.test.ts b/packages/event/tests/unit/event.observer.test.ts index 221bd2b9..b700a72f 100644 --- a/packages/event/tests/unit/event.observer.test.ts +++ b/packages/event/tests/unit/event.observer.test.ts @@ -1,17 +1,18 @@ import { EventObserver, Event } from '../../src'; import { Agile, Observer, SubscriptionContainer } from '@agile-ts/core'; -import mockConsole from 'jest-mock-console'; +import { LogMock } from '../../../core/tests/helper/logMock'; describe('EventObserver Tests', () => { let dummyAgile: Agile; let dummyEvent: Event; beforeEach(() => { - jest.clearAllMocks(); - mockConsole(['error', 'warn']); + LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); dummyEvent = new Event(dummyAgile); + + jest.clearAllMocks(); }); it('should create EventObserver (default config)', () => { diff --git a/packages/event/tests/unit/event.test.ts b/packages/event/tests/unit/event.test.ts index f2bdd61f..bd369952 100644 --- a/packages/event/tests/unit/event.test.ts +++ b/packages/event/tests/unit/event.test.ts @@ -1,16 +1,17 @@ import { Event, EventObserver } from '../../src'; import { Agile, Observer } from '@agile-ts/core'; import * as Utils from '@agile-ts/utils'; -import mockConsole from 'jest-mock-console'; +import { LogMock } from '../../../core/tests/helper/logMock'; describe('Event Tests', () => { let dummyAgile: Agile; beforeEach(() => { - jest.clearAllMocks(); - mockConsole(['error', 'warn']); + LogMock.mockLogs(); dummyAgile = new Agile({ localStorage: false }); + + jest.clearAllMocks(); }); it('should create Event (default config)', () => { diff --git a/packages/event/tests/unit/shared.test.ts b/packages/event/tests/unit/shared.test.ts new file mode 100644 index 00000000..7b5719a0 --- /dev/null +++ b/packages/event/tests/unit/shared.test.ts @@ -0,0 +1,51 @@ +import { Agile, assignSharedAgileInstance } from '@agile-ts/core'; +import { Event, createEvent } from '../../src'; +import { LogMock } from '../../../core/tests/helper/logMock'; + +jest.mock('../../src/event'); + +describe('Shared Tests', () => { + let sharedAgileInstance: Agile; + + beforeEach(() => { + LogMock.mockLogs(); + + sharedAgileInstance = new Agile(); + assignSharedAgileInstance(sharedAgileInstance); + + jest.clearAllMocks(); + }); + + describe('createEvent function tests', () => { + const EventMock = Event as jest.MockedClass; + + it('should create Event with the shared Agile Instance', () => { + const event = createEvent({ + key: 'myCoolEvent', + delay: 10, + }); + + expect(event).toBeInstanceOf(Event); + expect(EventMock).toHaveBeenCalledWith(sharedAgileInstance, { + key: 'myCoolEvent', + delay: 10, + }); + }); + + it('should create Event with a specified Agile Instance', () => { + const agile = new Agile(); + + const event = createEvent({ + key: 'myCoolEvent', + delay: 10, + agileInstance: agile, + }); + + expect(event).toBeInstanceOf(Event); + expect(EventMock).toHaveBeenCalledWith(agile, { + key: 'myCoolEvent', + delay: 10, + }); + }); + }); +}); diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts index e3a2a889..2012864f 100644 --- a/packages/logger/src/index.ts +++ b/packages/logger/src/index.ts @@ -1,491 +1,32 @@ -import { - defineConfig, - generateId, - includesArray, - isFunction, - isValidObject, -} from '@agile-ts/utils'; +import { CreateLoggerConfigInterface, Logger } from './logger'; -export class Logger { - public key?: LoggerKey; - - public isActive: boolean; - public config: LoggerConfigInterface; - public allowedTags: string[] = []; - public loggerCategories: { [key: string]: LoggerCategoryInterface } = {}; // Holds all registered Logger Categories - public watchers: { - [key: string]: LoggerWatcherConfigInterface; - } = {}; - - /** - * @public - * Logger - Handy Class for handling console.logs - */ - constructor(config: LoggerConfig = {}) { - let _config = typeof config === 'function' ? config(this) : config; - _config = defineConfig(_config, { - prefix: '', - allowedTags: [], - canUseCustomStyles: true, - active: true, - level: 0, - timestamp: false, - }); - this.isActive = _config.active as any; - this.allowedTags = _config.allowedTags as any; - this.config = { - timestamp: _config.timestamp as any, - prefix: _config.prefix as any, - canUseCustomStyles: _config.canUseCustomStyles as any, - level: _config.level as any, - }; - this.addDefaultLoggerCategories(); - } - - /** - * @public - * Adds Conditions to Logs - */ - public get if() { - return { - tag: (tags: string[]) => this.tag(tags), - }; - } - - /** - * @public - * Default Levels of Logger - */ - static get level() { - return { - TRACE: 1, - DEBUG: 2, - LOG: 5, - TABLE: 5, - INFO: 10, - SUCCESS: 15, - WARN: 20, - ERROR: 50, - }; - } - - //========================================================================================================= - // Add Default Logger Categories - //========================================================================================================= - /** - * @internal - * Adds Default Logger Categories - */ - private addDefaultLoggerCategories() { - this.createLoggerCategory({ - key: 'log', - level: Logger.level.LOG, - }); - this.createLoggerCategory({ - key: 'debug', - customStyle: 'color: #656565;', - prefix: 'Debug', - level: Logger.level.DEBUG, - }); - this.createLoggerCategory({ - key: 'info', - customStyle: 'color: #6c69a0;', - prefix: 'Info', - level: Logger.level.INFO, - }); - this.createLoggerCategory({ - key: 'success', - customStyle: 'color: #00b300;', - prefix: 'Success', - level: Logger.level.SUCCESS, - }); - this.createLoggerCategory({ - key: 'warn', - prefix: 'Warn', - level: Logger.level.WARN, - }); - this.createLoggerCategory({ - key: 'error', - prefix: 'Error', - level: Logger.level.ERROR, - }); - this.createLoggerCategory({ - key: 'trace', - prefix: 'Trace', - level: Logger.level.TRACE, - }); - this.createLoggerCategory({ - key: 'table', - level: Logger.level.TABLE, - }); - } - - //========================================================================================================= - // Tag - //========================================================================================================= - /** - * @internal - * Only executes following 'command' if all given tags are included in allowedTags - * @param tags - Tags - */ - private tag(tags: string[]) { - if (includesArray(this.allowedTags, tags)) { - return { - log: (...data: any[]) => this.log(...data), - debug: (...data: any[]) => this.debug(...data), - info: (...data: any[]) => this.info(...data), - success: (...data: any[]) => this.success(...data), - warn: (...data: any[]) => this.warn(...data), - error: (...data: any[]) => this.error(...data), - trace: (...data: any[]) => this.trace(...data), - table: (...data: any[]) => this.table(...data), - }; - } - return { - log: () => { - /* do nothing */ - }, - debug: () => { - /* do nothing */ - }, - info: () => { - /* do nothing */ - }, - success: () => { - /* do nothing */ - }, - warn: () => { - /* do nothing */ - }, - error: () => { - /* do nothing */ - }, - trace: () => { - /* do nothing */ - }, - table: () => { - /* do nothing */ - }, - }; - } - - public log(...data: any[]) { - this.invokeConsole(data, 'log', 'log'); - } - - public debug(...data: any[]) { - this.invokeConsole( - data, - 'debug', - typeof console.debug !== 'undefined' ? 'debug' : 'log' - ); - } - - public info(...data: any[]) { - this.invokeConsole( - data, - 'info', - typeof console.info !== 'undefined' ? 'info' : 'log' - ); - } - - public success(...data: any[]) { - this.invokeConsole(data, 'success', 'log'); - } - - public warn(...data: any[]) { - this.invokeConsole( - data, - 'warn', - typeof console.warn !== 'undefined' ? 'warn' : 'log' - ); - } - - public error(...data: any[]) { - this.invokeConsole( - data, - 'error', - typeof console.error !== 'undefined' ? 'error' : 'log' - ); - } - - public trace(...data: any[]) { - this.invokeConsole( - data, - 'trace', - typeof console.trace !== 'undefined' ? 'trace' : 'log' - ); - } - - public table(...data: any[]) { - this.invokeConsole( - data, - 'table', - typeof console.table !== 'undefined' ? 'table' : 'log' - ); - } - - public custom(loggerCategory: string, ...data: any[]) { - this.invokeConsole(data, loggerCategory, 'log'); - } - - //========================================================================================================= - // Invoke Console - //========================================================================================================= - /** - * @internal - * Logs data in Console - * @param data - Data - * @param loggerCategoryKey - Key/Name of Logger Category - * @param consoleLogType - console[consoleLogProperty] - */ - private invokeConsole( - data: any[], - loggerCategoryKey: LoggerCategoryKey, - consoleLogType: ConsoleLogType - ) { - const loggerCategory = this.getLoggerCategory(loggerCategoryKey); - - // Check if Logger Category is allowed - if (!this.isActive || loggerCategory.level < this.config.level) return; - - // Build Prefix of Log - const buildPrefix = (): string => { - let prefix = ''; - if (this.config.timestamp) - prefix = prefix.concat(`[${Date.now().toString()}] `); - if (this.config.prefix) prefix = prefix.concat(this.config.prefix); - if (loggerCategory.prefix) - prefix = prefix.concat(' ' + loggerCategory.prefix); - if (this.config.prefix || loggerCategory.prefix) - prefix = prefix.concat(':'); - return prefix; - }; - - // Add built Prefix - if (typeof data[0] === 'string') - data[0] = buildPrefix().concat(' ').concat(data[0]); - else data.unshift(buildPrefix()); - - // Call Watcher Callbacks - for (const key in this.watchers) { - const watcher = this.watchers[key]; - if (loggerCategory.level >= (watcher.level || 0)) { - watcher.callback(loggerCategory, data); - } - } - - // Init Custom Style - if (this.config.canUseCustomStyles && loggerCategory.customStyle) { - const newLogs: any[] = []; - let hasStyledString = false; // NOTE: Only one style can be used for one String block! - for (const log of data) { - if (!hasStyledString && typeof log === 'string') { - newLogs.push(`%c${log}`); - newLogs.push(loggerCategory.customStyle); - hasStyledString = true; - } else { - newLogs.push(log); - } - } - data = newLogs; - } - - // Handle Console Table Log - if (consoleLogType === 'table') { - if (typeof data[0] === 'string') { - console.log(data[0]); - console.table(data.filter((d) => typeof d !== 'string' && 'number')); - } - return; - } - - // Normal Log - console[consoleLogType](...data); - } - - //========================================================================================================= - // Create Logger Category - //========================================================================================================= - /** - * @public - * Creates new Logger Category - * @param loggerCategory - Logger Category - */ - public createLoggerCategory(loggerCategory: LoggerCategoryInterface) { - loggerCategory = defineConfig(loggerCategory, { - prefix: '', - level: 0, - }); - this.loggerCategories[loggerCategory.key] = loggerCategory; - } - - //========================================================================================================= - // Get Logger Category - //========================================================================================================= - /** - * @public - * Get Logger Category - * @param key - Key/Name of Logger Category - */ - public getLoggerCategory(key: LoggerCategoryKey) { - return this.loggerCategories[key]; - } - - //========================================================================================================= - // Watch - //========================================================================================================= - /** - * @public - * Watches Logger and detects Logs - * @param config - Config - * @return Key of Watcher Function - */ - public watch(config: LoggerWatcherConfigInterface): string; - /** - * @public - * Watches Logger and detects Logs - * @param key - Key of Watcher Function - * @param config - Config - */ - public watch(key: string, config: LoggerWatcherConfigInterface): this; - public watch( - keyOrConfig: string | LoggerWatcherConfigInterface, - config?: LoggerWatcherConfigInterface - ): this | string { - const generateKey = isValidObject(keyOrConfig); - let _config: LoggerWatcherConfigInterface; - let key: string; - - if (generateKey) { - key = generateId(); - _config = keyOrConfig as LoggerWatcherConfigInterface; - } else { - key = keyOrConfig as string; - _config = config as LoggerWatcherConfigInterface; - } - - _config = defineConfig(_config, { - level: 0, - }); - - // Check if Callback is a Function - if (!isFunction(_config.callback)) { - console.error( - 'Agile: A Watcher Callback Function has to be an function!' - ); - return this; - } - - // Check if Callback Function already exists - if (this.watchers[key]) { - console.error( - `Agile: Watcher Callback Function with the key/name ${key} already exists!` - ); - return this; - } - - this.watchers[key] = _config; - return generateKey ? key : this; - } - - //========================================================================================================= - // Remove Watcher - //========================================================================================================= - /** - * @public - * Removes Watcher at given Key - * @param key - Key of Watcher that gets removed - */ - public removeWatcher(key: string): this { - delete this.watchers[key]; - return this; - } - - //========================================================================================================= - // Set Level - //========================================================================================================= - /** - * @public - * Assigns new Level to Logger - * NOTE: Default Levels can be found in 'Logger.level.x' - * @param level - Level - */ - public setLevel(level: number): this { - this.config.level = level; - return this; - } -} - -export type LoggerCategoryKey = string | number; -export type LoggerKey = string | number; +const defaultLogConfig = { + prefix: 'Agile', + active: true, + level: Logger.level.WARN, + canUseCustomStyles: true, + allowedTags: ['runtime', 'storage', 'subscription', 'multieditor'], +}; /** - * @param key - Key/Name of Logger Category - * @param customStyle - Css Styles that get applied to the Logs - * @param prefix - Prefix that gets written before each Log of this Category - * @param level - Until which Level this Logger Category gets logged + * Shared Agile Logger. */ -export interface LoggerCategoryInterface { - key: LoggerCategoryKey; - customStyle?: string; - prefix?: string; - level: number; -} - -/** - * @param prefix - Prefix that gets written before each log of this Logger - * @param canUseCustomStyles - If custom Styles can be applied to the Logs - * @param level - Handles which Logger Categories can be Logged - * @param timestamp - Timestamp that ges written before each log of this Logger - */ -export interface LoggerConfigInterface { - prefix: string; - canUseCustomStyles: boolean; - level: number; - timestamp: boolean; -} +let sharedAgileLogger = new Logger(defaultLogConfig); /** - * @param prefix - Prefix that gets written before each log of this Logger - * @param allowedTags - Only Logs that, contains the allowed Tags or have no Tag get logged - * @param canUseCustomStyles - If custom Styles can be applied to the Logs - * @param active - If Logger is active - * @param level - Handles which Logger Categories can be Logged - * @param timestamp - Timestamp that ges written before each log of this Logger + * Assigns the specified configuration object to the shared Agile Logger. + * + * @param config - Configuration object */ -export interface CreateLoggerConfigInterface { - prefix?: string; - allowedTags?: LoggerKey[]; - canUseCustomStyles?: boolean; - active?: boolean; - level?: number; - timestamp?: boolean; +// https://stackoverflow.com/questions/32558514/javascript-es6-export-const-vs-export-let +function assignSharedAgileLoggerConfig( + config: CreateLoggerConfigInterface = {} +): Logger { + config = { ...defaultLogConfig, ...config }; + sharedAgileLogger = new Logger(config); + return sharedAgileLogger; } -export type LoggerConfig = - | CreateLoggerConfigInterface - | ((logger: Logger) => CreateLoggerConfigInterface); - -export type ConsoleLogType = - | 'log' - | 'warn' - | 'error' - | 'trace' - | 'table' - | 'info' - | 'debug'; - -export type LoggerWatcherCallback = ( - loggerCategory: LoggerCategoryInterface, - data: any[] -) => void; - -/** - * @param callback - Callback Function that gets called if something gets Logged - * @param level - At which level the watcher is called - */ -export interface LoggerWatcherConfigInterface { - callback: LoggerWatcherCallback; - level?: number; -} +export { sharedAgileLogger, assignSharedAgileLoggerConfig }; +export * from './logger'; +export default Logger; diff --git a/packages/logger/src/logger.ts b/packages/logger/src/logger.ts new file mode 100644 index 00000000..b6756030 --- /dev/null +++ b/packages/logger/src/logger.ts @@ -0,0 +1,494 @@ +import { + generateId, + includesArray, + isFunction, + isValidObject, +} from '@agile-ts/utils'; + +export class Logger { + public key?: LoggerKey; + + public isActive: boolean; + public config: LoggerConfigInterface; + public allowedTags: string[] = []; + public loggerCategories: { [key: string]: LoggerCategoryInterface } = {}; // Holds all registered Logger Categories + public watchers: { + [key: string]: LoggerWatcherConfigInterface; + } = {}; + + /** + * @public + * Logger - Handy Class for handling console.logs + */ + constructor(config: LoggerConfig = {}) { + let _config = typeof config === 'function' ? config(this) : config; + _config = { + prefix: '', + allowedTags: [], + canUseCustomStyles: true, + active: true, + level: 0, + timestamp: false, + ..._config, + }; + this.isActive = _config.active as any; + this.allowedTags = _config.allowedTags as any; + this.config = { + timestamp: _config.timestamp as any, + prefix: _config.prefix as any, + canUseCustomStyles: _config.canUseCustomStyles as any, + level: _config.level as any, + }; + this.addDefaultLoggerCategories(); + } + + /** + * @public + * Adds Conditions to Logs + */ + public get if() { + return { + tag: (tags: string[]) => this.tag(tags), + }; + } + + /** + * @public + * Default Levels of Logger + */ + static get level() { + return { + TRACE: 1, + DEBUG: 2, + LOG: 5, + TABLE: 5, + INFO: 10, + SUCCESS: 15, + WARN: 20, + ERROR: 50, + }; + } + + //========================================================================================================= + // Add Default Logger Categories + //========================================================================================================= + /** + * @internal + * Adds Default Logger Categories + */ + private addDefaultLoggerCategories() { + this.createLoggerCategory({ + key: 'log', + level: Logger.level.LOG, + }); + this.createLoggerCategory({ + key: 'debug', + customStyle: 'color: #656565;', + prefix: 'Debug', + level: Logger.level.DEBUG, + }); + this.createLoggerCategory({ + key: 'info', + customStyle: 'color: #6c69a0;', + prefix: 'Info', + level: Logger.level.INFO, + }); + this.createLoggerCategory({ + key: 'success', + customStyle: 'color: #00b300;', + prefix: 'Success', + level: Logger.level.SUCCESS, + }); + this.createLoggerCategory({ + key: 'warn', + prefix: 'Warn', + level: Logger.level.WARN, + }); + this.createLoggerCategory({ + key: 'error', + prefix: 'Error', + level: Logger.level.ERROR, + }); + this.createLoggerCategory({ + key: 'trace', + prefix: 'Trace', + level: Logger.level.TRACE, + }); + this.createLoggerCategory({ + key: 'table', + level: Logger.level.TABLE, + }); + } + + //========================================================================================================= + // Tag + //========================================================================================================= + /** + * @internal + * Only executes following 'command' if all given tags are included in allowedTags + * @param tags - Tags + */ + private tag(tags: string[]) { + if (includesArray(this.allowedTags, tags)) { + return { + log: (...data: any[]) => this.log(...data), + debug: (...data: any[]) => this.debug(...data), + info: (...data: any[]) => this.info(...data), + success: (...data: any[]) => this.success(...data), + warn: (...data: any[]) => this.warn(...data), + error: (...data: any[]) => this.error(...data), + trace: (...data: any[]) => this.trace(...data), + table: (...data: any[]) => this.table(...data), + }; + } + return { + log: () => { + /* do nothing */ + }, + debug: () => { + /* do nothing */ + }, + info: () => { + /* do nothing */ + }, + success: () => { + /* do nothing */ + }, + warn: () => { + /* do nothing */ + }, + error: () => { + /* do nothing */ + }, + trace: () => { + /* do nothing */ + }, + table: () => { + /* do nothing */ + }, + }; + } + + public log(...data: any[]) { + this.invokeConsole(data, 'log', 'log'); + } + + public debug(...data: any[]) { + this.invokeConsole( + data, + 'debug', + typeof console.debug !== 'undefined' ? 'debug' : 'log' + ); + } + + public info(...data: any[]) { + this.invokeConsole( + data, + 'info', + typeof console.info !== 'undefined' ? 'info' : 'log' + ); + } + + public success(...data: any[]) { + this.invokeConsole(data, 'success', 'log'); + } + + public warn(...data: any[]) { + this.invokeConsole( + data, + 'warn', + typeof console.warn !== 'undefined' ? 'warn' : 'log' + ); + } + + public error(...data: any[]) { + this.invokeConsole( + data, + 'error', + typeof console.error !== 'undefined' ? 'error' : 'log' + ); + } + + public trace(...data: any[]) { + this.invokeConsole( + data, + 'trace', + typeof console.trace !== 'undefined' ? 'trace' : 'log' + ); + } + + public table(...data: any[]) { + this.invokeConsole( + data, + 'table', + typeof console.table !== 'undefined' ? 'table' : 'log' + ); + } + + public custom(loggerCategory: string, ...data: any[]) { + this.invokeConsole(data, loggerCategory, 'log'); + } + + //========================================================================================================= + // Invoke Console + //========================================================================================================= + /** + * @internal + * Logs data in Console + * @param data - Data + * @param loggerCategoryKey - Key/Name of Logger Category + * @param consoleLogType - console[consoleLogProperty] + */ + private invokeConsole( + data: any[], + loggerCategoryKey: LoggerCategoryKey, + consoleLogType: ConsoleLogType + ) { + const loggerCategory = this.getLoggerCategory(loggerCategoryKey); + + // Check if Logger Category is allowed + if (!this.isActive || loggerCategory.level < this.config.level) return; + + // Build Prefix of Log + const buildPrefix = (): string => { + let prefix = ''; + if (this.config.timestamp) + prefix = prefix.concat(`[${Date.now().toString()}] `); + if (this.config.prefix) prefix = prefix.concat(this.config.prefix); + if (loggerCategory.prefix) + prefix = prefix.concat(' ' + loggerCategory.prefix); + if (this.config.prefix || loggerCategory.prefix) + prefix = prefix.concat(':'); + return prefix; + }; + + // Add built Prefix + if (typeof data[0] === 'string') + data[0] = buildPrefix().concat(' ').concat(data[0]); + else data.unshift(buildPrefix()); + + // Call Watcher Callbacks + for (const key in this.watchers) { + const watcher = this.watchers[key]; + if (loggerCategory.level >= (watcher.level || 0)) { + watcher.callback(loggerCategory, data); + } + } + + // Init Custom Style + if (this.config.canUseCustomStyles && loggerCategory.customStyle) { + const newLogs: any[] = []; + let hasStyledString = false; // NOTE: Only one style can be used for one String block! + for (const log of data) { + if (!hasStyledString && typeof log === 'string') { + newLogs.push(`%c${log}`); + newLogs.push(loggerCategory.customStyle); + hasStyledString = true; + } else { + newLogs.push(log); + } + } + data = newLogs; + } + + // Handle Console Table Log + if (consoleLogType === 'table') { + if (typeof data[0] === 'string') { + console.log(data[0]); + console.table(data.filter((d) => typeof d !== 'string' && 'number')); + } + return; + } + + // Normal Log + console[consoleLogType](...data); + } + + //========================================================================================================= + // Create Logger Category + //========================================================================================================= + /** + * @public + * Creates new Logger Category + * @param loggerCategory - Logger Category + */ + public createLoggerCategory(loggerCategory: LoggerCategoryInterface) { + loggerCategory = { + prefix: '', + // @ts-ignore + level: 0, + ...loggerCategory, + }; + this.loggerCategories[loggerCategory.key] = loggerCategory; + } + + //========================================================================================================= + // Get Logger Category + //========================================================================================================= + /** + * @public + * Get Logger Category + * @param key - Key/Name of Logger Category + */ + public getLoggerCategory(key: LoggerCategoryKey) { + return this.loggerCategories[key]; + } + + //========================================================================================================= + // Watch + //========================================================================================================= + /** + * @public + * Watches Logger and detects Logs + * @param config - Config + * @return Key of Watcher Function + */ + public watch(config: LoggerWatcherConfigInterface): string; + /** + * @public + * Watches Logger and detects Logs + * @param key - Key of Watcher Function + * @param config - Config + */ + public watch(key: string, config: LoggerWatcherConfigInterface): this; + public watch( + keyOrConfig: string | LoggerWatcherConfigInterface, + config?: LoggerWatcherConfigInterface + ): this | string { + const generateKey = isValidObject(keyOrConfig); + let _config: LoggerWatcherConfigInterface; + let key: string; + + if (generateKey) { + key = generateId(); + _config = keyOrConfig as LoggerWatcherConfigInterface; + } else { + key = keyOrConfig as string; + _config = config as LoggerWatcherConfigInterface; + } + + _config = { + level: 0, + ..._config, + }; + + // Check if Callback is a Function + if (!isFunction(_config.callback)) { + console.error( + 'Agile: A Watcher Callback Function has to be an function!' + ); + return this; + } + + // Check if Callback Function already exists + if (this.watchers[key] != null) { + console.error( + `Agile: Watcher Callback Function with the key/name ${key} already exists!` + ); + return this; + } + + this.watchers[key] = _config; + return generateKey ? key : this; + } + + //========================================================================================================= + // Remove Watcher + //========================================================================================================= + /** + * @public + * Removes Watcher at given Key + * @param key - Key of Watcher that gets removed + */ + public removeWatcher(key: string): this { + delete this.watchers[key]; + return this; + } + + //========================================================================================================= + // Set Level + //========================================================================================================= + /** + * @public + * Assigns new Level to Logger + * NOTE: Default Levels can be found in 'Logger.level.x' + * @param level - Level + */ + public setLevel(level: number): this { + this.config.level = level; + return this; + } +} + +export type LoggerCategoryKey = string | number; +export type LoggerKey = string | number; + +/** + * @param key - Key/Name of Logger Category + * @param customStyle - Css Styles that get applied to the Logs + * @param prefix - Prefix that gets written before each Log of this Category + * @param level - Until which Level this Logger Category gets logged + */ +export interface LoggerCategoryInterface { + key: LoggerCategoryKey; + customStyle?: string; + prefix?: string; + level: number; +} + +/** + * @param prefix - Prefix that gets written before each log of this Logger + * @param canUseCustomStyles - If custom Styles can be applied to the Logs + * @param level - Handles which Logger Categories can be Logged + * @param timestamp - Timestamp that ges written before each log of this Logger + */ +export interface LoggerConfigInterface { + prefix: string; + canUseCustomStyles: boolean; + level: number; + timestamp: boolean; +} + +/** + * @param prefix - Prefix that gets written before each log of this Logger + * @param allowedTags - Only Logs that, contains the allowed Tags or have no Tag get logged + * @param canUseCustomStyles - If custom Styles can be applied to the Logs + * @param active - If Logger is active + * @param level - Handles which Logger Categories can be Logged + * @param timestamp - Timestamp that ges written before each log of this Logger + */ +export interface CreateLoggerConfigInterface { + prefix?: string; + allowedTags?: LoggerKey[]; + canUseCustomStyles?: boolean; + active?: boolean; + level?: number; + timestamp?: boolean; +} + +export type LoggerConfig = + | CreateLoggerConfigInterface + | ((logger: Logger) => CreateLoggerConfigInterface); + +export type ConsoleLogType = + | 'log' + | 'warn' + | 'error' + | 'trace' + | 'table' + | 'info' + | 'debug'; + +export type LoggerWatcherCallback = ( + loggerCategory: LoggerCategoryInterface, + data: any[] +) => void; + +/** + * @param callback - Callback Function that gets called if something gets Logged + * @param level - At which level the watcher is called + */ +export interface LoggerWatcherConfigInterface { + callback: LoggerWatcherCallback; + level?: number; +} diff --git a/packages/multieditor/src/item.ts b/packages/multieditor/src/item.ts index 738eece2..5d1458aa 100644 --- a/packages/multieditor/src/item.ts +++ b/packages/multieditor/src/item.ts @@ -1,8 +1,4 @@ -import { - defineConfig, - State, - StateRuntimeJobConfigInterface, -} from '@agile-ts/core'; +import { State, StateRuntimeJobConfigInterface } from '@agile-ts/core'; import { MultiEditor, Validator, Status, ItemKey } from './internal'; export class Item extends State { @@ -31,9 +27,10 @@ export class Item extends State { super(editor.agileInstance(), data, { key: key, }); - config = defineConfig(config, { + config = { canBeEdited: true, - }); + ...config, + }; this.editor = () => editor; this.validator = editor.getValidator(key); this.config = config; diff --git a/packages/multieditor/src/multieditor.ts b/packages/multieditor/src/multieditor.ts index 5a08d585..440e2549 100644 --- a/packages/multieditor/src/multieditor.ts +++ b/packages/multieditor/src/multieditor.ts @@ -2,8 +2,8 @@ import { Agile, ComputeValueMethod, copy, - defineConfig, getAgileInstance, + LogCodeManager, Observer, } from '@agile-ts/core'; import { @@ -52,19 +52,20 @@ export class MultiEditor< ) { if (!agileInstance) agileInstance = getAgileInstance(null); if (!agileInstance) - Agile.logger.error( + LogCodeManager.getLogger()?.error( 'No Global agileInstance found! Please pass an agileInstance into the MultiEditor!' ); this.agileInstance = () => agileInstance as any; let _config = typeof config === 'function' ? config(this) : config; - _config = defineConfig(_config, { + _config = { fixedProperties: [], editableProperties: Object.keys(_config.data), validateMethods: {}, computeMethods: {}, reValidateMode: 'onSubmit', validate: 'editable', - }); + ..._config, + }; this._key = _config?.key; this.onSubmit = _config.onSubmit as any; this.fixedProperties = _config.fixedProperties as any; @@ -166,9 +167,10 @@ export class MultiEditor< ): this { const item = this.getItemById(key); if (!item) return this; - config = defineConfig(config, { + config = { background: true, - }); + ...config, + }; // Apply changes to Item item.set(value, config); @@ -193,10 +195,11 @@ export class MultiEditor< ): this { const item = this.getItemById(key); if (!item) return this; - config = defineConfig(config, { + config = { background: false, reset: true, - }); + ...config, + }; // Update initial Value item.initialStateValue = copy(value); @@ -227,10 +230,11 @@ export class MultiEditor< config: SubmitConfigInterface = {} ): Promise { const preparedData: DataObject = {}; - config = defineConfig(config, { + config = { assignToInitial: true, onSubmitConfig: undefined, - }); + ...config, + }; // Assign Statuses to Items for (const key in this.data) { @@ -247,9 +251,9 @@ export class MultiEditor< this.submitted = true; // Logging - Agile.logger.if - .tag(['multieditor']) - .info(`Submit MultiEditor '${this.key}'`, this.isValid); + // Agile.logger.if + // .tag(['multieditor']) + // .info(`Submit MultiEditor '${this.key}'`, this.isValid); // Check if Editor is Valid if (!this.isValid) return false; @@ -354,7 +358,9 @@ export class MultiEditor< */ public getItemById(key: ItemKey): Item | undefined { if (!Object.prototype.hasOwnProperty.call(this.data, key)) { - Agile.logger.error(`Editor Item '${key}' does not exists!`); + LogCodeManager.getLogger()?.error( + `Editor Item '${key}' does not exists!` + ); return undefined; } return this.data[key]; diff --git a/packages/multieditor/src/status/status.observer.ts b/packages/multieditor/src/status/status.observer.ts index 4805ac20..dd39089b 100644 --- a/packages/multieditor/src/status/status.observer.ts +++ b/packages/multieditor/src/status/status.observer.ts @@ -2,7 +2,6 @@ import { Status, StatusInterface } from '../internal'; import { Agile, copy, - defineConfig, equal, IngestConfigInterface, Observer, @@ -45,7 +44,7 @@ export class StatusObserver extends Observer { * @param config - Config */ public assign(config: StatusIngestConfigInterface = {}): void { - config = defineConfig(config, { + config = { perform: true, background: false, sideEffects: { @@ -53,7 +52,8 @@ export class StatusObserver extends Observer { exclude: [], }, force: false, - }); + ...config, + }; // Set Next Status Value this.nextValue = copy(this.status().nextValue); diff --git a/packages/multieditor/src/validator/index.ts b/packages/multieditor/src/validator/index.ts index 0e3625f9..bcc9fe7d 100644 --- a/packages/multieditor/src/validator/index.ts +++ b/packages/multieditor/src/validator/index.ts @@ -1,10 +1,4 @@ -import { - Agile, - copy, - defineConfig, - generateId, - isFunction, -} from '@agile-ts/core'; +import { copy, generateId, isFunction, LogCodeManager } from '@agile-ts/core'; import { DataObject, MultiEditor, @@ -24,9 +18,10 @@ export class Validator { * @param config - Config */ constructor(config: ValidatorConfigInterface = {}) { - this.config = defineConfig(config, { + this.config = { prefix: 'default', - }); + ...config, + }; this._key = this.config.key; } @@ -125,13 +120,15 @@ export class Validator { // Check if Validation Method is a Function if (!isFunction(_method)) { - Agile.logger.error('A Validation Method has to be a function!'); + LogCodeManager.getLogger()?.error( + 'A Validation Method has to be a function!' + ); return this; } // Check if Validation Method already exists if (this.validationMethods[key]) { - Agile.logger.error( + LogCodeManager.getLogger()?.error( `Validation Method with the key/name '${key}' already exists!` ); return this; diff --git a/packages/react/.size-limit.js b/packages/react/.size-limit.js new file mode 100644 index 00000000..addba251 --- /dev/null +++ b/packages/react/.size-limit.js @@ -0,0 +1,6 @@ +module.exports = [ + { + path: 'dist/*', + limit: '2 kB', + }, +]; diff --git a/packages/react/README.md b/packages/react/README.md index e02ab0d9..fa074c38 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -75,8 +75,7 @@ Therefore, we have created a table that shows which versions fit together withou | @agile-ts/react | @agile-ts/core | NPM Version | Supported React versions | Supports hook based components | | ---------------- | ----------------------- | ------------------------ | -------------------------|---------------------------------- | -| v0.0.15+ | v0.0.16+ | v6+ | 16.8+ | Yes | -| v0.0.7 - v0.0.14 | v0.0.7 - v0.0.15 | v6+ | 16.8+ | Yes | +| v0.1.1+ | v0.1.1+ | v6+ | 16.8+ | Yes | _Older Versions aren't supported anymore_ diff --git a/packages/react/package.json b/packages/react/package.json index 922ce4ec..376b08b2 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -35,7 +35,8 @@ "preview": "npm pack", "test": "jest", "test:coverage": "jest --coverage", - "lint": "eslint src/**/*" + "lint": "eslint src/**/*", + "size": "yarn run build && size-limit" }, "devDependencies": { "@agile-ts/core": "file:../core", diff --git a/packages/react/src/hocs/AgileHOC.ts b/packages/react/src/hocs/AgileHOC.ts index 0a4261f2..4832162c 100644 --- a/packages/react/src/hocs/AgileHOC.ts +++ b/packages/react/src/hocs/AgileHOC.ts @@ -10,6 +10,7 @@ import { flatMerge, extractRelevantObservers, normalizeArray, + LogCodeManager, } from '@agile-ts/core'; /** @@ -57,7 +58,10 @@ export function AgileHOC( } } if (!agileInstance || !agileInstance.subController) { - Agile.logger.error('Failed to subscribe Component with deps', deps); + LogCodeManager.getLogger()?.error( + 'Failed to subscribe Component with deps', + deps + ); return reactComponent; } diff --git a/packages/react/src/hooks/useAgile.ts b/packages/react/src/hooks/useAgile.ts index dead032f..97fb30d0 100644 --- a/packages/react/src/hooks/useAgile.ts +++ b/packages/react/src/hooks/useAgile.ts @@ -6,7 +6,6 @@ import { Observer, State, SubscriptionContainerKeyType, - defineConfig, isValidObject, generateId, ProxyWeakMapType, @@ -14,9 +13,10 @@ import { extractRelevantObservers, SelectorWeakMapType, SelectorMethodType, + LogCodeManager, + normalizeArray, } from '@agile-ts/core'; import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'; -import { normalizeArray } from '@agile-ts/utils'; import { AgileOutputHookArrayType, AgileOutputHookType } from './useOutput'; // TODO https://stackoverflow.com/questions/68148235/require-module-inside-a-function-doesnt-work @@ -66,13 +66,15 @@ export function useAgile< deps: X | Y, config: AgileHookConfigInterface = {} ): AgileOutputHookArrayType | AgileOutputHookType { - config = defineConfig(config, { + config = { key: generateId(), proxyBased: false, - agileInstance: null, + agileInstance: null as any, componentId: undefined, observerType: undefined, - }); + deps: [], + ...config, + }; const depsArray = extractRelevantObservers( normalizeArray(deps), config.observerType @@ -142,7 +144,7 @@ export function useAgile< // Try to extract Agile Instance from the specified Instance/s if (!agileInstance) agileInstance = getAgileInstance(observers[0]); if (!agileInstance || !agileInstance.subController) { - Agile.logger.error( + LogCodeManager.getLogger()?.error( 'Failed to subscribe Component with deps because of missing valid Agile Instance.', deps ); @@ -160,8 +162,9 @@ export function useAgile< // Building the Path WeakMap in the 'useIsomorphicLayoutEffect' // because the 'useIsomorphicLayoutEffect' is called after the rerender. // -> All used paths in the UI-Component were successfully tracked. - const proxyWeakMap: ProxyWeakMapType = new WeakMap(); + let proxyWeakMap: ProxyWeakMapType | undefined = undefined; if (config.proxyBased && proxyPackage != null) { + proxyWeakMap = new WeakMap(); for (const observer of observers) { const proxyTree = proxyTreeWeakMap.get(observer); if (proxyTree != null) { @@ -173,8 +176,9 @@ export function useAgile< } // Build Selector WeakMap based on the specified selector method - const selectorWeakMap: SelectorWeakMapType = new WeakMap(); + let selectorWeakMap: SelectorWeakMapType | undefined = undefined; if (config.selector != null) { + selectorWeakMap = new WeakMap(); for (const observer of observers) { selectorWeakMap.set(observer, { methods: [config.selector] }); } @@ -191,7 +195,7 @@ export function useAgile< proxyWeakMap, waitForMount: false, componentId: config.componentId, - selectorWeakMap: selectorWeakMap, + selectorWeakMap, } ); @@ -199,7 +203,7 @@ export function useAgile< return () => { agileInstance?.subController.unsubscribe(subscriptionContainer); }; - }, []); + }, config.deps); return getReturnValue(depsArray); } @@ -258,4 +262,13 @@ export interface AgileHookConfigInterface { * @default undefined */ observerType?: string; + /** + * Dependencies that determine, in addition to unmounting and remounting the React-Component, + * when the specified Agile Sub Instances should be re-subscribed to the React-Component. + * + * [Github issue](https://github.com/agile-ts/agile/issues/170) + * + * @default [] + */ + deps?: any[]; } diff --git a/packages/react/src/hooks/useIsomorphicLayoutEffect.ts b/packages/react/src/hooks/useIsomorphicLayoutEffect.ts index 9e397c06..c76c0393 100644 --- a/packages/react/src/hooks/useIsomorphicLayoutEffect.ts +++ b/packages/react/src/hooks/useIsomorphicLayoutEffect.ts @@ -1,4 +1,5 @@ import { useEffect, useLayoutEffect } from 'react'; +import { runsOnServer } from '@agile-ts/core'; // React currently throws a warning when using useLayoutEffect on the server. // To get around it, we can conditionally useEffect on the server (no-op) and @@ -9,9 +10,6 @@ import { useEffect, useLayoutEffect } from 'react'; // is created synchronously, otherwise a store update may occur before the // subscription is created and an inconsistent state may be observed -export const useIsomorphicLayoutEffect = - typeof window !== 'undefined' && - typeof window.document !== 'undefined' && - typeof window.document.createElement !== 'undefined' - ? useLayoutEffect - : useEffect; +export const useIsomorphicLayoutEffect = !runsOnServer() + ? useLayoutEffect + : useEffect; diff --git a/packages/react/src/react.integration.ts b/packages/react/src/react.integration.ts index a0bf8774..2ba63ec8 100644 --- a/packages/react/src/react.integration.ts +++ b/packages/react/src/react.integration.ts @@ -1,4 +1,4 @@ -import { Agile, flatMerge, Integration } from '@agile-ts/core'; +import { flatMerge, Integration, Integrations } from '@agile-ts/core'; import { AgileReactComponent } from './hocs/AgileHOC'; import React from 'react'; @@ -24,6 +24,6 @@ const reactIntegration = new Integration({ } }, }); -Agile.initialIntegrations.push(reactIntegration); +Integrations.addInitialIntegration(reactIntegration); export default reactIntegration; diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index e9104543..736c82f2 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -8,19 +8,21 @@ * @param value - Array/Object that gets copied */ export function copy(value: T): T { - // Extra checking '!value' because 'typeof null === object' - if (!value) return value; + // Extra checking 'value == null' because 'typeof null === object' + if (value == null || typeof value !== 'object') return value; - // Ignore everything that is no object or array - const valConstructorName = Object.getPrototypeOf(value).constructor.name; - if (!['object', 'array'].includes(valConstructorName.toLowerCase())) + // Ignore everything that is no object or array but has the type of an object (e.g. classes) + const valConstructorName = Object.getPrototypeOf( + value + ).constructor.name.toLowerCase(); + if (valConstructorName !== 'object' && valConstructorName !== 'array') return value; let temp; const newObject: any = Array.isArray(value) ? [] : {}; for (const property in value) { temp = value[property]; - newObject[property] = typeof temp === 'object' ? copy(temp) : temp; + newObject[property] = copy(temp); } return newObject as T; } @@ -86,9 +88,10 @@ export function normalizeArray( items?: DataType | Array, config: { createUndefinedArray?: boolean } = {} ): Array { - config = defineConfig(config, { + config = { createUndefinedArray: false, // If it should return [] or [undefined] if the passed Item is undefined - }); + ...config, + }; if (items == null && !config.createUndefinedArray) return []; return Array.isArray(items) ? items : [items as DataType]; } @@ -140,34 +143,6 @@ export function isJsonString(value: any): boolean { return true; } -//========================================================================================================= -// Define Config -//========================================================================================================= -/** - * @internal - * Merges default values/properties into config object - * @param config - Config object that receives default values - * @param defaults - Default values object that gets merged into config object - * @param overwriteUndefinedProperties - If undefined Properties in config gets overwritten by the default value - */ -export function defineConfig( - config: ConfigInterface, - defaults: Object, - overwriteUndefinedProperties?: boolean -): ConfigInterface { - if (overwriteUndefinedProperties === undefined) - overwriteUndefinedProperties = true; - - if (overwriteUndefinedProperties) { - const finalConfig = { ...defaults, ...config }; - for (const key in finalConfig) - if (finalConfig[key] === undefined) finalConfig[key] = defaults[key]; - return finalConfig; - } - - return { ...defaults, ...config }; -} - //========================================================================================================= // Flat Merge //========================================================================================================= @@ -192,9 +167,10 @@ export function flatMerge( changes: Object, config: FlatMergeConfigInterface = {} ): DataType { - config = defineConfig(config, { + config = { addNewProperties: true, - }); + ...config, + }; // Copy Source to avoid References const _source = copy(source); @@ -223,7 +199,14 @@ export function flatMerge( * @param value2 - Second Value */ export function equal(value1: any, value2: any): boolean { - return value1 === value2 || JSON.stringify(value1) === JSON.stringify(value2); + return ( + value1 === value2 || + // Checking if 'value1' and 'value2' is typeof object before + // using the JSON.stringify comparison to optimize the performance + (typeof value1 === 'object' && + typeof value2 === 'object' && + JSON.stringify(value1) === JSON.stringify(value2)) + ); } //========================================================================================================= diff --git a/packages/utils/tests/unit/utils.test.ts b/packages/utils/tests/unit/utils.test.ts index 8206d311..cefdff3c 100644 --- a/packages/utils/tests/unit/utils.test.ts +++ b/packages/utils/tests/unit/utils.test.ts @@ -1,7 +1,6 @@ import { clone, copy, - defineConfig, equal, flatMerge, generateId, @@ -15,12 +14,12 @@ import { createArrayFromObject, removeProperties, } from '../../src'; -import mockConsole from 'jest-mock-console'; +import { LogMock } from '../../../core/tests/helper/logMock'; describe('Utils Tests', () => { beforeEach(() => { + LogMock.mockLogs(); jest.clearAllMocks(); - mockConsole(['error', 'warn']); }); describe('copy function tests', () => { @@ -259,58 +258,6 @@ describe('Utils Tests', () => { }); }); - describe('defineConfig function tests', () => { - it('should merge defaults into config and overwrite undefined properties (default config)', () => { - const config = { - allowLogging: true, - loops: 10, - isHuman: undefined, - }; - expect( - defineConfig(config, { - allowLogging: false, - loops: 15, - isHuman: true, - isRobot: false, - name: 'jeff', - }) - ).toStrictEqual({ - allowLogging: true, - loops: 10, - isHuman: true, - isRobot: false, - name: 'jeff', - }); - }); - - it("should merge defaults into config and shouldn't overwrite undefined properties (overwriteUndefinedProperties = false)", () => { - const config = { - allowLogging: true, - loops: 10, - isHuman: undefined, - }; - expect( - defineConfig( - config, - { - allowLogging: false, - loops: 15, - isHuman: true, - isRobot: false, - name: 'jeff', - }, - false - ) - ).toStrictEqual({ - allowLogging: true, - loops: 10, - isHuman: undefined, - isRobot: false, - name: 'jeff', - }); - }); - }); - describe('flatMerge function tests', () => { it('should merge Changes Object into Source Object', () => { const source = { diff --git a/packages/vue/README.md b/packages/vue/README.md index a82cbad0..f446a9d7 100644 --- a/packages/vue/README.md +++ b/packages/vue/README.md @@ -43,7 +43,7 @@ Therefore, we have created a table that shows which versions fit together withou | @agile-ts/vue | @agile-ts/core | NPM Version | Supported Vue versions | | ---------------- | ----------------------- | ------------------------ | -------------------------| -| v0.0.01+ | v0.0.16+ | v6+ | 2.x (3.x not tested) | +| v0.1.1+ | v0.1.1+ | v6+ | 2.x (3.x not tested) | _Older Versions aren't supported anymore_ diff --git a/packages/vue/src/vue.integration.ts b/packages/vue/src/vue.integration.ts index cf2268b9..d6a367ec 100644 --- a/packages/vue/src/vue.integration.ts +++ b/packages/vue/src/vue.integration.ts @@ -1,4 +1,4 @@ -import Agile, { Integration } from '@agile-ts/core'; +import Agile, { Integration, Integrations } from '@agile-ts/core'; import Vue from 'vue'; import { bindAgileInstances, DepsType } from './bindAgileInstances'; @@ -80,6 +80,6 @@ const vueIntegration = new Integration({ return Promise.resolve(true); }, }); -Agile.initialIntegrations.push(vueIntegration); +Integrations.addInitialIntegration(vueIntegration); export default vueIntegration; diff --git a/yarn.lock b/yarn.lock index f1bbca79..d3795c28 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3,13 +3,17 @@ "@agile-ts/core@file:packages/core": - version "0.0.17" + version "0.1.0" dependencies: - "@agile-ts/logger" "^0.0.4" - "@agile-ts/utils" "^0.0.4" + "@agile-ts/utils" "^0.0.5" + +"@agile-ts/logger@file:packages/logger": + version "0.0.5" + dependencies: + "@agile-ts/utils" "^0.0.5" "@agile-ts/proxytree@file:packages/proxytree": - version "0.0.3" + version "0.0.4" "@akryum/winattr@^3.0.0": version "3.0.0"