diff --git a/benchmark/README.md b/benchmark/README.md index c509afd3..ce2e9db2 100644 --- a/benchmark/README.md +++ b/benchmark/README.md @@ -10,75 +10,95 @@ https://stackoverflow.com/questions/28524653/what-do-the-results-from-benchmark- ## Counter Benchmark ```ts -1. Zustand x 30,591 ops/sec ±1.15% (61 runs sampled) -2. Redux x 30,239 ops/sec ±1.64% (63 runs sampled) -3. Mobx x 29,032 ops/sec ±1.24% (64 runs sampled) -4. AgileTs x 28,327 ops/sec ±2.96% (60 runs sampled) -5. Redux-Toolkit x 22,808 ops/sec ±1.79% (65 runs sampled) -6. Jotai x 22,479 ops/sec ±5.79% (63 runs sampled) -7. Valtio x 20,784 ops/sec ±2.75% (63 runs sampled) -8. Recoil x 14,351 ops/sec ±1.55% (65 runs sampled) +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 -Agile Collection x 13,729 ops/sec ±3.42% (60 runs sampled) [updatedFieldsCount: 76468, renderFieldsCount: 73] -Agile State x 19,008 ops/sec ±1.87% (66 runs sampled) [updatedFieldsCount: 103559, renderFieldsCount: 72] -Agile nested State x 21,119 ops/sec ±1.45% (64 runs sampled) [updatedFieldsCount: 116226, renderFieldsCount: 72] -Hookstate x 20,026 ops/sec ±0.68% (64 runs sampled) [updatedFieldsCount: 112513, renderFieldsCount: 112513] -Jotai x 16,372 ops/sec ±3.34% (63 runs sampled) [updatedFieldsCount: 90275, renderFieldsCount: 90275] -Mobx x 15,892 ops/sec ±3.42% (60 runs sampled) [updatedFieldsCount: 82400, renderFieldsCount: 82400] -Nano Stores x 21,455 ops/sec ±1.00% (66 runs sampled) [updatedFieldsCount: 114136, renderFieldsCount: 114136] -Recoil x 11,504 ops/sec ±3.44% (63 runs sampled) [updatedFieldsCount: 61553, renderFieldsCount: 61554] -Redux x 13,070 ops/sec ±2.73% (62 runs sampled) [updatedFieldsCount: 69239, renderFieldsCount: 69240] -Valtio x 9,962 ops/sec ±2.60% (60 runs sampled) [updatedFieldsCount: 54290, renderFieldsCount: 108579] +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 -Agile Collection x 10,651 ops/sec ±4.14% (58 runs sampled) [updatedFieldsCount: 56668, renderFieldsCount: 582] -Agile State x 16,175 ops/sec ±1.55% (65 runs sampled) [updatedFieldsCount: 87481, renderFieldsCount: 80] -Agile nested State x 20,703 ops/sec ±1.27% (65 runs sampled) [updatedFieldsCount: 113946, renderFieldsCount: 712] -Hookstate x 18,733 ops/sec ±3.14% (59 runs sampled) [updatedFieldsCount: 105792, renderFieldsCount: 105801] -Jotai x 15,602 ops/sec ±3.65% (61 runs sampled) [updatedFieldsCount: 85977, renderFieldsCount: 85986] -Mobx x 9,283 ops/sec ±3.16% (52 runs sampled) [updatedFieldsCount: 50806, renderFieldsCount: 508060] -Nano Stores x 20,125 ops/sec ±1.62% (62 runs sampled) [updatedFieldsCount: 108704, renderFieldsCount: 108713] -Recoil x 11,103 ops/sec ±4.50% (61 runs sampled) [updatedFieldsCount: 62920, renderFieldsCount: 62939] -Redux x 8,728 ops/sec ±1.61% (64 runs sampled) [updatedFieldsCount: 50794, renderFieldsCount: 507950] -Valtio x 3,557 ops/sec ±2.96% (23 runs sampled) [updatedFieldsCount: 22473, renderFieldsCount: 449450] +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 -Agile Collection x 3,897 ops/sec ±3.01% (25 runs sampled) [updatedFieldsCount: 24427, renderFieldsCount: 2502] -Agile State x 8,355 ops/sec ±0.85% (67 runs sampled) [updatedFieldsCount: 46249, renderFieldsCount: 173] -Agile nested State x 18,641 ops/sec ±1.17% (63 runs sampled) [updatedFieldsCount: 98669, renderFieldsCount: 6802] -Hookstate x 14,865 ops/sec ±2.51% (61 runs sampled) [updatedFieldsCount: 81616, renderFieldsCount: 81715] -Jotai x 12,676 ops/sec ±3.09% (61 runs sampled) [updatedFieldsCount: 65930, renderFieldsCount: 66029] -Mobx x 1,812 ops/sec ±1.49% (63 runs sampled) [updatedFieldsCount: 9639, renderFieldsCount: 963900] -Nano Stores x 16,283 ops/sec ±1.39% (62 runs sampled) [updatedFieldsCount: 84772, renderFieldsCount: 84871] -Recoil x 9,418 ops/sec ±2.94% (62 runs sampled) [updatedFieldsCount: 52425, renderFieldsCount: 52624] -Redux x 1,896 ops/sec ±1.74% (62 runs sampled) [updatedFieldsCount: 10133, renderFieldsCount: 1013400] -Valtio x 472 ops/sec ±2.97% (61 runs sampled) [updatedFieldsCount: 2494, renderFieldsCount: 498700] +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 -Agile Collection x 503 ops/sec ±2.23% (62 runs sampled) [updatedFieldsCount: 2616, renderFieldsCount: 3520] -Agile State x 1,437 ops/sec ±1.48% (59 runs sampled) [updatedFieldsCount: 7569, renderFieldsCount: 1061] -Agile nested State x 9,411 ops/sec ±1.54% (56 runs sampled) [updatedFieldsCount: 46693, renderFieldsCount: 33243] -Hookstate x 4,539 ops/sec ±3.61% (27 runs sampled) [updatedFieldsCount: 26381, renderFieldsCount: 27380] -Jotai x 4,014 ops/sec ±5.35% (53 runs sampled) [updatedFieldsCount: 20390, renderFieldsCount: 21389] -Mobx x 151 ops/sec ±0.75% (59 runs sampled) [updatedFieldsCount: 786, renderFieldsCount: 786000] -Nano Stores x 5,511 ops/sec ±6.27% (32 runs sampled) [updatedFieldsCount: 31266, renderFieldsCount: 32265] -Recoil x 3,562 ops/sec ±3.16% (58 runs sampled) [updatedFieldsCount: 18503, renderFieldsCount: 20502] -Redux x 165 ops/sec ±1.40% (57 runs sampled) [updatedFieldsCount: 858, renderFieldsCount: 859000] -Valtio x 38.76 ops/sec ±5.50% (42 runs sampled) [updatedFieldsCount: 215, renderFieldsCount: 429000] +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 -Agile Hard Coded x 23,201 ops/sec ±1.39% (64 runs sampled) -Agile Auto Tracking x 22,661 ops/sec ±3.31% (60 runs sampled) -Jotai x 18,489 ops/sec ±5.43% (62 runs sampled) -Recoil x 10,312 ops/sec ±2.57% (64 runs sampled) +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 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 index f30fcef4..7269c0a0 100644 --- a/benchmark/benchmarks/react/1000fields/bench/agilets/collection.tsx +++ b/benchmark/benchmarks/react/1000fields/bench/agilets/collection.tsx @@ -1,8 +1,10 @@ import React from 'react'; import ReactDom from 'react-dom'; -import { createCollection } from '@agile-ts/core'; +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) => ({ diff --git a/benchmark/benchmarks/react/1000fields/bench/agilets/nestedState.tsx b/benchmark/benchmarks/react/1000fields/bench/agilets/nestedState.tsx index 88fe5ba6..377b6b01 100644 --- a/benchmark/benchmarks/react/1000fields/bench/agilets/nestedState.tsx +++ b/benchmark/benchmarks/react/1000fields/bench/agilets/nestedState.tsx @@ -1,8 +1,10 @@ import * as React from 'react'; import * as ReactDom from 'react-dom'; -import { createState, State } from '@agile-ts/core'; +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) => diff --git a/benchmark/benchmarks/react/1000fields/bench/agilets/state.tsx b/benchmark/benchmarks/react/1000fields/bench/agilets/state.tsx index 08069b33..93f9798a 100644 --- a/benchmark/benchmarks/react/1000fields/bench/agilets/state.tsx +++ b/benchmark/benchmarks/react/1000fields/bench/agilets/state.tsx @@ -1,8 +1,10 @@ import React from 'react'; import ReactDom from 'react-dom'; -import { createState } from '@agile-ts/core'; +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}`) diff --git a/benchmark/benchmarks/react/1000fields/index.ts b/benchmark/benchmarks/react/1000fields/index.ts index bb42d83e..9b251c1d 100644 --- a/benchmark/benchmarks/react/1000fields/index.ts +++ b/benchmark/benchmarks/react/1000fields/index.ts @@ -1,5 +1,12 @@ -import Benchmark, { Suite, Options } from 'benchmark'; 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'; @@ -66,14 +73,16 @@ function configTest( }; } +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('Pulse State', configTest(pulseState)) + // .add('Pulse nested State', configTest(pulseNestedState)) .add('Hookstate', configTest(hookstate)) .add('Jotai', configTest(jotai)) .add('Mobx', configTest(mobx)) @@ -84,16 +93,18 @@ suite // Add Listener .on('start', function (this: any) { - console.log(`Starting ${this.name}`); + startBenchmarkLog(this.name); }) .on('cycle', (event: any) => { - console.log( - String(event.target), + const cycleResult = getCycleResult(event); + cycleLog( + cycleResult, `[updatedFieldsCount: ${event.target.updatedFieldsCount}, renderFieldsCount: ${event.target.renderFieldsCount}]` ); + results.push(cycleResult); }) .on('complete', function (this: any) { - console.log(`Fastest is ${this.filter('fastest').map('name')}`); + endBenchmarkLog(this.name, results, this.filter('fastest').map('name')); // @ts-ignore // Notify server that the Benchmark Test Suite has ended diff --git a/benchmark/benchmarks/react/computed/bench/agilets/autoTracking.tsx b/benchmark/benchmarks/react/computed/bench/agilets/autoTracking.tsx index df85d176..0b596405 100644 --- a/benchmark/benchmarks/react/computed/bench/agilets/autoTracking.tsx +++ b/benchmark/benchmarks/react/computed/bench/agilets/autoTracking.tsx @@ -1,8 +1,9 @@ import React from 'react'; import ReactDom from 'react-dom'; -import { createComputed, createState } from '@agile-ts/core'; +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; diff --git a/benchmark/benchmarks/react/computed/bench/agilets/hardCoded.tsx b/benchmark/benchmarks/react/computed/bench/agilets/hardCoded.tsx index ac782b55..59cc57a8 100644 --- a/benchmark/benchmarks/react/computed/bench/agilets/hardCoded.tsx +++ b/benchmark/benchmarks/react/computed/bench/agilets/hardCoded.tsx @@ -1,8 +1,9 @@ import React from 'react'; import ReactDom from 'react-dom'; -import { createComputed, createState } from '@agile-ts/core'; +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( () => { diff --git a/benchmark/benchmarks/react/computed/index.ts b/benchmark/benchmarks/react/computed/index.ts index 3c089e8a..e70b9c70 100644 --- a/benchmark/benchmarks/react/computed/index.ts +++ b/benchmark/benchmarks/react/computed/index.ts @@ -1,5 +1,12 @@ 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'; @@ -13,7 +20,7 @@ import recoil from './bench/recoil'; window.Benchmark = Benchmark; // Create new Benchmark Test Suite -const suite = new Suite('Count'); +const suite = new Suite('Computed'); // Retrieve the Element to render the Benchmark Test Suite in const target = document.getElementById('bench')!; @@ -53,6 +60,8 @@ function configTest(renderElement: (target: HTMLElement) => void): Options { }; } +const results: CycleResultInterface[] = []; + // Add Tests to the Benchmark Test Suite suite .add('Agile Auto Tracking', configTest(agileAutoTracking)) @@ -63,16 +72,18 @@ suite // Add Listener .on('start', function (this: any) { - console.log(`Starting ${this.name}`); + startBenchmarkLog(this.name); }) .on('cycle', (event: any) => { - console.log( - String(event.target), + const cycleResult = getCycleResult(event); + cycleLog( + cycleResult, `[Count: ${event.target.output}, ComputedCount: ${event.target.computedOutput}]` ); + results.push(cycleResult); }) .on('complete', function (this: any) { - console.log(`Fastest is ${this.filter('fastest').map('name')}`); + endBenchmarkLog(this.name, results, this.filter('fastest').map('name')); // @ts-ignore // Notify server that the Benchmark Test Suite has ended diff --git a/benchmark/benchmarks/react/counter/bench/agilets.tsx b/benchmark/benchmarks/react/counter/bench/agilets.tsx index e4e8ddb1..309b2f2a 100644 --- a/benchmark/benchmarks/react/counter/bench/agilets.tsx +++ b/benchmark/benchmarks/react/counter/bench/agilets.tsx @@ -1,8 +1,9 @@ import React from 'react'; import ReactDom from 'react-dom'; -import { createState } from '@agile-ts/core'; +import { createState, LogCodeManager } from '@agile-ts/core'; import { useAgile } from '@agile-ts/react'; +LogCodeManager.getLogger().isActive = false; const COUNT = createState(0); const App = () => { diff --git a/benchmark/benchmarks/react/counter/index.ts b/benchmark/benchmarks/react/counter/index.ts index 946030fa..067a85de 100644 --- a/benchmark/benchmarks/react/counter/index.ts +++ b/benchmark/benchmarks/react/counter/index.ts @@ -1,5 +1,12 @@ 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'; @@ -52,29 +59,33 @@ function configTest(renderElement: (target: HTMLElement) => void): Options { }; } +const results: CycleResultInterface[] = []; + // Add Tests to the Benchmark Test Suite suite .add('AgileTs', configTest(agilets)) - .add('Hookstate', configTest(hookstate)) - .add('Jotai', configTest(jotai)) - .add('Mobx', configTest(mobx)) - .add('Nano Stores', configTest(nanostores)) - .add('PulseJs', configTest(pulsejs)) - .add('Recoil', configTest(recoil)) - .add('Redux', configTest(redux)) - .add('Redux-Toolkit', configTest(reduxToolkit)) - .add('Valtio', configTest(valtio)) - .add('Zustand', configTest(zustand)) + // .add('Hookstate', configTest(hookstate)) + // .add('Jotai', configTest(jotai)) + // .add('Mobx', configTest(mobx)) + // .add('Nano Stores', configTest(nanostores)) + // .add('PulseJs', configTest(pulsejs)) + // .add('Recoil', configTest(recoil)) + // .add('Redux', configTest(redux)) + // .add('Redux-Toolkit', configTest(reduxToolkit)) + // .add('Valtio', configTest(valtio)) + // .add('Zustand', configTest(zustand)) // Add Listener .on('start', function (this: any) { - console.log(`Starting ${this.name}`); + startBenchmarkLog(this.name); }) .on('cycle', (event: any) => { - console.log(String(event.target), `[Count: ${event.target.output}]`); + const cycleResult = getCycleResult(event); + cycleLog(cycleResult, `[Count: ${event.target.output}]`); + results.push(cycleResult); }) .on('complete', function (this: any) { - console.log(`Fastest is ${this.filter('fastest').map('name')}`); + endBenchmarkLog(this.name, results, this.filter('fastest').map('name')); // @ts-ignore // Notify server that the Benchmark Test Suite has ended 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/package.json b/benchmark/package.json index 5217433f..d7cf2247 100644 --- a/benchmark/package.json +++ b/benchmark/package.json @@ -11,8 +11,9 @@ "test:counter": "yarn test ./benchmarks/react/counter", "test:1000fields": "yarn test ./benchmarks/react/1000fields", "test:computed": "yarn test ./benchmarks/react/computed", - "install:local:agile": "yalc add @agile-ts/core @agile-ts/react", - "install:public:agile": "yarn add @agile-ts/core @agile-ts/react" + "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", @@ -26,6 +27,7 @@ "@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", diff --git a/benchmark/run.ts b/benchmark/run.ts index a711778c..2bd03642 100644 --- a/benchmark/run.ts +++ b/benchmark/run.ts @@ -1,6 +1,7 @@ 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(); @@ -16,6 +17,8 @@ if (entry == null) { } 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( @@ -38,7 +41,11 @@ const startBenchmark = async () => { ); const serverUrl = `http://${server.host}:${server.port}`; - console.log(`Server is running at port: ${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(); @@ -48,12 +55,16 @@ const startBenchmark = async () => { // Option to open and test the Benchmark Test Suite in the browser manually if (process.env.MANUAL_BENCHMARK === 'true') { console.log( - `Open the Browser at '${serverUrl}' to run the tests manually.` + `${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) => { @@ -63,7 +74,17 @@ const startBenchmark = async () => { // 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) => { - console.log(...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 diff --git a/benchmark/yarn.lock b/benchmark/yarn.lock index 9c8ac2c3..16d56ebc 100644 --- a/benchmark/yarn.lock +++ b/benchmark/yarn.lock @@ -4,14 +4,6 @@ "@agile-ts/core@file:.yalc/@agile-ts/core": version "0.1.0" - dependencies: - "@agile-ts/logger" "^0.0.5" - "@agile-ts/utils" "^0.0.5" - -"@agile-ts/logger@^0.0.5": - version "0.0.5" - resolved "https://registry.yarnpkg.com/@agile-ts/logger/-/logger-0.0.5.tgz#bad39e1995a0c14e7f3f7c6e44c028f4d7a30f38" - integrity sha512-qBNUyPJGecOZOS9r8dyGF/VLBioEY5DYZn4Hoq+sEkpyvoi718c90i57B1M++I2MCCONVMGytG61Gs1sts73qw== dependencies: "@agile-ts/utils" "^0.0.5" @@ -123,6 +115,13 @@ agent-base@6: 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" @@ -149,6 +148,26 @@ buffer-crc32@~0.2.3: 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" @@ -258,6 +277,11 @@ hamt_plus@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" @@ -550,6 +574,13 @@ stack-utils@^2.0.3: 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" 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 8090cf6b..8fdd52f6 100644 --- a/examples/react/release/boxes/package.json +++ b/examples/react/release/boxes/package.json @@ -4,6 +4,7 @@ "private": true, "dependencies": { "@agile-ts/core": "file:.yalc/@agile-ts/core", + "@agile-ts/logger": "file:.yalc/@agile-ts/logger", "@agile-ts/proxytree": "file:.yalc/@agile-ts/proxytree", "@agile-ts/react": "file:.yalc/@agile-ts/react", "@chakra-ui/react": "^1.6.3", @@ -32,8 +33,8 @@ "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject", - "install:dev:agile": "yalc add @agile-ts/core @agile-ts/react @agile-ts/proxytree & yarn install", - "install:prod:agile": "yarn add @agile-ts/core @agile-ts/react @agile-ts/proxytree" + "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/components/EditProperties/index.tsx b/examples/react/release/boxes/src/components/EditProperties/index.tsx index b1caa3cd..3729f759 100644 --- a/examples/react/release/boxes/src/components/EditProperties/index.tsx +++ b/examples/react/release/boxes/src/components/EditProperties/index.tsx @@ -136,8 +136,8 @@ const Property = ({ id: number | string; }) => { const ELEMENT = core.ui.ELEMENTS.getItem(id); - const element = useProxy(ELEMENT, { componentId: 'Property' }); - console.log('Element', ELEMENT); + const element = useProxy(ELEMENT, { componentId: 'Property', deps: [id] }); + return ( = (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 e30b38a1..7fff41aa 100644 --- a/examples/react/release/boxes/yarn.lock +++ b/examples/react/release/boxes/yarn.lock @@ -3,28 +3,25 @@ "@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/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" + 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/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/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/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 4e6444b4..85c03544 100644 --- a/packages/core/src/agile.ts +++ b/packages/core/src/agile.ts @@ -13,9 +13,6 @@ import { Storages, CreateStorageConfigInterface, RegisterConfigInterface, - defineConfig, - Logger, - CreateLoggerConfigInterface, StateConfigInterface, flatMerge, LogCodeManager, @@ -45,14 +42,6 @@ export class Agile { // Integrations (UI-Frameworks) that are integrated into the Agile Instance public integrations: Integrations; - // Static Agile Logger with the default config - // (-> is overwritten by the last created Agile Instance) - static logger = new Logger({ - prefix: 'Agile', - active: true, - level: Logger.level.ERROR, - }); - // Identifier used to bind an Agile Instance globally static globalKey = '__agile__'; @@ -83,14 +72,14 @@ export class Agile { * @param config - Configuration object */ constructor(config: CreateAgileConfigInterface = {}) { - config = defineConfig(config, { - localStorage: true, + config = { + localStorage: false, waitForMount: true, - logConfig: {}, bindGlobal: false, autoIntegrate: true, bucket: true, - }); + ...config, + }; this.config = { waitForMount: config.waitForMount as any, bucket: config.bucket as any, @@ -105,10 +94,7 @@ export class Agile { localStorage: config.localStorage, }); - // Assign customized Logger config to the static Logger - this.configureLogger(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 @@ -119,24 +105,6 @@ export class Agile { } } - /** - * Configures the logging behaviour of AgileTs. - * - * @public - * @param config - Configuration object - */ - public configureLogger(config: CreateLoggerConfigInterface = {}): this { - config = defineConfig(config, { - prefix: 'Agile', - active: true, - level: Logger.level.SUCCESS, - canUseCustomStyles: true, - allowedTags: ['runtime', 'storage', 'subscription', 'multieditor'], - }); - Agile.logger = new Logger(config); - return this; - } - /** * Returns a newly created Storage. * @@ -337,17 +305,6 @@ export type AgileKey = string | number; export interface CreateAgileConfigInterface extends IntegrationsConfigInterface { - /** - * Configures the logging behaviour of AgileTs. - * @default { - prefix: 'Agile', - active: true, - level: Logger.level.WARN, - canUseCustomStyles: true, - allowedTags: ['runtime', 'storage', 'subscription', 'multieditor'], - } - */ - logConfig?: CreateLoggerConfigInterface; /** * Whether the Subscription Container shouldn't be ready * until the UI-Component it represents has been mounted. @@ -356,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; /** 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 937c5458..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 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 1efb0805..a7421b18 100644 --- a/packages/core/src/integrations/index.ts +++ b/packages/core/src/integrations/index.ts @@ -1,4 +1,4 @@ -import { Agile, defineConfig, Integration, LogCodeManager } from '../internal'; +import { Agile, Integration, LogCodeManager } from '../internal'; const onRegisterInitialIntegrationCallbacks: (( integration: Integration @@ -12,14 +12,14 @@ export class Integrations { public integrations: Set = new Set(); // External added Integrations - // that are to integrate into each created Agile Instance + // that are to integrate into not yet existing Agile Instances static initialIntegrations: Integration[] = []; /** - * Adds an external Integration to be registered in each Agile Instance created. + * Registers the specified Integration in each existing or not-yet created Agile Instance. * * @public - * @param integration - Integration to be registered in each Agile Instance created. + * @param integration - Integration to be registered in each Agile Instance. */ static addInitialIntegration(integration: Integration): void { if (integration instanceof Integration) { @@ -36,12 +36,15 @@ export class Integrations { * Fires on each external added Integration. * * @public - * @param callback - Callback to be fired when an Integration was added externally. + * @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); + }); } /** @@ -54,17 +57,13 @@ export class Integrations { * @param config - Configuration object */ constructor(agileInstance: Agile, config: IntegrationsConfigInterface = {}) { - config = defineConfig(config, { + config = { autoIntegrate: true, - }); + ...config, + }; this.agileInstance = () => agileInstance; if (config.autoIntegrate) { - // Integrate Integrations to be initially integrated - Integrations.initialIntegrations.forEach((integration) => { - this.integrate(integration); - }); - // Setup listener to be notified when an external registered Integration was added Integrations.onRegisterInitialIntegration((integration) => { this.integrate(integration); @@ -82,7 +81,11 @@ export class Integrations { public async integrate(integration: Integration): Promise { // Check if Integration is valid if (integration._key == null) { - LogCodeManager.log('18:03:00', [integration._key], integration); + LogCodeManager.log( + '18:03:00', + [integration._key, this.agileInstance().key], + integration + ); return false; } @@ -95,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; } diff --git a/packages/core/src/internal.ts b/packages/core/src/internal.ts index ad6631d3..b9356568 100644 --- a/packages/core/src/internal.ts +++ b/packages/core/src/internal.ts @@ -9,7 +9,6 @@ export * from './utils'; export * from '@agile-ts/utils'; // Logger -export * from '@agile-ts/logger'; export * from './logCodeManager'; // Agile diff --git a/packages/core/src/logCodeManager.ts b/packages/core/src/logCodeManager.ts index d90deb35..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. @@ -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 7dec2575..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) @@ -124,10 +123,16 @@ export class Runtime { this.isPerformingJobs = false; if (this.jobsToRerender.length > 0) { if (this.agileInstance().config.bucket) { - // https://stackoverflow.com/questions/9083594/call-settimeout-without-delay - setTimeout(() => { - this.updateSubscribers(); - }); + // 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(); } } @@ -176,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; @@ -230,7 +239,7 @@ export class Runtime { job.subscriptionContainersToUpdate.delete(subscriptionContainer); }); - }); + } return Array.from(subscriptionsToUpdate); } @@ -247,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(); @@ -260,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 index b3d91305..79d792d3 100644 --- a/packages/core/src/shared.ts +++ b/packages/core/src/shared.ts @@ -7,10 +7,8 @@ import { CreateComputedConfigInterface, CreateStorageConfigInterface, DefaultItem, - defineConfig, DependableAgileInstancesType, flatMerge, - Logger, removeProperties, runsOnServer, State, @@ -23,7 +21,6 @@ import { */ let sharedAgileInstance = new Agile({ key: 'shared', - logConfig: { prefix: 'Agile', level: Logger.level.ERROR, active: true }, localStorage: !runsOnServer(), }); export { sharedAgileInstance as shared }; @@ -76,9 +73,10 @@ export function createState( initialValue: ValueType, config: CreateStateConfigInterfaceWithAgile = {} ): State { - config = defineConfig(config, { + config = { agileInstance: sharedAgileInstance, - }); + ...config, + }; return new State( config.agileInstance as any, initialValue, @@ -172,9 +170,10 @@ export function createComputed( if (configOrDeps) _config = configOrDeps; } - _config = defineConfig(_config, { + _config = { agileInstance: sharedAgileInstance, - }); + ..._config, + }; return new Computed( _config.agileInstance as any, @@ -186,7 +185,7 @@ export function createComputed( export interface CreateAgileSubInstanceInterface { /** * Instance of Agile the Instance belongs to. - * @default Agile.shared + * @default sharedAgileInstance */ agileInstance?: Agile; } 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/tests/unit/agile.test.ts b/packages/core/tests/unit/agile.test.ts index 136702ae..2d0dc29e 100644 --- a/packages/core/tests/unit/agile.test.ts +++ b/packages/core/tests/unit/agile.test.ts @@ -7,7 +7,6 @@ import { Storage, Computed, Collection, - Logger, Storages, } from '../../src'; import testIntegration from '../helper/test.integration'; @@ -77,7 +76,6 @@ describe('Agile Tests', () => { // Reset globalThis globalThis[Agile.globalKey] = undefined; - jest.spyOn(Agile.prototype, 'configureLogger'); jest.spyOn(Agile.prototype, 'integrate'); jest.clearAllMocks(); @@ -100,10 +98,9 @@ describe('Agile Tests', () => { expect(SubControllerMock).toHaveBeenCalledWith(agile); expect(agile.subController).toBeInstanceOf(SubController); expect(StoragesMock).toHaveBeenCalledWith(agile, { - localStorage: true, + localStorage: false, }); expect(agile.storages).toBeInstanceOf(Storages); - expect(agile.configureLogger).toHaveBeenCalledWith({}); // Check if Agile Instance got bound globally expect(globalThis[Agile.globalKey]).toBeUndefined(); @@ -113,13 +110,7 @@ describe('Agile Tests', () => { const agile = new Agile({ waitForMount: false, bucket: false, - localStorage: false, - logConfig: { - level: Logger.level.DEBUG, - active: false, - prefix: 'Jeff', - timestamp: true, - }, + localStorage: true, bindGlobal: true, key: 'jeff', autoIntegrate: false, @@ -139,15 +130,9 @@ describe('Agile Tests', () => { expect(SubControllerMock).toHaveBeenCalledWith(agile); expect(agile.subController).toBeInstanceOf(SubController); expect(StoragesMock).toHaveBeenCalledWith(agile, { - localStorage: false, + localStorage: true, }); expect(agile.storages).toBeInstanceOf(Storages); - expect(agile.configureLogger).toHaveBeenCalledWith({ - active: false, - level: Logger.level.DEBUG, - prefix: 'Jeff', - timestamp: true, - }); // Check if Agile Instance got bound globally expect(globalThis[Agile.globalKey]).toBe(agile); @@ -182,31 +167,6 @@ describe('Agile Tests', () => { jest.clearAllMocks(); // Because creating the Agile Instance calls some mocks }); - describe('configureLogger function tests', () => { - it('should overwrite the static Logger with a new Logger Instance', () => { - Agile.logger.config = 'outdated' as any; - - agile.configureLogger({ - active: true, - level: 0, - }); - - expect(Agile.logger.config).toStrictEqual({ - canUseCustomStyles: true, - level: 0, - prefix: 'Agile', - timestamp: false, - }); - expect(Agile.logger.isActive).toBeTruthy(); - expect(Agile.logger.allowedTags).toStrictEqual([ - 'runtime', - 'storage', - 'subscription', - 'multieditor', - ]); - }); - }); - describe('createStorage function tests', () => { beforeEach(() => { jest.spyOn(Shared, 'createStorage'); diff --git a/packages/core/tests/unit/integrations/integrations.test.ts b/packages/core/tests/unit/integrations/integrations.test.ts index b8550a22..a7598d61 100644 --- a/packages/core/tests/unit/integrations/integrations.test.ts +++ b/packages/core/tests/unit/integrations/integrations.test.ts @@ -38,11 +38,12 @@ describe('Integrations Tests', () => { 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 (specific config)', () => { + it('should create Integrations without the before specified initial Integrations (autoIntegrate = false)', () => { Integrations.initialIntegrations = [dummyIntegration1, dummyIntegration2]; const integrations = new Integrations(dummyAgile, { autoIntegrate: false }); @@ -61,9 +62,27 @@ describe('Integrations Tests', () => { }); describe('onRegisterInitialIntegration function tests', () => { - it('should register specified onRegisterInitialIntegration callback', () => { - // Nothing to testable + let callback; + beforeEach(() => { + callback = jest.fn(); }); + + 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', () => { @@ -74,6 +93,7 @@ describe('Integrations Tests', () => { Integrations.onRegisterInitialIntegration(callback1); Integrations.onRegisterInitialIntegration(callback2); }); + it( 'should add valid Integration to the initialIntegrations array ' + 'and fire the onRegisterInitialIntegration callbacks', @@ -149,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/runtime.test.ts b/packages/core/tests/unit/runtime/runtime.test.ts index c75e3893..5bcc3014 100644 --- a/packages/core/tests/unit/runtime/runtime.test.ts +++ b/packages/core/tests/unit/runtime/runtime.test.ts @@ -31,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', () => { @@ -112,10 +113,11 @@ describe('Runtime Tests', () => { it( "should perform specified Job and all remaining Jobs in the 'jobQueue' " + - "and call 'updateSubscribers' in a setTimeout " + + "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); @@ -131,10 +133,12 @@ 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(); }); } ); @@ -145,15 +149,35 @@ describe('Runtime Tests', () => { '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); + 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); @@ -185,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/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/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/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 becea846..c76c0393 100644 --- a/packages/react/src/hooks/useIsomorphicLayoutEffect.ts +++ b/packages/react/src/hooks/useIsomorphicLayoutEffect.ts @@ -10,6 +10,6 @@ import { runsOnServer } from '@agile-ts/core'; // is created synchronously, otherwise a store update may occur before the // subscription is created and an inconsistent state may be observed -export const useIsomorphicLayoutEffect = runsOnServer() +export const useIsomorphicLayoutEffect = !runsOnServer() ? useLayoutEffect : useEffect; 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/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"