├── main.ts ├── src ├── guides │ ├── Benchmarks.ts │ ├── BasicFeatures.ts │ ├── CycleResolver.ts │ ├── AdvancedFeatures.ts │ └── Benchmarks.md ├── class │ ├── BetterMixin.ts │ ├── RequiredProperty.ts │ └── Base.ts ├── util │ ├── Uniqable.ts │ ├── LeveledQueue.ts │ └── Helpers.ts ├── event │ ├── Hook.ts │ └── Event.ts ├── chrono │ ├── CycleResolver.ts │ ├── Revision.ts │ └── TransactionCycleDetectionWalkContext.ts ├── graph │ └── Graph.ts ├── schema │ ├── Field.ts │ ├── Schema.ts │ └── EntityMeta.ts ├── replica │ ├── Replica.ts │ └── Identifier.ts ├── environment │ └── Debug.ts ├── collection │ └── Collection.ts ├── visualization │ └── Cytoscape.ts └── lab │ ├── Meta.ts │ ├── TreeNode.ts │ ├── LinkedList.ts │ └── TreeNodeReference.ts ├── tests ├── pages │ ├── cytoscape.css │ └── cytoscape.html ├── benchmark │ ├── suite.html │ ├── compact │ │ ├── compact.html │ │ └── compact.ts │ ├── events │ │ └── events.html │ ├── suite.ts │ ├── walk_depth │ │ ├── data.ts │ │ └── walk_depth.ts │ ├── allocation.ts │ ├── memory_leak.ts │ ├── shallow_changes.ts │ └── deep_changes.ts ├── util │ └── uniqable.t.ts ├── index.html ├── replica │ ├── 033_cycle_info.t.ts │ ├── 025_tree_node.t.ts │ ├── 002_fields.t.ts │ ├── 040_calculate_only.t.ts │ └── 001_entity.t.ts ├── graph │ ├── 020_node.t.ts │ └── 010_walkable.t.ts ├── chrono │ ├── 032_propagation_options.t.ts │ ├── 030_iteration.t.ts │ ├── 033_cycle_info.t.ts │ ├── 050_undo_redo.t.ts │ ├── 040_add_remove.t.ts │ ├── 031_garbage_collection.t.ts │ ├── 030_transaction_reject.t.ts │ ├── 013_sync_calculation.t.ts │ ├── 013_async_calculation.t.ts │ ├── 014_parallel_propagation.t.ts │ ├── 015_listeners.t.ts │ ├── 032_commit_async.t.ts │ └── 030_propagation_2.t.ts ├── schema │ └── 010_schema.t.ts ├── visualization │ └── 010_replica.t.ts ├── collection │ └── 010_chained_iterator.t.ts ├── cycle_resolver │ ├── 010_memoizing.t.ts │ ├── 060_sedwu_fixed_units.t.ts │ └── 040_sedwu_fixed_duration_effort_driven.t.ts ├── event │ └── events.t.ts └── index.ts ├── examples └── basic │ ├── cytoscape.css │ ├── index.ts │ └── index.html ├── index.js ├── scripts ├── build.sh ├── util.sh ├── make_dist.sh ├── publish_docs.sh ├── changelog.js ├── build_docs.sh └── publish.sh ├── benchmarks ├── memory_usage.html ├── generators_overhead_2.js ├── map_creation.js ├── map_indexing.js ├── generators_overhead.js ├── fast_addition_set.js ├── memory_usage.js ├── lazy_property.js ├── object_creation.js └── prototype-compactification.js ├── .gitignore ├── tsconfig.json ├── LICENSE.md ├── package.json ├── CHANGELOG.md ├── tslint.json ├── README.md └── docs_src └── README.md /main.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | } 3 | -------------------------------------------------------------------------------- /src/guides/Benchmarks.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * [[include:Benchmarks.md]] 3 | */ 4 | export const BenchmarksGuide = '' 5 | -------------------------------------------------------------------------------- /src/class/BetterMixin.ts: -------------------------------------------------------------------------------- 1 | // backward compat 2 | export * from "./Mixin.js" 3 | export { Base } from "./Base.js" 4 | -------------------------------------------------------------------------------- /src/guides/BasicFeatures.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * [[include:BasicFeatures.md]] 3 | */ 4 | export const BasicFeaturesGuide = '' 5 | -------------------------------------------------------------------------------- /src/guides/CycleResolver.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * [[include:CycleResolver.md]] 3 | */ 4 | export const CycleResolverGuide = '' 5 | -------------------------------------------------------------------------------- /src/guides/AdvancedFeatures.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * [[include:AdvancedFeatures.md]] 3 | */ 4 | export const AdvancedFeaturesGuide = '' 5 | -------------------------------------------------------------------------------- /tests/pages/cytoscape.css: -------------------------------------------------------------------------------- 1 | #graph { 2 | width: 100%; 3 | height: 100%; 4 | position: relative; 5 | display: block; 6 | } 7 | -------------------------------------------------------------------------------- /examples/basic/cytoscape.css: -------------------------------------------------------------------------------- 1 | #graph { 2 | width: 100%; 3 | height: 100%; 4 | position: relative; 5 | display: block; 6 | } 7 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Set options as a parameter, environment variable, or rc file. 2 | require = require("esm")(module/*, options*/) 3 | module.exports = require("./main.js") 4 | 5 | 6 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # exit if any of command has failed 4 | set -e 5 | 6 | DIR="$( cd "$( dirname "$0" )" && pwd )" 7 | 8 | cd "$DIR/.." 9 | 10 | npx tsc 11 | -------------------------------------------------------------------------------- /tests/benchmark/suite.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /benchmarks/memory_usage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/benchmark/compact/compact.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/benchmark/events/events.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /scripts/util.sh: -------------------------------------------------------------------------------- 1 | git_repo_has_changes() ( 2 | ( 3 | cd "$1" 4 | 5 | if git diff-index --quiet HEAD --; then 6 | echo 'false' 7 | else 8 | echo 'true' 9 | fi 10 | ) 11 | ) 12 | -------------------------------------------------------------------------------- /benchmarks/generators_overhead_2.js: -------------------------------------------------------------------------------- 1 | const generatorFunc = function * (a) { return a + 1 } 2 | 3 | const N = 5000 4 | 5 | //---------------------------------- 6 | let res = 0 7 | 8 | for (let i = 0; i < N; i++) { 9 | res += generatorFunc(i).next().value 10 | } 11 | -------------------------------------------------------------------------------- /tests/benchmark/suite.ts: -------------------------------------------------------------------------------- 1 | import { runAllGraphPopulation } from "./allocation.js" 2 | import { runAllDeepChanges } from "./deep_changes.js" 3 | import { runAllShallowChanges } from "./shallow_changes.js" 4 | 5 | export const runAll = async () => { 6 | await runAllDeepChanges() 7 | await runAllShallowChanges() 8 | await runAllGraphPopulation() 9 | 10 | // await runAllMemoryLeak() 11 | } 12 | 13 | runAll() 14 | -------------------------------------------------------------------------------- /benchmarks/map_creation.js: -------------------------------------------------------------------------------- 1 | const nestedMap = new Map() 2 | 3 | const distinctMap1 = new Map() 4 | const distinctMap2 = new Map() 5 | 6 | 7 | const count = 100000 8 | 9 | 10 | for (let i = 0; i < count; i++) { 11 | nestedMap.set(i, { value : i, nested : { value : i + 1 } }) 12 | } 13 | 14 | 15 | 16 | for (let i = 0; i < count; i++) { 17 | distinctMap1.set(i, { value : i }) 18 | distinctMap2.set(i, { value : i + 1 }) 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE related files 2 | /.idea 3 | 4 | # dependencies 5 | /node_modules 6 | 7 | # distribution files 8 | /DIST 9 | /DIST_DOCS 10 | 11 | 12 | # files generated by TypeScript 13 | /documentation/**/*.js 14 | /documentation/**/*.js.map 15 | /src/**/*.js 16 | /src/**/*.js.map 17 | /tests/**/*.js 18 | /tests/**/*.js.map 19 | /main.js 20 | /tsconfig.tsbuildinfo 21 | 22 | /examples/basic/**/*.js 23 | 24 | # docs 25 | /docs 26 | 27 | # misc local files 28 | /misc 29 | 30 | # Libre Office lock files 31 | .~lock* 32 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions" : { 3 | // "importHelpers" : true, 4 | "experimentalDecorators" : true, 5 | 6 | "target" : "ES2017", 7 | 8 | "sourceMap" : false, 9 | "lib" : [ 10 | "esnext" 11 | ], 12 | "module" : "ESNext", 13 | "moduleResolution" : "Node" 14 | }, 15 | 16 | "include" : [ 17 | "main.ts", 18 | "src/**/*.ts", 19 | "tests/**/*.ts", 20 | "examples/**/*.ts" 21 | ], 22 | 23 | "exclude" : [ 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /tests/util/uniqable.t.ts: -------------------------------------------------------------------------------- 1 | import { MIN_SMI } from "../../src/util/Helpers.js" 2 | import { compact, Uniqable } from "../../src/util/Uniqable.js" 3 | 4 | declare const StartTest : any 5 | 6 | StartTest(t => { 7 | 8 | const getUniqable = () => { return { uniqable : MIN_SMI } } 9 | 10 | t.it('Compacting arrays should work', async t => { 11 | const el1 = getUniqable() 12 | const el2 = getUniqable() 13 | const el3 = getUniqable() 14 | 15 | const elements : Uniqable[] = [ el1, el2, el1, el3, el1, el2, el3 ] 16 | 17 | compact(elements) 18 | 19 | t.isDeeply(elements, [ el1, el2, el3 ]) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /scripts/make_dist.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # exit if any of command has failed 4 | set -e 5 | 6 | DIR="$( cd "$( dirname "$0" )" && pwd )" 7 | . "$DIR"/util.sh 8 | 9 | if [[ $(git_repo_has_changes "$DIR/..") == 'true' ]]; then 10 | echo ">>Repository has changes, aborting making distribution" 11 | exit 1 12 | fi 13 | 14 | DIST="$DIR/../DIST" 15 | 16 | rm -rf "$DIST" 17 | 18 | git worktree prune 19 | 20 | git worktree add "$DIST" --no-checkout --detach 21 | 22 | ( 23 | cd "$DIST" 24 | 25 | git checkout HEAD 26 | 27 | rm -rf "$DIST/.git" "$DIST/benchmarks" "$DIST/scripts/make_dist.sh" 28 | 29 | ln -s "$DIR/../node_modules" "node_modules" 30 | ) 31 | 32 | -------------------------------------------------------------------------------- /benchmarks/map_indexing.js: -------------------------------------------------------------------------------- 1 | const mapByRefs = new Map() 2 | const mapByInts = new Map() 3 | 4 | const refs = []; 5 | 6 | [...new Array(10000)].forEach((value, index) => { 7 | const ref = { value : Math.random() * 1e10, intKey : index } 8 | 9 | refs.push(ref) 10 | 11 | mapByRefs.set(ref, ref.value) 12 | mapByInts.set(index, ref.value) 13 | }) 14 | 15 | 16 | let sum1 = 0 17 | let sum2 = 0 18 | 19 | let ref 20 | 21 | for (let i = 0; i < 50000; i++) { 22 | ref = refs[ i % 10000 ] 23 | 24 | sum1 += mapByInts.get(ref.intKey) 25 | } 26 | 27 | for (let i = 0; i < 50000; i++) { 28 | ref = refs[ i % 10000 ] 29 | 30 | sum2 += mapByRefs.get(ref) 31 | } 32 | -------------------------------------------------------------------------------- /scripts/publish_docs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # exit if any of command has failed 4 | set -e 5 | 6 | DIR="$( cd "$( dirname "$0" )" && pwd )" 7 | DOCS="$( cd "$( dirname "$1" )" && pwd )" 8 | 9 | if [[ -z $DOCS ]]; then 10 | echo ">>No path to docs given" 11 | 12 | exit 1 13 | fi 14 | 15 | DIST="$DIR/../DIST_DOCS" 16 | 17 | rm -rf "$DIST" 18 | 19 | git worktree prune 20 | 21 | git worktree add "$DIST" gh-pages 22 | 23 | cd $DIST 24 | 25 | git pull 26 | 27 | rm -rf "$DIST/docs" 28 | 29 | cp -r "$DOCS" "$DIST/docs" 30 | 31 | git commit -a -m "Doc updated" || true 32 | 33 | git push 34 | 35 | git worktree remove "$DIST" 36 | 37 | echo ">>Successfully updated github pages" 38 | -------------------------------------------------------------------------------- /benchmarks/generators_overhead.js: -------------------------------------------------------------------------------- 1 | const regularHelper = function (a) { return a * 2 } 2 | const generatorHelper = function * (a) { return a * 2 } 3 | 4 | const regularFunc = function (a) { return a + regularHelper(a) } 5 | const generatorFunc = function * (a) { return a + (yield* generatorHelper(a)) } 6 | 7 | const N = 1000 8 | 9 | //---------------------------------- 10 | let res = 0 11 | 12 | for (let i = 0; i < N; i++) { 13 | res += regularFunc(0) 14 | } 15 | 16 | //---------------------------------- 17 | let res = 0 18 | 19 | for (let i = 0; i < N; i++) { 20 | res += generatorFunc(0).next().value 21 | } 22 | -------------------------------------------------------------------------------- /benchmarks/fast_addition_set.js: -------------------------------------------------------------------------------- 1 | class FastAdditionSet { 2 | tempStorage = [] 3 | 4 | internalSet 5 | 6 | 7 | add (el) { 8 | if (this.internalSet) { 9 | return this.internalSet.add(el) 10 | } 11 | 12 | this.tempStorage.push(el) 13 | } 14 | 15 | 16 | asSet () { 17 | if (this.internalSet) return this.internalSet 18 | 19 | return this.internalSet = new Set(this.tempStorage) 20 | } 21 | 22 | } 23 | 24 | const regularSet = new Set() 25 | const fastAdditionSet = new FastAdditionSet() 26 | 27 | for (let i = 0; i < 1000; i++) { 28 | regularSet.add(i) 29 | } 30 | 31 | regularSet.has(18) 32 | 33 | 34 | for (let i = 0; i < 1000; i++) { 35 | fastAdditionSet.add(i) 36 | } 37 | 38 | fastAdditionSet.asSet().has(18) 39 | -------------------------------------------------------------------------------- /src/class/RequiredProperty.ts: -------------------------------------------------------------------------------- 1 | import { DEBUG_ONLY } from "../environment/Debug.js" 2 | 3 | const RequiredProperties = Symbol('RequiredProperties') 4 | 5 | const emptyFn = () => undefined 6 | 7 | export const required : PropertyDecorator = DEBUG_ONLY((proto : object, propertyKey : string | symbol) : void => { 8 | let required = proto[ RequiredProperties ] 9 | 10 | if (!required) required = proto[ RequiredProperties ] = [] 11 | 12 | required.push(propertyKey) 13 | }) 14 | 15 | 16 | export const validateRequiredProperties = DEBUG_ONLY((context : any) => { 17 | const required = context[ RequiredProperties ] 18 | 19 | if (required) { 20 | for (let i = 0; i < required.length; i++) 21 | if (context[ required[ i ] ] === undefined) throw new Error(`Required attribute [${ String(required[ i ]) }] is not provided`) 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /src/util/Uniqable.ts: -------------------------------------------------------------------------------- 1 | import { MIN_SMI } from "./Helpers.js" 2 | 3 | export interface Uniqable { 4 | uniqable : number 5 | } 6 | 7 | 8 | let UNIQABLE : number = MIN_SMI 9 | 10 | 11 | // in-place mutation 12 | export const compact = (array : Uniqable[]) => { 13 | const uniqableId : number = ++UNIQABLE 14 | 15 | let uniqueIndex : number = -1 16 | 17 | for (let i = 0; i < array.length; ++i) { 18 | const element : Uniqable = array[ i ] 19 | 20 | if (element.uniqable !== uniqableId) { 21 | element.uniqable = uniqableId 22 | 23 | ++uniqueIndex 24 | 25 | if (uniqueIndex !== i) array[ uniqueIndex ] = element 26 | } 27 | } 28 | 29 | // assuming its better to not touch the array's `length` property 30 | // unless we really have to 31 | if (array.length !== uniqueIndex + 1) array.length = uniqueIndex + 1 32 | } 33 | -------------------------------------------------------------------------------- /examples/basic/index.ts: -------------------------------------------------------------------------------- 1 | import { ChronoGraph } from "../../src/chrono/Graph.js" 2 | import { CytoscapeWrapper } from "../../src/visualization/Cytoscape.js" 3 | 4 | declare const window, document : any 5 | 6 | const graph : ChronoGraph = ChronoGraph.new() 7 | 8 | const i1 = graph.variableNamed('i1', 0) 9 | const i2 = graph.variableNamed('i2', 10) 10 | const i3 = graph.variableNamed('i3', 0) 11 | 12 | const c1 = graph.identifierNamed('c1', function* () { 13 | return (yield i1) + (yield i2) 14 | }) 15 | 16 | const c2 = graph.identifierNamed('c2', function* () { 17 | return (yield c1) + 1 18 | }) 19 | 20 | const c3 = graph.identifierNamed('c3', function* () { 21 | return (yield c2) + (yield i3) 22 | }) 23 | 24 | graph.commit() 25 | 26 | 27 | window.addEventListener('load', () => { 28 | const wrapper = CytoscapeWrapper.new({ graph }) 29 | 30 | wrapper.renderTo(document.getElementById('graph')) 31 | }) 32 | 33 | -------------------------------------------------------------------------------- /benchmarks/memory_usage.js: -------------------------------------------------------------------------------- 1 | const count = 100000 2 | 3 | class BenchMapByReference { 4 | nodes = new Map() 5 | } 6 | class BenchMapByInteger { 7 | nodes = new Map() 8 | } 9 | 10 | const mapByRefs = window.mapByRefs = new BenchMapByReference() 11 | const mapByInts = window.mapByInts = new BenchMapByInteger() 12 | 13 | ;[ ...memory ].forEach((_, index) => { 14 | const value = { a : Math.random() * 1e10 } 15 | const ref = { intKey : index } 16 | 17 | mapByRefs.map.set(ref, value) 18 | 19 | // memory[ index ] = ref 20 | // mapByInts.map.set(index, ref) 21 | }) 22 | 23 | console.log("Populated both maps") 24 | 25 | // let sum1 = 0 26 | // let sum2 = 0 27 | // 28 | // let ref 29 | // 30 | // for (let i = 0; i < 50000; i++) { 31 | // ref = refs[ i % 10000 ] 32 | // 33 | // sum1 += mapByInts.get(ref.intKey) 34 | // } 35 | // 36 | // for (let i = 0; i < 50000; i++) { 37 | // ref = refs[ i % 10000 ] 38 | // 39 | // sum2 += mapByRefs.get(ref) 40 | // } 41 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /tests/pages/cytoscape.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /src/event/Hook.ts: -------------------------------------------------------------------------------- 1 | //--------------------------------------------------------------------------------------------------------------------- 2 | export type Listener = (...payload : Payload) => any 3 | 4 | export type Disposer = () => any 5 | 6 | //--------------------------------------------------------------------------------------------------------------------- 7 | export class Hook { 8 | hooks : Listener [] = [] 9 | 10 | 11 | on (listener : Listener) : Disposer { 12 | this.hooks.push(listener) 13 | 14 | return () => this.un(listener) 15 | } 16 | 17 | 18 | un (listener : Listener) { 19 | const index = this.hooks.indexOf(listener) 20 | 21 | if (index !== -1) this.hooks.splice(index, 1) 22 | } 23 | 24 | 25 | trigger (...payload : Payload) { 26 | const listeners = this.hooks.slice() 27 | 28 | for (let i = 0; i < listeners.length; ++i) { 29 | listeners[ i ](...payload) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/basic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2020 Bryntum 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/benchmark/walk_depth/data.ts: -------------------------------------------------------------------------------- 1 | import { Base } from "../../../src/class/Base.js" 2 | 3 | //--------------------------------------------------------------------------------------------------------------------- 4 | export class Node extends Base { 5 | count : number = 0 6 | 7 | outgoing : Node[] = [] 8 | } 9 | 10 | 11 | //--------------------------------------------------------------------------------------------------------------------- 12 | export type GraphGenerationResult = { nodes : Node[] } 13 | 14 | 15 | //--------------------------------------------------------------------------------------------------------------------- 16 | export const deepGraphGen = (nodesNum : number = 1000, edgesNum : number = 5) : GraphGenerationResult => { 17 | const nodes : Node[] = [] 18 | 19 | for (let i = 0; i < nodesNum; i++) nodes.push(Node.new({ count : i })) 20 | 21 | for (let i = 0; i < nodesNum; i++) { 22 | 23 | for (let k = i + 1; k < i + edgesNum + 1 && k < nodesNum; k++) { 24 | nodes[ i ].outgoing.push(nodes[ k ]) 25 | } 26 | } 27 | 28 | return { nodes } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /scripts/changelog.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | const prependZero = (int, minLength) => { 5 | const str = String(int) 6 | 7 | return '0000000'.slice(0, minLength - str.length) + str 8 | } 9 | 10 | const now = new Date() 11 | const version = require('../package.json').version 12 | const versionStr = `${version} ${now.getFullYear()}-${ prependZero(now.getMonth() + 1, 2) }-${ prependZero(now.getDate(), 2) } ${ prependZero(now.getHours(), 2) }:${ prependZero(now.getMinutes(), 2) }` 13 | 14 | const updateVersion = () => { 15 | let changelog = fs.readFileSync('CHANGELOG.md', 'utf8') 16 | 17 | changelog = changelog.replace(/\{\{ \$NEXT \}\}/m, versionStr) 18 | 19 | fs.writeFileSync('CHANGELOG.md', changelog, 'utf8') 20 | } 21 | 22 | 23 | const updateVersionAndStartNew = () => { 24 | let changelog = fs.readFileSync('CHANGELOG.md', 'utf8') 25 | 26 | changelog = changelog.replace(/\{\{ \$NEXT \}\}/m, `{{ $NEXT }}\n\n${versionStr}`) 27 | 28 | fs.writeFileSync('CHANGELOG.md', changelog, 'utf8') 29 | } 30 | 31 | module.exports = { 32 | updateVersion, 33 | updateVersionAndStartNew 34 | } 35 | -------------------------------------------------------------------------------- /tests/replica/033_cycle_info.t.ts: -------------------------------------------------------------------------------- 1 | import { Base } from "../../src/class/Base.js" 2 | import { calculate, Entity, field } from "../../src/replica/Entity.js" 3 | import { Replica } from "../../src/replica/Replica.js" 4 | 5 | declare const StartTest : any 6 | 7 | StartTest(t => { 8 | 9 | t.it('Should show the detailed information about the cyclic computation', async t => { 10 | class Some extends Entity.mix(Base) { 11 | @field() 12 | iden1 : string 13 | 14 | @field() 15 | iden2 : string 16 | 17 | @calculate('iden1') 18 | * calculateIden1 () { 19 | return yield this.$.iden2 20 | } 21 | 22 | @calculate('iden2') 23 | * calculateIden2 () { 24 | return yield this.$.iden1 25 | } 26 | } 27 | 28 | const replica : Replica = Replica.new({ autoCommit : false }) 29 | 30 | const some = Some.new() 31 | 32 | replica.addEntity(some) 33 | 34 | // replica.read(some.$.iden1) 35 | 36 | // ---------------- 37 | t.throwsOk(() => replica.read(some.$.iden1), /iden1.*iden2/s, 'Include identifier name in the cycle info') 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /scripts/build_docs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # exit if any of command has failed 4 | set -e 5 | 6 | DIR="$( cd "$( dirname "$0" )" && pwd )" 7 | 8 | cd "$DIR/.." 9 | 10 | rm -rf "docs" 11 | 12 | npx typedoc --readme "docs_src/README.md" --includes 'src/guides' --out docs --exclude 'tests/**/*' --exclude 'documentation/**/*' --exclude 'main.ts' --exclude 'src/lab/**/*' --excludeNotDocumented --listInvalidSymbolLinks --theme node_modules/typedoc-default-themes/bin/default/ 13 | 14 | cp -f "docs_src/README.md" "README.md" 15 | 16 | sed -i -e 's!\[\[BasicFeaturesGuide[|]Basic features\]\]![Basic features](https://bryntum.github.io/chronograph/docs/modules/_src_guides_basicfeatures_.html#basicfeaturesguide)!' "README.md" 17 | sed -i -e 's!\[\[AdvancedFeaturesGuide[|]Advanced features\]\]![Advanced features](https://bryntum.github.io/chronograph/docs/modules/_src_guides_advancedfeatures_.html#advancedfeaturesguide)!' "README.md" 18 | sed -i -e 's!\[API docs\][(]\./globals.html[)]![API docs](https://bryntum.github.io/chronograph/docs/)!' "README.md" 19 | sed -i -e 's!\[\[BenchmarksGuide[|]Benchmarks\]\]![Benchmarks](https://bryntum.github.io/chronograph/docs/modules/_src_guides_benchmarks_.html#benchmarksguide)!' "README.md" 20 | 21 | sed -i -e 's!!!' "README.md" 22 | -------------------------------------------------------------------------------- /scripts/publish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # exit if any of command has failed 4 | set -e 5 | 6 | DIR="$( cd "$( dirname "$0" )" && pwd )" 7 | . "$DIR"/util.sh 8 | 9 | if [[ $(git_repo_has_changes "$DIR/..") == 'true' ]]; then 10 | echo ">>Repository has changes, aborting release" 11 | exit 1 12 | fi 13 | 14 | DIST="$DIR/../DIST" 15 | 16 | "$DIR"/make_dist.sh 17 | 18 | cd $DIST 19 | 20 | # prepare the dist 21 | scripts/build.sh 22 | 23 | # run suite in node 24 | npx siesta ./tests || (echo ">>Test suite failed, aborting release" && false) 25 | 26 | # publish 27 | scripts/build_docs.sh 28 | 29 | if [[ -z "$V" ]]; then 30 | V="patch" 31 | fi 32 | 33 | # bump version in distribution - won't be refelected in main repo, since "make_dist" removes the ".git" 34 | npm version $V 35 | 36 | node -e "require(\"./scripts/changelog.js\").updateVersion()" 37 | 38 | echo "" > .npmignore 39 | 40 | npm publish --access public 41 | 42 | # post-publish, update the main repo 43 | cd "$DIR/.." 44 | 45 | # bump version in main repo 46 | npm version $V 47 | 48 | node -e "require(\"./scripts/changelog.js\").updateVersionAndStartNew()" 49 | 50 | git add CHANGELOG.md 51 | git commit -m "Updated changelog" 52 | 53 | git push 54 | 55 | # the trailing dot is required 56 | "$DIR"/publish_docs.sh "$DIST/docs/." 57 | -------------------------------------------------------------------------------- /src/chrono/CycleResolver.ts: -------------------------------------------------------------------------------- 1 | import { CycleResolutionInput, Variable } from "../cycle_resolver/CycleResolver.js" 2 | import { HasProposedNotPreviousValue, PreviousValueOf } from "./Effect.js" 3 | import { Identifier } from "./Identifier.js" 4 | import { SyncEffectHandler } from "./Transaction.js" 5 | 6 | 7 | //--------------------------------------------------------------------------------------------------------------------- 8 | /** 9 | * A subclass of [[CycleResolutionInput]] with additional convenience method [[collectInfo]]. 10 | */ 11 | export class CycleResolutionInputChrono extends CycleResolutionInput { 12 | 13 | /** 14 | * This method, given an effect handler, identifier and a variable, will add [[CycleResolutionInput.addPreviousValueFlag|previous value]] 15 | * and [[CycleResolutionInput.addProposedValueFlag|proposed value]] flags for that variable. 16 | * 17 | * @param Y An effect handler function, which is given as a 1st argument of every calculation function 18 | * @param identifier 19 | * @param symbol 20 | */ 21 | collectInfo (Y : SyncEffectHandler, identifier : Identifier, symbol : Variable) { 22 | if (Y(PreviousValueOf(identifier)) != null) this.addPreviousValueFlag(symbol) 23 | 24 | if (Y(HasProposedNotPreviousValue(identifier))) this.addProposedValueFlag(symbol) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/graph/020_node.t.ts: -------------------------------------------------------------------------------- 1 | import { Base } from "../../src/class/Base.js" 2 | import { Node, WalkForwardContext } from "../../src/graph/Node.js" 3 | 4 | declare const StartTest : any 5 | 6 | class WalkerNode extends Node.derive(Base) { 7 | id : number 8 | NodeT : WalkerNode 9 | } 10 | 11 | 12 | StartTest(t => { 13 | 14 | t.it('Minimal walk forward with "duplex" nodes', t => { 15 | const node5 = WalkerNode.new({ id : 5 }) 16 | const node4 = WalkerNode.new({ id : 4 }) 17 | const node3 = WalkerNode.new({ id : 3 }) 18 | const node2 = WalkerNode.new({ id : 2 }) 19 | const node1 = WalkerNode.new({ id : 1 }) 20 | 21 | node3.addEdgeTo(node5) 22 | node4.addEdgeTo(node3) 23 | node2.addEdgeTo(node4) 24 | node2.addEdgeTo(node3) 25 | node1.addEdgeTo(node2) 26 | 27 | const walkPath = [] 28 | const topoPath = [] 29 | 30 | WalkForwardContext.new({ 31 | onNode : (node : WalkerNode) => { 32 | walkPath.push(node.id) 33 | }, 34 | 35 | onTopologicalNode : (node : WalkerNode) => { 36 | topoPath.push(node.id) 37 | } 38 | }).startFrom([ node1 ]) 39 | 40 | t.isDeeply(walkPath, [ 1, 2, 3, 5, 4 ], 'Correct walk path') 41 | t.isDeeply(topoPath, [ 5, 3, 4, 2, 1 ], 'Correct topo path') 42 | }) 43 | 44 | }) 45 | -------------------------------------------------------------------------------- /tests/replica/025_tree_node.t.ts: -------------------------------------------------------------------------------- 1 | // import { Base } from "../../src/class/BetterMixin.js" 2 | // import { Replica } from "../../src/replica/Replica.js" 3 | // import { TreeNode } from "../../src/replica/TreeNode.js" 4 | 5 | declare const StartTest : any 6 | 7 | StartTest(t => { 8 | 9 | t.it('TreeNode w/o propagate', async t => { 10 | 11 | // class TreeNodeBase extends TreeNode.derive(Base) {} 12 | // 13 | // const replica1 = Replica.new() 14 | // const node1 = TreeNodeBase.new() 15 | // 16 | // replica1.addEntity(node1) 17 | // 18 | // t.isDeeply(node1.childrenOrdered.children, new Set([]), 'Correctly resolved `children` reference') 19 | // 20 | // const node2 = TreeNodeBase.new({ parent : node1, nextSibling : null }) 21 | // const node3 = TreeNodeBase.new({ parent : node1, nextSibling : null }) 22 | // const node4 = TreeNodeBase.new({ parent : node1, previousSibling : null }) 23 | // const node5 = TreeNodeBase.new({ parent : node1, nextSibling : node3 }) 24 | // const node6 = TreeNodeBase.new({ parent : node1, nextSibling : node5 }) 25 | // 26 | // replica1.addEntities([ node2, node3, node4, node5, node6 ]) 27 | // 28 | // t.isDeeply(node1.childrenOrdered, [ node4, node2, node6, node5, node3 ], 'Correctly resolved `children` reference') 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bryntum/chronograph", 3 | "version": "1.0.3", 4 | "description": "Open-source reactive computational engine, implemented in TypeScript and developed by Bryntum", 5 | "main": "index.js", 6 | "module": "main.js", 7 | "scripts": { 8 | "test": "npx siesta ./tests", 9 | "docs": "scripts/build_docs.sh" 10 | }, 11 | "homepage": "https://github.com/bryntum/chronograph", 12 | "bugs": "https://github.com/bryntum/chronograph/issues", 13 | "repository": "git@github.com:bryntum/chronograph.git", 14 | "keywords": [ 15 | "reactive", 16 | "graph", 17 | "computation", 18 | "engine" 19 | ], 20 | "author": "Nickolay Platonov, Bryntum", 21 | "license": "MIT", 22 | "dependencies": { 23 | "esm": "^3.2.25", 24 | "tslib": "^1.9.3" 25 | }, 26 | "devDependencies": { 27 | "cytoscape": "3.14.1", 28 | "dagre": "^0.8.5", 29 | "klayjs": "^0.4.1", 30 | "cytoscape-dagre": "^2.2.2", 31 | "cytoscape-klay": "^3.1.3", 32 | "layout-base": "^1.0.2", 33 | "cose-base": "^1.0.2", 34 | "cytoscape-cose-bilkent": "^4.1.0", 35 | "@types/node": "^12.7.2", 36 | "core-js": "^2.6.5", 37 | "deoptigate": "^0.5.0", 38 | "mobx": "^5.13.0", 39 | "regenerator-runtime": "^0.13.1", 40 | "tslint": "^5.16.0", 41 | "typedoc": "https://github.com/bryntum/typedoc.git#custom", 42 | "typedoc-default-themes": "https://github.com/bryntum/typedoc-default-themes.git", 43 | "typescript": "3.8.3" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/event/Event.ts: -------------------------------------------------------------------------------- 1 | //--------------------------------------------------------------------------------------------------------------------- 2 | import { MIN_SMI } from "../util/Helpers.js" 3 | import { compact, Uniqable } from "../util/Uniqable.js" 4 | import { Disposer, Listener } from "./Hook.js" 5 | 6 | 7 | //--------------------------------------------------------------------------------------------------------------------- 8 | export class Event { 9 | compacted : boolean = false 10 | listeners : Listener [] & Uniqable[] = [] 11 | 12 | 13 | on (listener : Listener) : Disposer { 14 | // @ts-ignore 15 | listener.uniqable = MIN_SMI 16 | 17 | this.listeners.push(listener) 18 | 19 | this.compacted = false 20 | 21 | return () => this.un(listener) 22 | } 23 | 24 | 25 | un (listener : Listener) { 26 | if (!this.compacted) this.compact() 27 | 28 | const index = this.listeners.indexOf(listener) 29 | 30 | if (index !== -1) this.listeners.splice(index, 1) 31 | } 32 | 33 | 34 | trigger (...payload : Payload) { 35 | if (!this.compacted) this.compact() 36 | 37 | const listeners = this.listeners.slice() 38 | 39 | for (let i = 0; i < listeners.length; ++i) { 40 | listeners[ i ](...payload) 41 | } 42 | } 43 | 44 | 45 | compact () { 46 | compact(this.listeners) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/chrono/032_propagation_options.t.ts: -------------------------------------------------------------------------------- 1 | import { ChronoGraph } from "../../src/chrono/Graph.js" 2 | 3 | declare const StartTest : any 4 | 5 | StartTest(t => { 6 | 7 | // TODO need to also test with references (something that involves different levels) 8 | 9 | 10 | t.it('Should be able to only calculate the specified nodes', async t => { 11 | const graph : ChronoGraph = ChronoGraph.new() 12 | 13 | const box1 = graph.variable(1) 14 | const box2 = graph.variable(2) 15 | 16 | const iden1 = graph.identifier(function * () { 17 | return (yield box1) + (yield box2) 18 | }) 19 | 20 | const box3 = graph.variable(3) 21 | 22 | const iden2 = graph.identifier(function * () { 23 | return (yield iden1) + (yield box3) 24 | }) 25 | 26 | // ---------------- 27 | const calculation1Spy = t.spyOn(iden1, 'calculation') 28 | const calculation2Spy = t.spyOn(iden2, 'calculation') 29 | 30 | graph.read(iden1) 31 | 32 | t.expect(calculation1Spy).toHaveBeenCalled(1) 33 | t.expect(calculation2Spy).toHaveBeenCalled(0) 34 | 35 | // ---------------- 36 | calculation1Spy.reset() 37 | calculation2Spy.reset() 38 | 39 | t.is(graph.read(iden1), 3, "Correct result calculated") 40 | t.is(graph.read(iden2), 6, "Correct result calculated") 41 | 42 | t.expect(calculation1Spy).toHaveBeenCalled(0) 43 | t.expect(calculation2Spy).toHaveBeenCalled(1) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /src/graph/Graph.ts: -------------------------------------------------------------------------------- 1 | import { Base } from "../class/Base.js" 2 | import { AnyConstructor, Mixin } from "../class/Mixin.js" 3 | import { Node, WalkableBackward, WalkableForward, WalkBackwardContext, WalkForwardContext } from "./Node.js" 4 | 5 | //--------------------------------------------------------------------------------------------------------------------- 6 | export class Graph extends Mixin( 7 | [], 8 | (base : AnyConstructor) => 9 | 10 | class Graph extends base implements WalkableForward, WalkableBackward { 11 | LabelT : any 12 | NodeT : Node 13 | 14 | nodes : Map = new Map() 15 | 16 | 17 | hasNode (node : this[ 'NodeT' ]) : boolean { 18 | return this.nodes.has(node) 19 | } 20 | 21 | 22 | addNode (node : this[ 'NodeT' ], label : this[ 'LabelT' ] = null) { 23 | this.nodes.set(node, label) 24 | } 25 | 26 | 27 | removeNode (node : this[ 'NodeT' ]) { 28 | this.nodes.delete(node) 29 | } 30 | 31 | 32 | forEachIncoming (context : WalkBackwardContext, func : (label : this[ 'LabelT' ], node : this[ 'NodeT' ]) => any) { 33 | this.nodes.forEach(func) 34 | } 35 | 36 | 37 | forEachOutgoing (context : WalkForwardContext, func : (label : this[ 'LabelT' ], node : this[ 'NodeT' ]) => any) { 38 | this.nodes.forEach(func) 39 | } 40 | }){} 41 | 42 | export class GraphBase extends Graph.derive(Base) {} 43 | 44 | 45 | // export class GraphBase2 extends Mixin([ Graph, Base ], IdentityMixin()) {} 46 | // 47 | // 48 | // const a = GraphBase2.new() 49 | // 50 | // a. 51 | // 52 | // a.zxc 53 | -------------------------------------------------------------------------------- /src/chrono/Revision.ts: -------------------------------------------------------------------------------- 1 | import { Base } from "../class/Base.js" 2 | import { Identifier } from "./Identifier.js" 3 | import { Quark, TombStone } from "./Quark.js" 4 | 5 | 6 | export type Scope = Map 7 | 8 | 9 | //--------------------------------------------------------------------------------------------------------------------- 10 | export type RevisionClock = number 11 | 12 | let CLOCK : RevisionClock = 0 13 | 14 | export class Revision extends Base { 15 | createdAt : RevisionClock = CLOCK++ 16 | 17 | name : string = 'revision-' + this.createdAt 18 | 19 | previous : Revision = undefined 20 | 21 | scope : Scope = new Map() 22 | 23 | reachableCount : number = 0 24 | referenceCount : number = 0 25 | 26 | selfDependent : Set = new Set() 27 | 28 | 29 | getLatestEntryFor (identifier : Identifier) : Quark { 30 | let revision : Revision = this 31 | 32 | while (revision) { 33 | const entry = revision.scope.get(identifier) 34 | 35 | if (entry) return entry 36 | 37 | revision = revision.previous 38 | } 39 | 40 | return null 41 | } 42 | 43 | 44 | hasIdentifier (identifier : Identifier) : boolean { 45 | const latestEntry = this.getLatestEntryFor(identifier) 46 | 47 | return Boolean(latestEntry && latestEntry.getValue() !== TombStone) 48 | } 49 | 50 | 51 | * previousAxis () : Generator { 52 | let revision : Revision = this 53 | 54 | while (revision) { 55 | yield revision 56 | 57 | revision = revision.previous 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/replica/002_fields.t.ts: -------------------------------------------------------------------------------- 1 | import { Base } from "../../src/class/Base.js" 2 | import { Entity, field } from "../../src/replica/Entity.js" 3 | import { Name } from "../../src/schema/Field.js" 4 | 5 | declare const StartTest : any 6 | 7 | StartTest(t => { 8 | 9 | t.it('Child class fields should stay at child level', async t => { 10 | 11 | class Vehicle extends Entity.mix(Base) { 12 | @field() 13 | name : string 14 | } 15 | 16 | class Car extends Vehicle { 17 | @field() 18 | drivingWheel : boolean = true 19 | } 20 | 21 | class Boat extends Vehicle { 22 | @field() 23 | helm : boolean = true 24 | } 25 | 26 | const vehicleFields = new Set() 27 | const carFields = new Set() 28 | const boatFields = new Set() 29 | 30 | Vehicle.$entity.forEachField(field => vehicleFields.add(field.name)) 31 | Car.$entity.forEachField(field => carFields.add(field.name)) 32 | Boat.$entity.forEachField(field => boatFields.add(field.name)) 33 | 34 | t.isDeeply(vehicleFields, new Set([ 'name' ]), "Vehicle fields are ok") 35 | t.isDeeply(carFields, new Set([ 'name', 'drivingWheel' ]), "Car fields are ok") 36 | t.isDeeply(boatFields, new Set([ 'name', 'helm' ]), "Boat fields are ok") 37 | 38 | t.ok(Vehicle.$entity.hasField('name'), "Vehicle has own field") 39 | 40 | t.ok(Boat.$entity.hasField('name'), "Boat has inherited field") 41 | t.ok(Boat.$entity.hasField('helm'), "Boat has own field") 42 | 43 | t.ok(Car.$entity.hasField('name'), "Car has inherited field") 44 | t.ok(Car.$entity.hasField('drivingWheel'), "Car has own field") 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Release history for ChronoGraph: 2 | 3 | ## {{ $NEXT }} 4 | 5 | 1.0.3 2021-06-10 12:58 6 | 7 | #### FEATURES / ENHANCEMENTS 8 | 9 | - None 10 | 11 | #### API CHANGES 12 | 13 | - None 14 | 15 | #### BUG FIXES 16 | 17 | - Fixed a bug, that calculation function from the entity super-class could leak to the sub-class. 18 | 19 | ## 1.0.2 2020-05-28 13:31 20 | 21 | #### FEATURES / ENHANCEMENTS 22 | 23 | - Chronograph now supports running in the "regenerator" environment (transpiled generators) 24 | 25 | - Added experimental "hook" utility 26 | 27 | #### API CHANGES 28 | 29 | - None 30 | 31 | #### BUG FIXES 32 | 33 | - Fixed a bug, that on-demand calculations for lazy identifiers were performed in separate 34 | transaction. This was breaking the `ProposedOrPrevious` effect. Such calculations are now 35 | performed in the currently running transaction. 36 | 37 | - Fixed a bug, that removing and adding an entity back to the graph right away was not updating 38 | the related buckets. 39 | 40 | - Fixed a bug, that transaction rejection could lead to exception if there were writes during 41 | `finalizeCommitAsync` method. 42 | 43 | - Fixed a bug, that identifier could be considered as unchanged mistakenly and its calculation 44 | method not called. 45 | 46 | - Fixed a bug, that using `derive` method of the mixin over the base class, that has extended 47 | another class, in turn created with `derive`, could skip some mixin requirements. 48 | 49 | 50 | ## 1.0.1 2020-04-07 19:56 51 | 52 | - Fixed links to docs 53 | 54 | ## 1.0.0 2020-04-07 19:26 55 | 56 | The 1.0.0 release. 57 | 58 | ## 0.0.5 2020-03-19 20:10 59 | 60 | Testing release script. 61 | 62 | ## 0.0.4 2020-03-18 18:25 63 | 64 | This is the initial release of the ChronoGraph. 65 | 66 | -------------------------------------------------------------------------------- /tests/chrono/030_iteration.t.ts: -------------------------------------------------------------------------------- 1 | import { ChronoGraph } from "../../src/chrono/Graph.js" 2 | import { CalculatedValueGen } from "../../src/chrono/Identifier.js" 3 | import { TransactionCommitResult } from "../../src/chrono/Transaction.js" 4 | 5 | declare const StartTest : any 6 | 7 | StartTest(t => { 8 | 9 | t.it('Should be able to write to graph in the `finalizeCommitAsync`', async t => { 10 | class CustomGraph extends ChronoGraph { 11 | 12 | async finalizeCommitAsync (transactionResult : TransactionCommitResult) { 13 | if (await graph.readAsync(c1) > 10) { 14 | graph.write(i1, 3) 15 | graph.write(i2, 3) 16 | } 17 | 18 | await super.finalizeCommitAsync(transactionResult) 19 | } 20 | } 21 | 22 | const graph : CustomGraph = CustomGraph.new({ onWriteDuringCommit : 'ignore' }) 23 | 24 | const i1 = graph.variableNamed('i1', 0) 25 | const i2 = graph.variableNamed('i2', 10) 26 | 27 | const c1 = graph.addIdentifier(CalculatedValueGen.new({ 28 | name : 'c1', 29 | sync : false, 30 | calculation : function* () { 31 | return (yield i1) + (yield i2) 32 | } 33 | })) 34 | 35 | await graph.commitAsync() 36 | 37 | // ---------------- 38 | const nodes = [ i1, i2, c1 ] 39 | 40 | t.isDeeply(nodes.map(node => graph.get(node)), [ 0, 10, 10 ], "Correct result calculated #1") 41 | 42 | // ---------------- 43 | graph.write(i1, 7) 44 | graph.write(i2, 8) 45 | 46 | await graph.commitAsync() 47 | 48 | t.isDeeply(nodes.map(node => graph.get(node)), [ 3, 3, 6 ], "Correct result calculated #1") 49 | }) 50 | }) 51 | 52 | -------------------------------------------------------------------------------- /tests/benchmark/allocation.ts: -------------------------------------------------------------------------------- 1 | import { Benchmark } from "../../src/benchmark/Benchmark.js" 2 | import { deepGraphGen, deepGraphSync, mobxGraph, replicaGen } from "./data.js" 3 | 4 | 5 | //--------------------------------------------------------------------------------------------------------------------- 6 | export const graphPopulationGen = Benchmark.new({ 7 | name : 'Graph population 100k - generators', 8 | 9 | cycle : (iteration : number, cycle : number, setup : any) => { 10 | deepGraphGen(100000) 11 | } 12 | }) 13 | 14 | 15 | export const graphPopulationSync = Benchmark.new({ 16 | name : 'Graph population 100k - synchronous', 17 | 18 | cycle : (iteration : number, cycle : number, setup : any) => { 19 | deepGraphSync(100000) 20 | } 21 | }) 22 | 23 | 24 | export const graphPopulationMobx = Benchmark.new({ 25 | name : 'Graph population 100k - Mobx', 26 | 27 | cycle : (iteration : number, cycle : number, setup : any) => { 28 | mobxGraph(100000) 29 | } 30 | }) 31 | 32 | 33 | //--------------------------------------------------------------------------------------------------------------------- 34 | export const replicaPopulation = Benchmark.new({ 35 | name : 'Replica population 125k', 36 | 37 | cycle : (iteration : number, cycle : number, setup : any) => { 38 | replicaGen(5000) 39 | } 40 | }) 41 | 42 | 43 | 44 | //--------------------------------------------------------------------------------------------------------------------- 45 | export const runAllGraphPopulation = async () => { 46 | await graphPopulationGen.measureTillRelativeMoe() 47 | await graphPopulationSync.measureTillRelativeMoe() 48 | await graphPopulationMobx.measureTillRelativeMoe() 49 | 50 | await replicaPopulation.measureTillRelativeMoe() 51 | } 52 | -------------------------------------------------------------------------------- /tests/chrono/033_cycle_info.t.ts: -------------------------------------------------------------------------------- 1 | import { HasProposedValue } from "../../src/chrono/Effect.js" 2 | import { ChronoGraph } from "../../src/chrono/Graph.js" 3 | 4 | declare const StartTest : any 5 | 6 | StartTest(t => { 7 | 8 | 9 | t.it('Should show the detailed information about the cyclic computation', async t => { 10 | const graph : ChronoGraph = ChronoGraph.new() 11 | 12 | const iden1 = graph.identifierNamed('iden1', function* (Y) { 13 | return yield iden2 14 | }) 15 | 16 | const iden2 = graph.identifierNamed('iden2', function* (Y) { 17 | return yield iden1 18 | }) 19 | 20 | // ---------------- 21 | t.throwsOk(() => graph.read(iden1), /iden1.*iden2/s, 'Include identifier name in the cycle info') 22 | }) 23 | 24 | 25 | t.it('Should show the detailed information about the cyclic computation, which involves edges from the past', async t => { 26 | const graph : ChronoGraph = ChronoGraph.new() 27 | 28 | const dispatcher = graph.identifierNamed('dispatcher', function* (Y) { 29 | const iden1HasProposed = yield HasProposedValue(iden1) 30 | const iden2HasProposed = yield HasProposedValue(iden2) 31 | 32 | return 'result' 33 | }) 34 | 35 | const iden1 = graph.identifierNamed('iden1', function* (Y) { 36 | const disp = yield dispatcher 37 | 38 | return yield iden2 39 | }) 40 | 41 | const iden2 = graph.identifierNamed('iden2', function* (Y) { 42 | const disp = yield dispatcher 43 | 44 | return yield iden1 45 | }) 46 | 47 | // ---------------- 48 | t.throwsOk(() => graph.read(iden1), /iden1.*iden2/s, 'Include identifier name in the cycle info') 49 | }) 50 | 51 | }) 52 | -------------------------------------------------------------------------------- /tests/schema/010_schema.t.ts: -------------------------------------------------------------------------------- 1 | import { Base } from "../../src/class/Base.js" 2 | import { Entity, field } from "../../src/replica/Entity.js" 3 | import { FieldIdentifier } from "../../src/replica/Identifier.js" 4 | import { Schema } from "../../src/schema/Schema.js" 5 | 6 | declare const StartTest : any 7 | 8 | 9 | StartTest(t => { 10 | 11 | t.it('Minimal Schema', t => { 12 | 13 | const SomeSchema = Schema.new({ name : 'Cool data schema' }) 14 | 15 | const entity = SomeSchema.getEntityDecorator() 16 | 17 | @entity 18 | class SomeEntity extends Entity.mix(Base) { 19 | @field() 20 | someField1 : string = 'someField' 21 | 22 | @field() 23 | someField2 : number = 11 24 | 25 | @field() 26 | someField3 : Date = new Date(2018, 11, 11) 27 | } 28 | 29 | 30 | //------------------------- 31 | const entity1 = SomeEntity.new() 32 | 33 | t.is(entity1.someField1, 'someField', 'Entity behaves as regular class') 34 | t.is(entity1.someField2, 11, 'Entity behaves as regular class') 35 | t.is(entity1.someField3, new Date(2018, 11, 11), 'Entity behaves as regular class') 36 | 37 | 38 | //------------------------- 39 | t.ok(SomeSchema.hasEntity('SomeEntity'), 'Entity has been created in the schema') 40 | 41 | const ent = SomeSchema.getEntity('SomeEntity') 42 | 43 | t.ok(ent.hasField('someField1'), 'Field has been created in the entity') 44 | t.ok(ent.hasField('someField2'), 'Field has been created in the entity') 45 | t.ok(ent.hasField('someField3'), 'Field has been created in the entity') 46 | 47 | t.isInstanceOf(entity1.$.someField1, FieldIdentifier) 48 | t.is(entity1.$.someField1.field, ent.getField('someField1')) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /src/schema/Field.ts: -------------------------------------------------------------------------------- 1 | import { Meta } from "../chrono/Identifier.js" 2 | import { AnyFunction } from "../class/Mixin.js" 3 | import { Context } from "../primitives/Calculation.js" 4 | import { FieldIdentifierConstructor, MinimalFieldIdentifierGen, MinimalFieldIdentifierSync, MinimalFieldVariable } from "../replica/Identifier.js" 5 | import { isGeneratorFunction } from "../util/Helpers.js" 6 | import { EntityMeta } from "./EntityMeta.js" 7 | 8 | /** 9 | * Type for the name of the entities/fields, just an alias to `string` 10 | */ 11 | export type Name = string 12 | 13 | /** 14 | * Type for the type of the fields, just an alias to `string` 15 | */ 16 | export type Type = string 17 | 18 | 19 | //--------------------------------------------------------------------------------------------------------------------- 20 | /** 21 | * This class describes a field of some [[EntityMeta]]. 22 | */ 23 | export class Field extends Meta { 24 | type : Type 25 | 26 | /** 27 | * Reference to the [[EntityMeta]] this field belongs to. 28 | */ 29 | entity : EntityMeta 30 | 31 | /** 32 | * Boolean flag, indicating whether this field should be persisted 33 | */ 34 | persistent : boolean = true 35 | 36 | /** 37 | * The class of the identifier, that will be used to instantiate a specific identifier from this field. 38 | */ 39 | identifierCls : FieldIdentifierConstructor 40 | 41 | ignoreEdgesFlow : boolean = false 42 | 43 | 44 | getIdentifierClass (calculationFunction : AnyFunction) : FieldIdentifierConstructor { 45 | if (this.identifierCls) return this.identifierCls 46 | 47 | if (!calculationFunction) return MinimalFieldVariable 48 | 49 | return isGeneratorFunction(calculationFunction) ? MinimalFieldIdentifierGen : MinimalFieldIdentifierSync 50 | } 51 | } 52 | 53 | -------------------------------------------------------------------------------- /tests/visualization/010_replica.t.ts: -------------------------------------------------------------------------------- 1 | import { ChronoGraph } from "../../src/chrono/Graph.js" 2 | import { Base } from "../../src/class/Base.js" 3 | import { Entity } from "../../src/replica/Entity.js" 4 | import { reference } from "../../src/replica/Reference.js" 5 | import { bucket } from "../../src/replica/ReferenceBucket.js" 6 | import { Replica } from "../../src/replica/Replica.js" 7 | import { Schema } from "../../src/schema/Schema.js" 8 | import { CytoscapeWrapper } from "../../src/visualization/Cytoscape.js" 9 | 10 | declare const StartTest, document : any 11 | 12 | StartTest(t => { 13 | 14 | t.it('Author/Book with commits', async t => { 15 | // const graph : ChronoGraph = ChronoGraph.new() 16 | // 17 | // const i1 = graph.variableNamed('i1', 0) 18 | // const i2 = graph.variableNamed('i2', 10) 19 | // 20 | // const c1 = graph.identifierNamed('c1', function* () { 21 | // return (yield i1) + (yield i2) 22 | // }) 23 | // 24 | // const c2 = graph.identifierNamed('c2', function* () { 25 | // return (yield i1) + (yield c1) 26 | // }) 27 | // 28 | // const c3 = graph.identifierNamed('c3', function* () { 29 | // return (yield c1) 30 | // }) 31 | // 32 | // const c4 = graph.identifierNamed('c4', function* () { 33 | // return (yield c3) 34 | // }) 35 | // 36 | // const c5 = graph.identifierNamed('c5', function* () { 37 | // return (yield c3) 38 | // }) 39 | // 40 | // const c6 = graph.identifierNamed('c6', function* () { 41 | // return (yield c5) + (yield i2) 42 | // }) 43 | // 44 | // graph.commit() 45 | // 46 | // const wrapper = CytoscapeWrapper.new({ graph : graph }) 47 | // 48 | // wrapper.renderTo(document.getElementById('graph')) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /src/class/Base.ts: -------------------------------------------------------------------------------- 1 | //--------------------------------------------------------------------------------------------------------------------- 2 | /** 3 | * This is a base class, providing the type-safe static constructor [[new]]. This is very convenient when using 4 | * [[Mixin|mixins]], as mixins can not have types in the constructors. 5 | */ 6 | export class Base { 7 | 8 | /** 9 | * This method applies its 1st argument (if any) to the current instance using `Object.assign()`. 10 | * 11 | * Supposed to be overridden in the subclasses to customize the instance creation process. 12 | * 13 | * @param props 14 | */ 15 | initialize (props? : Partial) { 16 | props && Object.assign(this, props) 17 | } 18 | 19 | 20 | /** 21 | * This is a type-safe static constructor method, accepting a single argument, with the object, corresponding to the 22 | * class properties. It will generate a compilation error, if unknown property is provided. 23 | * 24 | * For example: 25 | * 26 | * ```ts 27 | * class MyClass extends Base { 28 | * prop : string 29 | * } 30 | * 31 | * const instance : MyClass = MyClass.new({ prop : 'prop', wrong : 11 }) 32 | * ``` 33 | * 34 | * will produce: 35 | * 36 | * ```plaintext 37 | * TS2345: Argument of type '{ prop: string; wrong: number; }' is not assignable to parameter of type 'Partial'.    38 | * Object literal may only specify known properties, and 'wrong' does not exist in type 'Partial' 39 | * ``` 40 | * 41 | * The only thing this constructor does is create an instance and call the [[initialize]] method on it, forwarding 42 | * the first argument. The customization of instance is supposed to be performed in that method. 43 | * 44 | * @param props 45 | */ 46 | static new (this : T, props? : Partial>) : InstanceType { 47 | const instance = new this() 48 | 49 | instance.initialize>(props) 50 | 51 | return instance as InstanceType 52 | } 53 | } 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /tests/chrono/050_undo_redo.t.ts: -------------------------------------------------------------------------------- 1 | import { ChronoGraph } from "../../src/chrono/Graph.js" 2 | 3 | declare const StartTest : any 4 | 5 | StartTest(t => { 6 | 7 | t.it('Undo/redo of variable value', async t => { 8 | const graph1 : ChronoGraph = ChronoGraph.new({ historyLimit : 2 }) 9 | 10 | const var1 = graph1.variable(0) 11 | 12 | graph1.commit() 13 | 14 | t.is(graph1.read(var1), 0, 'Correct value') 15 | 16 | //-------------- 17 | graph1.write(var1, 1) 18 | 19 | graph1.commit() 20 | 21 | t.is(graph1.read(var1), 1, 'Correct value') 22 | 23 | //-------------- 24 | graph1.undo() 25 | 26 | t.is(graph1.read(var1), 0, 'Correct value') 27 | 28 | //-------------- 29 | graph1.redo() 30 | 31 | t.is(graph1.read(var1), 1, 'Correct value') 32 | }) 33 | 34 | 35 | t.it('Undo/redo of new identifier', async t => { 36 | const graph1 : ChronoGraph = ChronoGraph.new({ historyLimit : 2 }) 37 | 38 | const var1 = graph1.variable(0) 39 | 40 | graph1.commit() 41 | 42 | t.is(graph1.read(var1), 0, 'Correct value') 43 | 44 | //-------------- 45 | graph1.undo() 46 | 47 | t.throwsOk(() => graph1.read(var1), 'Unknown identifier') 48 | 49 | //-------------- 50 | graph1.redo() 51 | 52 | t.is(graph1.read(var1), 0, 'Correct value') 53 | }) 54 | 55 | 56 | t.it('Undo/redo of identifier removal', async t => { 57 | const graph1 : ChronoGraph = ChronoGraph.new({ historyLimit : 2 }) 58 | 59 | const var1 = graph1.variable(0) 60 | 61 | graph1.commit() 62 | 63 | t.is(graph1.read(var1), 0, 'Correct value') 64 | 65 | //-------------- 66 | graph1.removeIdentifier(var1) 67 | 68 | graph1.commit() 69 | 70 | t.throwsOk(() => graph1.read(var1), 'Unknown identifier') 71 | 72 | //-------------- 73 | graph1.undo() 74 | 75 | t.is(graph1.read(var1), 0, 'Correct value') 76 | 77 | //-------------- 78 | graph1.redo() 79 | 80 | t.throwsOk(() => graph1.read(var1), 'Unknown identifier') 81 | }) 82 | 83 | }) 84 | -------------------------------------------------------------------------------- /tests/collection/010_chained_iterator.t.ts: -------------------------------------------------------------------------------- 1 | import { CI, map, MemoizedIterator, MI } from "../../src/collection/Iterator.js" 2 | 3 | declare const StartTest : any 4 | 5 | StartTest(t => { 6 | 7 | t.it('Should be able to use chained iterators', t => { 8 | const a : Set = new Set([ '1', '12', '123' ]) 9 | 10 | const iter1 : Iterable = CI(a) 11 | const iter2 : Iterable = map(a, el => el.length) 12 | 13 | t.isDeeply(Array.from(iter1), [ '1', '12', '123' ]) 14 | t.isDeeply(Array.from(iter2), [ 1, 2, 3 ]) 15 | }) 16 | 17 | 18 | t.it('Should be able to use memoized iterators', t => { 19 | const a : Set = new Set([ '1', '12', '123' ]) 20 | 21 | const iter1 : MemoizedIterator = MI(a) 22 | const iter2 : Iterable = iter1.map(el => el.length) 23 | 24 | t.isDeeply(Array.from(iter1), [ '1', '12', '123' ]) 25 | t.isDeeply(Array.from(iter1), [ '1', '12', '123' ]) 26 | 27 | t.isDeeply(Array.from(iter2), [ 1, 2, 3 ]) 28 | }) 29 | 30 | 31 | t.it('Should be able to use iterators, derived from the memoized iterator in any order', t => { 32 | const a : Set = new Set([ '1', '12', '123' ]) 33 | 34 | const iter1 : MemoizedIterator = MI(a) 35 | const iter2 : Iterable = iter1.map(el => el.length) 36 | const iter3 : Iterable = iter1.map(el => el.repeat(2)) 37 | 38 | const iterator2 = iter2[ Symbol.iterator ]() 39 | const iterator3 = iter3[ Symbol.iterator ]() 40 | 41 | t.isDeeply( 42 | [ iterator2.next().value, iterator3.next().value ], 43 | [ 1, '11' ] 44 | ) 45 | 46 | // opposite order 47 | t.isDeeply( 48 | [ iterator3.next().value, iterator2.next().value ], 49 | [ '1212', 2 ] 50 | ) 51 | 52 | t.isDeeply( 53 | [ iterator2.next().value, iterator3.next().value ], 54 | [ 3, '123123' ] 55 | ) 56 | 57 | t.isDeeply( 58 | [ iterator3.next().done, iterator2.next().done ], 59 | [ true, true ] 60 | ) 61 | }) 62 | }) 63 | 64 | 65 | -------------------------------------------------------------------------------- /tests/replica/040_calculate_only.t.ts: -------------------------------------------------------------------------------- 1 | import { Base } from "../../src/class/Base.js" 2 | import { CalculationIterator } from "../../src/primitives/Calculation.js" 3 | import { calculate, Entity, field } from "../../src/replica/Entity.js" 4 | import { reference } from "../../src/replica/Reference.js" 5 | import { bucket } from "../../src/replica/ReferenceBucket.js" 6 | import { Replica } from "../../src/replica/Replica.js" 7 | import { Schema } from "../../src/schema/Schema.js" 8 | 9 | declare const StartTest : any 10 | 11 | StartTest(t => { 12 | 13 | t.it('Author/Book', async t => { 14 | const SomeSchema = Schema.new({ name : 'Cool data schema' }) 15 | 16 | const entity = SomeSchema.getEntityDecorator() 17 | 18 | @entity 19 | class Author extends Entity.mix(Base) { 20 | @bucket() 21 | books : Set 22 | 23 | @field() 24 | booksCount : number 25 | 26 | 27 | @calculate('booksCount') 28 | * calculateBooksCount () : CalculationIterator { 29 | const books : Set = yield this.$.books 30 | 31 | return books.size 32 | } 33 | } 34 | 35 | @entity 36 | class Book extends Entity.mix(Base) { 37 | @reference({ bucket : 'books' }) 38 | writtenBy : Author 39 | } 40 | 41 | const replica1 = Replica.new({ schema : SomeSchema }) 42 | 43 | const markTwain = Author.new() 44 | const tomSoyer = Book.new({ writtenBy : markTwain }) 45 | 46 | replica1.addEntity(markTwain) 47 | replica1.addEntity(tomSoyer) 48 | 49 | //-------------------- 50 | replica1.commit() 51 | 52 | t.isDeeply(markTwain.books, new Set([ tomSoyer ]), 'Correctly filled bucket') 53 | t.isDeeply(tomSoyer.writtenBy, markTwain, 'Correct reference value') 54 | 55 | //-------------------- 56 | const tomSoyer2 = Book.new({ writtenBy : markTwain }) 57 | 58 | replica1.addEntity(tomSoyer2) 59 | 60 | // replica1.propagate({ calculateOnly : [ markTwain.$.booksCount ] }) 61 | 62 | t.is(replica1.read(markTwain.$.booksCount), 2, 'Correctly taken new reference into account with `calculateOnly` option') 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /tests/benchmark/memory_leak.ts: -------------------------------------------------------------------------------- 1 | import { Benchmark } from "../../src/benchmark/Benchmark.js" 2 | import { ChronoGraph } from "../../src/chrono/Graph.js" 3 | import { CalculatedValueGen, Variable } from "../../src/chrono/Identifier.js" 4 | import { GraphGenerationResult, mostlyShadowingGraph } from "./data.js" 5 | 6 | 7 | //--------------------------------------------------------------------------------------------------------------------- 8 | export const shadowingQuarksMemoryLeak = Benchmark.new({ 9 | name : 'Memory leak because of shadowing quarks', 10 | 11 | setup : async () : Promise => { 12 | return mostlyShadowingGraph(100000) 13 | }, 14 | 15 | 16 | cycle : (iteration : number, cycle : number, state : GraphGenerationResult) => { 17 | const { graph, boxes } = state 18 | 19 | graph.write(boxes[ 0 ], iteration + cycle) 20 | 21 | graph.commit() 22 | } 23 | }) 24 | 25 | 26 | //--------------------------------------------------------------------------------------------------------------------- 27 | export const tombStonesMemoryLeak = Benchmark.new({ 28 | name : 'Memory leak because of tombstones', 29 | 30 | setup : async () : Promise => { 31 | return { graph : ChronoGraph.new(), boxes : [], counter : 0 } 32 | }, 33 | 34 | 35 | cycle : (iteration : number, cycle : number, state : GraphGenerationResult) => { 36 | const { graph } = state 37 | 38 | const boxes = [] 39 | 40 | for (let i = 0; i < 50000; i++) { 41 | const iden1 = Variable.new({ name : i }) 42 | const iden2 = CalculatedValueGen.new({ *calculation () { return yield iden1 } }) 43 | 44 | boxes.push(iden1, iden2) 45 | 46 | graph.addIdentifier(iden1, 0) 47 | graph.addIdentifier(iden2) 48 | } 49 | 50 | graph.commit() 51 | 52 | boxes.forEach(identifier => graph.removeIdentifier(identifier)) 53 | 54 | graph.commit() 55 | } 56 | }) 57 | 58 | 59 | 60 | //--------------------------------------------------------------------------------------------------------------------- 61 | export const runAllMemoryLeak = async () => { 62 | await shadowingQuarksMemoryLeak.measureTillMaxTime() 63 | 64 | await tombStonesMemoryLeak.measureTillMaxTime() 65 | } 66 | -------------------------------------------------------------------------------- /tests/chrono/040_add_remove.t.ts: -------------------------------------------------------------------------------- 1 | import { ChronoGraph } from "../../src/chrono/Graph.js" 2 | 3 | declare const StartTest : any 4 | 5 | StartTest(t => { 6 | 7 | t.it('Add variable', async t => { 8 | const graph1 : ChronoGraph = ChronoGraph.new() 9 | 10 | const var1 = graph1.variable(0) 11 | 12 | t.livesOk(() => graph1.read(var1)) 13 | 14 | graph1.commit() 15 | 16 | t.is(graph1.read(var1), 0, 'Correct value') 17 | 18 | //-------------- 19 | const graph2 = graph1.branch() 20 | 21 | const var2 = graph2.variable(1) 22 | 23 | graph2.commit() 24 | 25 | t.is(graph2.read(var2), 1, 'Correct value') 26 | 27 | //-------------- 28 | t.throwsOk(() => graph1.read(var2), 'Unknown identifier', 'First branch does not know about variable in 2nd branch') 29 | }) 30 | 31 | 32 | t.it('Remove variable', async t => { 33 | const graph1 : ChronoGraph = ChronoGraph.new() 34 | 35 | const var1 = graph1.variable(0) 36 | 37 | const iden1 = graph1.identifier(function * () { 38 | return yield var1 39 | }) 40 | 41 | t.livesOk(() => graph1.read(var1)) 42 | 43 | graph1.commit() 44 | 45 | t.is(graph1.read(var1), 0, 'Correct value') 46 | 47 | //-------------- 48 | const graph2 = graph1.branch() 49 | 50 | //-------------- 51 | graph1.removeIdentifier(var1) 52 | 53 | t.throwsOk(() => graph1.read(var1), 'Unknown identifier') 54 | 55 | //-------------- 56 | t.is(graph2.read(var1), 0, 'Other branches not affected by removal') 57 | }) 58 | 59 | 60 | t.it('Remove identifier', async t => { 61 | const graph1 : ChronoGraph = ChronoGraph.new() 62 | 63 | const var1 = graph1.variable(0) 64 | 65 | const iden1 = graph1.identifier(function * () { 66 | return (yield var1) + 1 67 | }) 68 | 69 | const iden2 = graph1.identifier(function * () { 70 | return (yield iden1) + 1 71 | }) 72 | 73 | t.is(graph1.read(iden2), 2) 74 | 75 | graph1.commit() 76 | 77 | // remove identifier with incoming edge and commit 78 | graph1.removeIdentifier(iden2) 79 | 80 | graph1.commit() 81 | 82 | // should not trigger a crash 83 | graph1.write(var1, 1) 84 | 85 | t.is(graph1.read(iden1), 2) 86 | }) 87 | }) 88 | -------------------------------------------------------------------------------- /tests/benchmark/compact/compact.ts: -------------------------------------------------------------------------------- 1 | import { BenchmarkC } from "../../../src/benchmark/Benchmark.js" 2 | import { MIN_SMI } from "../../../src/util/Helpers.js" 3 | import { compact, Uniqable } from "../../../src/util/Uniqable.js" 4 | 5 | 6 | const toCompact : number = 10000 7 | 8 | type CompactBenchState = { 9 | array : Uniqable[][] 10 | } 11 | 12 | const getUniqable = () => { return { uniqable : MIN_SMI } } 13 | 14 | const el1 = getUniqable() 15 | const el2 = getUniqable() 16 | const el3 = getUniqable() 17 | 18 | const elements : Uniqable[] = [ el1, el2, el1, el3, el1, el2, el3, el1, el2, el1, el3, el1, el2, el3, el1, el2, el1, el3, el1, el2, el3, ] 19 | 20 | const compactees = Array(toCompact).fill(null).map(() => elements) 21 | 22 | //--------------------------------------------------------------------------------------------------------------------- 23 | const compactInPlaceBenchmark = BenchmarkC({ 24 | name : `Compact in place ${toCompact}`, 25 | 26 | async setup () : Promise { 27 | return { 28 | array : compactees 29 | } 30 | }, 31 | 32 | cycle (iteration : number, cycle : number, setup : CompactBenchState) { 33 | const { array } = setup 34 | 35 | const compacted = [] 36 | 37 | for (let i = 0; i < array.length; i++) { 38 | const arrayToCompact = array[ i ].slice() 39 | 40 | compacted.push(compact(arrayToCompact)) 41 | } 42 | } 43 | }) 44 | 45 | 46 | //--------------------------------------------------------------------------------------------------------------------- 47 | const compactImmutableBenchmark = BenchmarkC({ 48 | name : `Compact immutable ${toCompact}`, 49 | 50 | async setup () : Promise { 51 | return { 52 | array : compactees 53 | } 54 | }, 55 | 56 | cycle (iteration : number, cycle : number, setup : CompactBenchState) { 57 | const { array } = setup 58 | 59 | const compacted = [] 60 | 61 | for (let i = 0; i < array.length; i++) { 62 | const arrayToCompact = array[ i ].slice() 63 | 64 | compacted.push(Array.from(new Set(arrayToCompact))) 65 | } 66 | } 67 | }) 68 | 69 | const runAll = async () => { 70 | await compactInPlaceBenchmark.measureTillMaxTime() 71 | await compactImmutableBenchmark.measureTillMaxTime() 72 | } 73 | 74 | 75 | runAll() 76 | -------------------------------------------------------------------------------- /tests/benchmark/walk_depth/walk_depth.ts: -------------------------------------------------------------------------------- 1 | import { Benchmark } from "../../../src/benchmark/Benchmark.js" 2 | import { 3 | WalkDepthNoCyclesNoVisitStateExternalStack, 4 | WalkDepthNoCyclesNoVisitStateRecursive 5 | } from "../../../src/collection/walk_depth/WalkDepthopedia.js" 6 | import { deepGraphGen, GraphGenerationResult, Node } from "./data.js" 7 | 8 | 9 | //--------------------------------------------------------------------------------------------------------------------- 10 | export const walkDepthNoCyclesNoVisitStateRecursive = Benchmark.new({ 11 | name : 'Walk depth no cycles, recursive', 12 | 13 | setup : async () : Promise => { 14 | return deepGraphGen(1300, 4) 15 | }, 16 | 17 | cycle : (iteration : number, cycle : number, setup : GraphGenerationResult) => { 18 | let total : number = 0 19 | 20 | const walkDepthContext = WalkDepthNoCyclesNoVisitStateRecursive.new({ 21 | next : (node : Node) => node.outgoing, 22 | 23 | onVisit : (node : Node) => total += node.count 24 | }) 25 | 26 | walkDepthContext.walkDepth(setup.nodes[ 0 ]) 27 | 28 | // console.log("Total: ", total) 29 | } 30 | }) 31 | 32 | 33 | //--------------------------------------------------------------------------------------------------------------------- 34 | export const walkDepthNoCyclesNoVisitStateExternalStack = Benchmark.new({ 35 | name : 'Walk depth no cycles, external stack', 36 | 37 | setup : async () : Promise => { 38 | return deepGraphGen(1300, 4) 39 | }, 40 | 41 | cycle : (iteration : number, cycle : number, setup : GraphGenerationResult) => { 42 | let total : number = 0 43 | 44 | const walkDepthContext = WalkDepthNoCyclesNoVisitStateExternalStack.new({ 45 | next : (node : Node) => node.outgoing, 46 | 47 | onVisit : (node : Node) => total += node.count 48 | }) 49 | 50 | walkDepthContext.walkDepth(setup.nodes[ 0 ]) 51 | 52 | // console.log("Total: ", total) 53 | } 54 | }) 55 | 56 | 57 | 58 | //--------------------------------------------------------------------------------------------------------------------- 59 | export const runAllWalkDepth = async () => { 60 | await walkDepthNoCyclesNoVisitStateRecursive.measureTillRelativeMoe() 61 | await walkDepthNoCyclesNoVisitStateExternalStack.measureTillRelativeMoe() 62 | } 63 | 64 | runAllWalkDepth() 65 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "space-before-function-paren": [true, "always"], 4 | // "one-variable-per-declaration": true, 5 | 6 | 7 | // "align": false, 8 | // "class-name": true, 9 | // "comment-format": [ true, "check-space" ], 10 | // "curly": true, 11 | // "eofline": true, 12 | // "forin": false, 13 | "indent": [ true, "spaces", 4 ], 14 | // "interface-name": [ true, "never-prefix" ], 15 | // "jsdoc-format": true, 16 | // "label-position": true, 17 | // "max-line-length": [true, 170], 18 | // "member-access": false, 19 | // "member-ordering": false, 20 | // "no-any": false, 21 | // "no-arg": true, 22 | // "no-bitwise": false, 23 | // "no-consecutive-blank-lines": true, 24 | // "no-console": false, 25 | // "no-construct": false, 26 | // "no-debugger": true, 27 | // "no-duplicate-variable": true, 28 | // "no-empty": false, 29 | // "no-eval": true, 30 | // "no-for-in-array": true, 31 | // "no-inferrable-types": [ true, "ignore-params" ], 32 | // "no-shadowed-variable": false, 33 | // "no-string-literal": false, 34 | // "no-switch-case-fall-through": false, 35 | // "no-trailing-whitespace": true, 36 | // "no-unnecessary-type-assertion": true, 37 | // "no-unused-expression": false, 38 | // "no-use-before-declare": false, 39 | "no-var-keyword": true, 40 | // "no-var-requires": false, 41 | // "strict-type-predicates": true, 42 | // "object-literal-sort-keys": false, 43 | // "one-line": [ true, "check-open-brace", "check-whitespace", "check-else", "check-catch" ], 44 | // "quotemark": [ true, "single", "avoid-escape" ], 45 | // "radix": true, 46 | "semicolon": [ true, "never" ], 47 | // "trailing-comma": [ true, { 48 | // "multiline": "never", 49 | // "singleline": "never" 50 | // } ], 51 | // "triple-equals": [ true, "allow-null-check" ], 52 | // "typedef": false, 53 | "typedef-whitespace": [ true, { 54 | "call-signature": "space", 55 | "index-signature": "space", 56 | "parameter": "space", 57 | "property-declaration": "space", 58 | "variable-declaration": "space" 59 | }, { 60 | "call-signature": "onespace", 61 | "index-signature": "onespace", 62 | "parameter": "onespace", 63 | "property-declaration": "onespace", 64 | "variable-declaration": "onespace" 65 | } ], 66 | // "variable-name": [ true, "check-format", "allow-leading-underscore", "ban-keywords" ], 67 | "whitespace": [ 68 | true, 69 | "check-branch", "check-decl", "check-operator", "check-module", "check-separator", 70 | "check-type", "check-type-operator", "check-preblock" 71 | ] 72 | }, 73 | 74 | "linterOptions": { 75 | "exclude": [ 76 | ] 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tests/chrono/031_garbage_collection.t.ts: -------------------------------------------------------------------------------- 1 | import { ChronoGraph } from "../../src/chrono/Graph.js" 2 | import { CalculatedValueGen } from "../../src/chrono/Identifier.js" 3 | import { CalculationIterator } from "../../src/primitives/Calculation.js" 4 | 5 | declare const StartTest : any 6 | 7 | StartTest(t => { 8 | 9 | t.it('Should garbage collect unneeded revisions', async t => { 10 | // explicitly set that we don't track history 11 | const graph : ChronoGraph = ChronoGraph.new({ historyLimit : 0 }) 12 | 13 | const box1 = graph.variable(0) 14 | const box2 = graph.variable(0) 15 | 16 | const box1p2 = graph.identifier(function * () { 17 | return (yield box1) + (yield box2) 18 | }) 19 | 20 | const box3 = graph.variable(1) 21 | 22 | const res = graph.identifier(function * () { 23 | return (yield box1p2) + (yield box3) 24 | }) 25 | 26 | // ---------------- 27 | graph.commit() 28 | 29 | t.is(graph.baseRevision.previous, null, "No extra revisions") 30 | 31 | // ---------------- 32 | graph.write(box1, 1) 33 | 34 | graph.commit() 35 | 36 | t.is(graph.baseRevision.previous, null, "No extra revisions") 37 | }) 38 | 39 | 40 | t.it('Garbage collecting should keep data dependencies', async t => { 41 | // explicitly set that we don't track history 42 | const graph : ChronoGraph = ChronoGraph.new({ historyLimit : 0 }) 43 | 44 | const var0 = graph.variableNamed('var0', 1) 45 | 46 | const var1 = graph.variableNamed('var1', 100) 47 | 48 | const var2 = graph.addIdentifier(CalculatedValueGen.new({ 49 | * calculation () : CalculationIterator { 50 | const value : number = (yield var1) as number 51 | 52 | return value + 1 53 | } 54 | })) 55 | 56 | //------------------ 57 | graph.commit() 58 | 59 | // create a revision with `var1 -> var2` edge 60 | t.is(graph.read(var2), 101, 'Correct value') 61 | 62 | // now we create couple of throw-away revisions, which will garbage collect the revision with `var1 -> var2` edge 63 | 64 | //------------------ 65 | graph.write(var0, 2) 66 | 67 | graph.commit() 68 | 69 | //------------------ 70 | graph.write(var0, 3) 71 | 72 | graph.commit() 73 | 74 | // and now making sure the dependency is still alive 75 | //------------------ 76 | graph.write(var1, 10) 77 | 78 | graph.commit() 79 | 80 | t.is(graph.read(var2), 11, 'Correct value') 81 | }) 82 | 83 | }) 84 | -------------------------------------------------------------------------------- /tests/cycle_resolver/010_memoizing.t.ts: -------------------------------------------------------------------------------- 1 | import { CalculateProposed, CycleResolution, CycleResolutionInput, Formula, CycleDescription } from "../../src/cycle_resolver/CycleResolver.js" 2 | 3 | declare const StartTest : any 4 | 5 | StartTest(t => { 6 | 7 | const StartDate = Symbol('StartDate') 8 | const EndDate = Symbol('EndDate') 9 | const Duration = Symbol('Duration') 10 | 11 | const startDateFormula = Formula.new({ 12 | output : StartDate, 13 | inputs : new Set([ Duration, EndDate ]) 14 | }) 15 | 16 | const endDateFormula = Formula.new({ 17 | output : EndDate, 18 | inputs : new Set([ Duration, StartDate ]) 19 | }) 20 | 21 | const durationFormula = Formula.new({ 22 | output : Duration, 23 | inputs : new Set([ StartDate, EndDate ]) 24 | }) 25 | 26 | 27 | const description = CycleDescription.new({ 28 | variables : new Set([ StartDate, EndDate, Duration ]), 29 | formulas : new Set([ startDateFormula, endDateFormula, durationFormula ]) 30 | }) 31 | 32 | const resolutionContext = CycleResolution.new({ 33 | description : description, 34 | defaultResolutionFormulas : new Set([ endDateFormula ]) 35 | }) 36 | 37 | let input 38 | 39 | t.beforeEach(t => { 40 | input = CycleResolutionInput.new({ context : resolutionContext }) 41 | }) 42 | 43 | 44 | t.it('Should only calculate the resolution once', t => { 45 | const spy = t.spyOn(resolutionContext, 'buildResolution') 46 | 47 | const resolution1 = resolutionContext.resolve(input) 48 | 49 | t.expect(spy).toHaveBeenCalled(1) 50 | 51 | t.isDeeply( 52 | resolution1, 53 | new Map([ 54 | [ StartDate, CalculateProposed ], 55 | [ EndDate, CalculateProposed ], 56 | [ Duration, CalculateProposed ] 57 | ]) 58 | ) 59 | 60 | //------------------------- 61 | spy.reset() 62 | 63 | const resolution2 = resolutionContext.resolve(input) 64 | 65 | t.expect(spy).toHaveBeenCalled(0) 66 | 67 | t.isStrict(resolution1, resolution2, "Cached resolution used") 68 | 69 | //------------------------- 70 | spy.reset() 71 | 72 | const input2 = CycleResolutionInput.new({ context : resolutionContext }) 73 | 74 | input2.addProposedValueFlag(StartDate) 75 | input2.addProposedValueFlag(EndDate) 76 | 77 | const resolution3 = resolutionContext.resolve(input2) 78 | 79 | t.expect(spy).toHaveBeenCalled(1) 80 | }) 81 | }) 82 | -------------------------------------------------------------------------------- /benchmarks/lazy_property.js: -------------------------------------------------------------------------------- 1 | const lazyBuild = (target, property, value) => { 2 | Object.defineProperty(target, property, { value }) 3 | 4 | return value 5 | } 6 | 7 | 8 | const lazyBuild2 = (target, storage, builder) => { 9 | if (target[ storage ] !== undefined) return target[ storage ] 10 | 11 | return target[ storage ] = builder() 12 | } 13 | 14 | 15 | const lazyBuild3 = (target, storage, builder) => { 16 | if (target.hasOwnProperty(storage)) return target[ storage ] 17 | 18 | return target[ storage ] = builder() 19 | } 20 | 21 | 22 | 23 | class Benchmark { 24 | 25 | constructor () { 26 | this.nonLazyProperty = 1 27 | } 28 | 29 | buildLazyProp () { 30 | return 1 31 | } 32 | 33 | 34 | getLazyPropertyTripleEqual () { 35 | if (this.lazyProp !== undefined) return this.lazyProp 36 | 37 | return this.lazyProp = this.buildLazyProp() 38 | } 39 | 40 | 41 | getLazyPropertyHasOwn () { 42 | if (this.lazyProp2 !== undefined) return this.lazyProp2 43 | 44 | return this.lazyProp2 = this.buildLazyProp() 45 | } 46 | 47 | 48 | get lazyProperty () { 49 | return lazyBuild(this, 'lazyProperty', 1) 50 | } 51 | 52 | 53 | get lazyProperty2 () { 54 | return lazyBuild2(this, '$lazyProperty2', () => 1) 55 | } 56 | 57 | 58 | get lazyProperty3 () { 59 | return lazyBuild3(this, '$lazyProperty3', () => 1) 60 | } 61 | 62 | 63 | get lazyProperty4 () { 64 | if (this.lazyProp4 !== undefined) return this.lazyProp4 65 | 66 | return this.lazyProp4 = 1 67 | } 68 | 69 | } 70 | 71 | 72 | const instances = [ ...new Array(10000) ].map((value, index) => new Test()) 73 | 74 | 75 | instances.reduce((sum, instance) => sum += instance.lazyProperty + instance.lazyProperty + instance.lazyProperty + instance.lazyProperty + instance.lazyProperty + instance.lazyProperty) 76 | 77 | instances.reduce((sum, instance) => sum += instance.lazyProperty2 + instance.lazyProperty2 + instance.lazyProperty2 + instance.lazyProperty2 + instance.lazyProperty2 + instance.lazyProperty2) 78 | 79 | instances.reduce((sum, instance) => sum += instance.getLazyProperty() + instance.getLazyProperty() + instance.getLazyProperty() + instance.getLazyProperty() + instance.getLazyProperty() + instance.getLazyProperty()) 80 | 81 | instances.reduce((sum, instance) => sum += instance.nonLazyProperty + instance.nonLazyProperty + instance.nonLazyProperty + instance.nonLazyProperty + instance.nonLazyProperty + instance.nonLazyProperty) 82 | 83 | instances.reduce((sum, instance) => sum += instance.lazyProperty4 + instance.lazyProperty4 + instance.lazyProperty4 + instance.lazyProperty4 + instance.lazyProperty4 + instance.lazyProperty4) 84 | -------------------------------------------------------------------------------- /src/replica/Replica.ts: -------------------------------------------------------------------------------- 1 | import { ChronoGraph } from "../chrono/Graph.js" 2 | import { Identifier } from "../chrono/Identifier.js" 3 | import { ClassUnion, Mixin } from "../class/Mixin.js" 4 | import { Schema } from "../schema/Schema.js" 5 | import { Entity } from "./Entity.js" 6 | 7 | export enum ReadMode { 8 | Current, 9 | Previous, 10 | ProposedOrPrevious, 11 | CurrentOrProposedOrPrevious 12 | } 13 | 14 | 15 | //--------------------------------------------------------------------------------------------------------------------- 16 | /** 17 | * Reactive graph, operating on the set of entities (see [[Entity]] and [[EntityMeta]]), each having a set of fields (see [[Field]]). 18 | * 19 | * Entities are mapped to JS classes and fields - to their properties, decorated with [[field]]. 20 | * 21 | * The calculation function for some field can be mapped to the class method, using the [[calculate]] decorator. 22 | * 23 | * An example of usage: 24 | * 25 | * ```ts 26 | * class Author extends Entity.mix(Base) { 27 | * @field() 28 | * firstName : string 29 | * @field() 30 | * lastName : string 31 | * @field() 32 | * fullName : string 33 | * 34 | * @calculate('fullName') 35 | * calculateFullName () : string { 36 | * return this.firstName + ' ' + this.lastName 37 | * } 38 | * } 39 | * ``` 40 | */ 41 | export class Replica extends Mixin( 42 | [ ChronoGraph ], 43 | (base : ClassUnion) => 44 | 45 | class Replica extends base { 46 | schema : Schema 47 | 48 | /** 49 | * Replica re-defines the default value of the `autoCommit` property to `true`. 50 | */ 51 | autoCommit : boolean = true 52 | 53 | readMode : ReadMode = ReadMode.Current 54 | 55 | /** 56 | * Add entity instance to the replica 57 | * 58 | * @param entity 59 | */ 60 | addEntity (entity : Entity) { 61 | entity.enterGraph(this) 62 | } 63 | 64 | 65 | /** 66 | * Add several entity instances to the replica 67 | * 68 | * @param entity 69 | */ 70 | addEntities (entities : Entity[]) { 71 | entities.forEach(entity => this.addEntity(entity)) 72 | } 73 | 74 | 75 | /** 76 | * Remove entity instance from the replica 77 | * 78 | * @param entity 79 | */ 80 | removeEntity (entity : Entity) { 81 | entity.leaveGraph(this) 82 | } 83 | 84 | 85 | /** 86 | * Remove several entity instances from the replica 87 | * 88 | * @param entity 89 | */ 90 | removeEntities (entities : Entity[]) { 91 | entities.forEach(entity => this.removeEntity(entity)) 92 | } 93 | }){} 94 | -------------------------------------------------------------------------------- /tests/chrono/030_transaction_reject.t.ts: -------------------------------------------------------------------------------- 1 | import { Reject } from "../../src/chrono/Effect.js" 2 | import { ChronoGraph } from "../../src/chrono/Graph.js" 3 | 4 | declare const StartTest : any 5 | 6 | StartTest(t => { 7 | 8 | t.it('Should be able to reject transaction using graph api', async t => { 9 | const graph : ChronoGraph = ChronoGraph.new() 10 | 11 | const i1 = graph.variableNamed('i1', 0) 12 | const i2 = graph.variableNamed('i2', 10) 13 | const i3 = graph.variableNamed('i3', 0) 14 | 15 | const c1 = graph.identifierNamed('c1', function* () { 16 | return (yield i1) + (yield i2) 17 | }) 18 | 19 | const c2 = graph.identifierNamed('c2', function* () { 20 | return (yield c1) + 1 21 | }) 22 | 23 | const c3 = graph.identifierNamed('c3', function* () { 24 | return (yield c2) + (yield i3) 25 | }) 26 | 27 | graph.commit() 28 | 29 | // ---------------- 30 | const nodes = [ i1, i2, i3, c1, c2, c3 ] 31 | 32 | t.isDeeply(nodes.map(node => graph.read(node)), [ 0, 10, 0, 10, 11, 11 ], "Correct result calculated #1") 33 | 34 | // ---------------- 35 | graph.write(i1, 1) 36 | graph.write(i2, 1) 37 | graph.write(i3, 1) 38 | 39 | t.isDeeply(nodes.map(node => graph.read(node)), [ 1, 1, 1, 2, 3, 4 ], "Correct result calculated #1") 40 | 41 | graph.reject() 42 | 43 | t.isDeeply(nodes.map(node => graph.read(node)), [ 0, 10, 0, 10, 11, 11 ], "Graph state rolled back to previous commit") 44 | }) 45 | 46 | 47 | t.it('Should be able to reject transaction using effect', async t => { 48 | const graph : ChronoGraph = ChronoGraph.new() 49 | 50 | const i1 = graph.variableNamed('i1', 0) 51 | const i2 = graph.variableNamed('i2', 10) 52 | 53 | const c1 = graph.identifierNamed('c1', function* () { 54 | const sum : number = (yield i1) + (yield i2) 55 | 56 | if (sum > 10) yield Reject('Too big') 57 | 58 | return sum 59 | }) 60 | 61 | graph.commit() 62 | 63 | // ---------------- 64 | const nodes = [ i1, i2, c1 ] 65 | 66 | t.isDeeply(nodes.map(node => graph.read(node)), [ 0, 10, 10 ], "Correct result calculated #1") 67 | 68 | // ---------------- 69 | graph.write(i1, 8) 70 | graph.write(i2, 7) 71 | 72 | const result = graph.commit() 73 | 74 | t.like(result.rejectedWith.reason, /Too big/) 75 | 76 | t.isDeeply(nodes.map(node => graph.read(node)), [ 0, 10, 10 ], "Correct result calculated #1") 77 | }) 78 | 79 | }) 80 | -------------------------------------------------------------------------------- /src/util/LeveledQueue.ts: -------------------------------------------------------------------------------- 1 | import { MAX_SMI } from "./Helpers.js" 2 | 3 | // Leveled LIFO queue 4 | 5 | export class LeveledQueue { 6 | length : number = 0 7 | 8 | levels : T[][] = [] 9 | 10 | lowestLevel : number = MAX_SMI 11 | 12 | 13 | getLowestLevel () : number { 14 | for (let i = this.lowestLevel !== MAX_SMI ? this.lowestLevel : 0; i < this.levels.length; i++) { 15 | if (this.levels[ i ]) return this.lowestLevel = i 16 | } 17 | 18 | return this.lowestLevel = MAX_SMI 19 | } 20 | 21 | 22 | takeLowestLevel () : T[] { 23 | for (let i = this.lowestLevel !== MAX_SMI ? this.lowestLevel : 0; i < this.levels.length; i++) { 24 | const level = this.levels[ i ] 25 | 26 | if (level) { 27 | this.length -= level.length 28 | 29 | this.levels[ i ] = null 30 | 31 | this.lowestLevel = i + 1 32 | 33 | return level 34 | } 35 | } 36 | } 37 | 38 | 39 | // resetCachedPosition () { 40 | // this.lowestLevel = MAX_SMI 41 | // } 42 | 43 | 44 | // last () { 45 | // for (let i = this.lowestLevel !== MAX_SMI ? this.lowestLevel : 0; i < this.levels.length; i++) { 46 | // const level = this.levels[ i ] 47 | // 48 | // if (level && level.length > 0) { 49 | // this.lowestLevel = i 50 | // 51 | // return level[ level.length - 1 ] 52 | // } 53 | // } 54 | // } 55 | 56 | 57 | pop () : T { 58 | for (let i = this.lowestLevel !== MAX_SMI ? this.lowestLevel : 0; i < this.levels.length; i++) { 59 | const level = this.levels[ i ] 60 | 61 | this.lowestLevel = i 62 | 63 | if (level && level.length > 0) { 64 | this.length-- 65 | 66 | return level.pop() 67 | } 68 | } 69 | 70 | this.lowestLevel = MAX_SMI 71 | } 72 | 73 | 74 | push (el : T) { 75 | const elLevel = el.level 76 | 77 | let level : T[] = this.levels[ elLevel ] 78 | 79 | if (!level) { 80 | // avoid holes in the array 81 | for (let i = this.levels.length; i < elLevel; i++) this.levels[ i ] = null 82 | 83 | level = this.levels[ elLevel ] = [] 84 | } 85 | 86 | level.push(el) 87 | 88 | this.length++ 89 | 90 | if (elLevel < this.lowestLevel) this.lowestLevel = elLevel 91 | } 92 | 93 | 94 | * [Symbol.iterator] () : Iterable { 95 | for (let i = 0; i < this.levels.length; i++) { 96 | const level = this.levels[ i ] 97 | 98 | if (level) yield* level 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /tests/chrono/013_sync_calculation.t.ts: -------------------------------------------------------------------------------- 1 | import { ChronoGraph } from "../../src/chrono/Graph.js" 2 | import { CalculatedValueGen, CalculatedValueSync } from "../../src/chrono/Identifier.js" 3 | import { SyncEffectHandler } from "../../src/chrono/Transaction.js" 4 | import { CalculationIterator } from "../../src/primitives/Calculation.js" 5 | 6 | declare const StartTest : any 7 | 8 | StartTest(t => { 9 | 10 | t.it('Should not re-entry synchronous calculations', async t => { 11 | const graph : ChronoGraph = ChronoGraph.new() 12 | 13 | const var1 = graph.variableNamed('v1', 1) 14 | 15 | let count : number = 0 16 | 17 | const iden1 = graph.addIdentifier(CalculatedValueSync.new({ 18 | calculation (YIELD : SyncEffectHandler) : number { 19 | count++ 20 | 21 | return YIELD(var1) + 1 22 | } 23 | })) 24 | 25 | const iden2 = graph.addIdentifier(CalculatedValueSync.new({ 26 | calculation (YIELD : SyncEffectHandler) : number { 27 | count++ 28 | 29 | return YIELD(iden1) + 1 30 | } 31 | })) 32 | 33 | const iden3 = graph.addIdentifier(CalculatedValueSync.new({ 34 | calculation (YIELD : SyncEffectHandler) : number { 35 | count++ 36 | 37 | return YIELD(iden2) + 1 38 | } 39 | })) 40 | 41 | graph.commit() 42 | 43 | t.is(graph.read(iden1), 2, 'Correct value') 44 | t.is(graph.read(iden2), 3, 'Correct value') 45 | t.is(graph.read(iden3), 4, 'Correct value') 46 | 47 | t.is(count, 3, 'Calculated every identifier only once') 48 | }) 49 | 50 | 51 | t.it('Should not re-entry gen calculations', async t => { 52 | const graph : ChronoGraph = ChronoGraph.new() 53 | 54 | const var1 = graph.variableNamed('v1', 1) 55 | 56 | let count : number = 0 57 | 58 | const iden1 = graph.addIdentifier(CalculatedValueGen.new({ 59 | *calculation (YIELD : SyncEffectHandler) : CalculationIterator { 60 | count++ 61 | 62 | return (yield var1) + 1 63 | } 64 | })) 65 | 66 | const iden2 = graph.addIdentifier(CalculatedValueGen.new({ 67 | *calculation (YIELD : SyncEffectHandler) : CalculationIterator { 68 | count++ 69 | 70 | return (yield iden1) + 1 71 | } 72 | })) 73 | 74 | const iden3 = graph.addIdentifier(CalculatedValueGen.new({ 75 | *calculation (YIELD : SyncEffectHandler) : CalculationIterator { 76 | count++ 77 | 78 | return (yield iden2) + 1 79 | } 80 | })) 81 | 82 | graph.commit() 83 | 84 | t.is(graph.read(iden1), 2, 'Correct value') 85 | t.is(graph.read(iden2), 3, 'Correct value') 86 | t.is(graph.read(iden3), 4, 'Correct value') 87 | 88 | t.is(count, 3, 'Calculated every identifier only once') 89 | }) 90 | 91 | }) 92 | -------------------------------------------------------------------------------- /tests/chrono/013_async_calculation.t.ts: -------------------------------------------------------------------------------- 1 | import { ChronoGraph } from "../../src/chrono/Graph.js" 2 | import { CalculatedValueGen } from "../../src/chrono/Identifier.js" 3 | import { SyncEffectHandler } from "../../src/chrono/Transaction.js" 4 | import { CalculationIterator } from "../../src/primitives/Calculation.js" 5 | import { delay } from "../../src/util/Helpers.js" 6 | 7 | declare const StartTest : any 8 | 9 | StartTest(t => { 10 | 11 | t.it('Should not re-entry async read', async t => { 12 | const graph : ChronoGraph = ChronoGraph.new() 13 | 14 | const var1 = graph.variableNamed('v1', 1) 15 | 16 | let count = 0 17 | 18 | const iden1 = graph.addIdentifier(CalculatedValueGen.new({ 19 | sync : false, 20 | 21 | *calculation (YIELD : SyncEffectHandler) : CalculationIterator { 22 | count++ 23 | 24 | yield delay(10) 25 | 26 | return (yield var1) + 1 27 | } 28 | })) 29 | 30 | 31 | const promise1 = graph.readAsync(iden1) 32 | const promise2 = graph.readAsync(iden1) 33 | 34 | t.is(await promise1, 2, 'Correct value') 35 | t.is(await promise2, 2, 'Correct value') 36 | t.is(count, 1, 'Calculated once') 37 | }) 38 | 39 | 40 | t.it('Should not re-entry async gen calculations that has been partially read already', async t => { 41 | const graph : ChronoGraph = ChronoGraph.new() 42 | 43 | const var1 = graph.variableNamed('var1', 1) 44 | 45 | let count : number = 0 46 | 47 | const iden1 = graph.addIdentifier(CalculatedValueGen.new({ 48 | name : 'iden1', 49 | sync : false, 50 | 51 | *calculation (YIELD : SyncEffectHandler) : CalculationIterator { 52 | count++ 53 | 54 | yield delay(10) 55 | 56 | return (yield var1) + 1 57 | } 58 | })) 59 | 60 | const iden2 = graph.addIdentifier(CalculatedValueGen.new({ 61 | name : 'iden2', 62 | sync : false, 63 | 64 | *calculation (YIELD : SyncEffectHandler) : CalculationIterator { 65 | count++ 66 | 67 | yield delay(10) 68 | 69 | return (yield iden1) + 1 70 | } 71 | })) 72 | 73 | const iden3 = graph.addIdentifier(CalculatedValueGen.new({ 74 | name : 'iden3', 75 | sync : false, 76 | 77 | *calculation (YIELD : SyncEffectHandler) : CalculationIterator { 78 | count++ 79 | 80 | yield delay(10) 81 | 82 | return (yield iden2) + 1 83 | } 84 | })) 85 | 86 | const promise1 = graph.readAsync(iden2) 87 | 88 | // t.is(graph.read(iden1), 2, 'Correct value') 89 | // t.is(graph.read(iden2), 3, 'Correct value') 90 | 91 | t.is(await graph.readAsync(iden3), 4, 'Correct value') 92 | // t.is(await promise1, 3, 'Correct value') 93 | 94 | t.is(count, 3, 'Calculated every identifier only once') 95 | }) 96 | 97 | }) 98 | -------------------------------------------------------------------------------- /src/schema/Schema.ts: -------------------------------------------------------------------------------- 1 | import { Base } from "../class/Base.js" 2 | import { ensureEntityOnPrototype, EntityConstructor } from "../replica/Entity.js" 3 | import { EntityMeta } from "./EntityMeta.js" 4 | import { Name } from "./Field.js" 5 | 6 | 7 | //--------------------------------------------------------------------------------------------------------------------- 8 | /** 9 | * This class describes a schema. Schemas are not used yet in ChronoGraph. 10 | * 11 | * Schema is just a collection of entities ([[EntityMeta]]) 12 | */ 13 | export class Schema extends Base { 14 | /** 15 | * The name of the schema 16 | */ 17 | name : Name 18 | 19 | entities : Map = new Map() 20 | 21 | /** 22 | * Checks whether the schema has an entity with the given name. 23 | * 24 | * @param name 25 | */ 26 | hasEntity (name : Name) : boolean { 27 | return this.entities.has(name) 28 | } 29 | 30 | 31 | /** 32 | * Returns an entity with the given name or `undefined` if there's no such in this schema 33 | * 34 | * @param name 35 | */ 36 | getEntity (name : Name) : EntityMeta { 37 | return this.entities.get(name) 38 | } 39 | 40 | 41 | /** 42 | * Adds an entity to the schema. 43 | * @param entity 44 | */ 45 | addEntity (entity : EntityMeta) : EntityMeta { 46 | const name = entity.name 47 | 48 | if (!name) throw new Error(`Entity must have a name`) 49 | if (this.hasEntity(name)) throw new Error(`Entity with name [${String(name)}] already exists`) 50 | 51 | entity.schema = this 52 | 53 | this.entities.set(name, entity) 54 | 55 | return entity 56 | } 57 | 58 | 59 | /** 60 | * Returns a class decorator which can be used to decorate classes as entities. 61 | */ 62 | getEntityDecorator () : ClassDecorator { 63 | // @ts-ignore : https://github.com/Microsoft/TypeScript/issues/29828 64 | return (target : EntityConstructor) => { 65 | const entity = entityDecoratorBody(target) 66 | 67 | this.addEntity(entity) 68 | 69 | return target 70 | } 71 | } 72 | } 73 | 74 | 75 | export const entityDecoratorBody = (target : T) => { 76 | const name = target.name 77 | if (!name) throw new Error(`Can't add entity - the target class has no name`) 78 | 79 | return ensureEntityOnPrototype(target.prototype) 80 | } 81 | 82 | 83 | /** 84 | * Entity decorator. It is required to be applied only if entity declares no field. 85 | * If record declares any field, there no strict need to apply this decorator. 86 | * Its better to do this anyway, for consistency. 87 | * 88 | * ```ts 89 | * @entity() 90 | * class Author extends Entity.mix(Base) { 91 | * } 92 | * 93 | * @entity() 94 | * class SpecialAuthor extends Author { 95 | * } 96 | * ``` 97 | */ 98 | export const entity = () : ClassDecorator => { 99 | // @ts-ignore : https://github.com/Microsoft/TypeScript/issues/29828 100 | return (target : T) : T => { 101 | entityDecoratorBody(target) 102 | 103 | return target 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/schema/EntityMeta.ts: -------------------------------------------------------------------------------- 1 | import { Base } from "../class/Base.js" 2 | import { Field, Name } from "./Field.js" 3 | import { Schema } from "./Schema.js" 4 | 5 | //--------------------------------------------------------------------------------------------------------------------- 6 | /** 7 | * This class describes an entity. Entity is simply a collection of [[Field]]s. Entity also may have a parent entity, 8 | * from which it inherit the fields. 9 | */ 10 | export class EntityMeta extends Base { 11 | /** 12 | * The name of the entity 13 | */ 14 | name : Name = undefined 15 | 16 | ownFields : Map = new Map() 17 | 18 | schema : Schema = undefined 19 | 20 | /** 21 | * The parent entity 22 | */ 23 | parentEntity : EntityMeta 24 | 25 | $skeleton : object = {} 26 | 27 | 28 | /** 29 | * Checks whether the entity has a field with given name (possibly inherited from parent entity). 30 | * 31 | * @param name 32 | */ 33 | hasField (name : Name) : boolean { 34 | return this.getField(name) !== undefined 35 | } 36 | 37 | 38 | /** 39 | * Returns a field with given name (possibly inherited) or `undefined` if there's none. 40 | * 41 | * @param name 42 | */ 43 | getField (name : Name) : Field { 44 | return this.allFields.get(name) 45 | } 46 | 47 | 48 | /** 49 | * Adds a field to this entity. 50 | * 51 | * @param field 52 | */ 53 | addField (field : T) : T { 54 | const name = field.name 55 | if (!name) throw new Error(`Field must have a name`) 56 | 57 | if (this.ownFields.has(name)) throw new Error(`Field with name [${name}] already exists`) 58 | 59 | field.entity = this 60 | 61 | this.ownFields.set(name, field) 62 | 63 | return field 64 | } 65 | 66 | 67 | forEachParent (func : (entity : EntityMeta) => any) { 68 | let entity : EntityMeta = this 69 | 70 | while (entity) { 71 | func(entity) 72 | 73 | entity = entity.parentEntity 74 | } 75 | } 76 | 77 | 78 | $allFields : Map = undefined 79 | 80 | 81 | get allFields () : Map { 82 | if (this.$allFields !== undefined) return this.$allFields 83 | 84 | const allFields : Map = new Map() 85 | const visited : Set = new Set() 86 | 87 | this.forEachParent(entity => { 88 | entity.ownFields.forEach((field : Field, name : Name) => { 89 | if (!visited.has(name)) { 90 | visited.add(name) 91 | 92 | allFields.set(name, field) 93 | } 94 | }) 95 | }) 96 | 97 | return this.$allFields = allFields 98 | } 99 | 100 | 101 | /** 102 | * Iterator for all fields of this entity (including inherited). 103 | * 104 | * @param func 105 | */ 106 | forEachField (func : (field : Field, name : Name) => any) { 107 | this.allFields.forEach(func) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [//]: # (The canonical source of this file is '/docs_src/README.md') 2 | [//]: # (Do not edit the /README.md directly) 3 | 4 | ChronoGraph 5 | =========== 6 | 7 | Chronograph is an open-source reactive state management system, implemented in TypeScript and developed at [Bryntum](https://www.bryntum.com/). It powers the business logic of the [Bryntum Gantt](https://www.bryntum.com/examples/gantt/advanced). 8 | 9 | ChronoGraph has the following features: 10 | 11 | - Cancelable transactions 12 | - O(1) undo/redo 13 | - Lazy/strict, sync/async computations 14 | - Data branching 15 | - Mixed computational unit (user input/calculated value) 16 | - Unlimited stack depth 17 | - Disciplined approach to cyclic computations 18 | - Entity/Relation framework 19 | 20 | And the following features are very feasible: 21 | 22 | - Possibility to split the whole computation into chunks (think `requestAnimationFrame`) 23 | - Possibility for breadth-first computation (think network latency) 24 | - Mapping to SQL 25 | - Mapping to GraphQL 26 | 27 | Reactive computations has become a popular trend recently, popularized by the React, Vue and Angular triade. However, all of the latter are user interface frameworks. 28 | 29 | ChronoGraph, in contrast, focuses on reactive computations, describing generic data graphs (such as Gantt project plans). It is designed to handle extremely large graphs - up to several hundred thousands "atoms". It also includes a small Entity/Relation framework, which maps to regular ES6 classes. 30 | 31 | 32 | 33 | Installation 34 | ============= 35 | 36 | ``` 37 | npm install @bryntum/chronograph --save 38 | ``` 39 | 40 | Documentation 41 | ============= 42 | 43 | You should be able to quickly pick up the base concept of reactivity from the [Basic features](https://bryntum.github.io/chronograph/docs/modules/_src_guides_basicfeatures_.html#basicfeaturesguide) guide. 44 | 45 | To find out about the remaining (and most interesting) features of ChronoGraph, continue to the [Advanced features](https://bryntum.github.io/chronograph/docs/modules/_src_guides_advancedfeatures_.html#advancedfeaturesguide) guide. 46 | 47 | The guides contain extensive references to the [API docs](https://bryntum.github.io/chronograph/docs/) 48 | 49 | The API surface is currently intentionally small and some features are not documented. Please [reach out](https://discord.gg/jErxFxY) if you need something specific. 50 | 51 | 52 | Benchmarks 53 | ========== 54 | 55 | ChronoGraph aims to have excellent performance. To reason about it objectively, we wrote a benchmark suite. 56 | More details in the [Benchmarks](https://bryntum.github.io/chronograph/docs/modules/_src_guides_benchmarks_.html#benchmarksguide) guide. 57 | 58 | Connect 59 | ======= 60 | 61 | We welcome all feedback. Please tell us what works well in ChronoGraph, what causes trouble and any other features you would like to see implemented. 62 | 63 | Please report any found bugs in the [issues tracker](https://github.com/bryntum/chronograph/issues) 64 | 65 | Ask questions in the [forum](https://bryntum.com/forum/viewforum.php?f=53) 66 | 67 | Chat live at [Discord](https://discord.gg/jErxFxY) 68 | 69 | Follow the [development blog](https://dev.to/chronograph/) 70 | 71 | 72 | COPYRIGHT AND LICENSE 73 | ================= 74 | 75 | MIT License 76 | 77 | Copyright (c) 2018-2020 Bryntum, Nickolay Platonov 78 | -------------------------------------------------------------------------------- /src/chrono/TransactionCycleDetectionWalkContext.ts: -------------------------------------------------------------------------------- 1 | import { Base } from "../class/Base.js" 2 | import { NOT_VISITED, OnCycleAction, VisitInfo, WalkContext, WalkStep } from "../graph/WalkDepth.js" 3 | import { Identifier } from "./Identifier.js" 4 | import { Transaction } from "./Transaction.js" 5 | import { Quark } from "./Quark.js" 6 | 7 | 8 | //--------------------------------------------------------------------------------------------------------------------- 9 | export class ComputationCycle extends Base { 10 | cycle : Identifier[] 11 | 12 | activeEntry : Quark 13 | 14 | requestedEntry : Quark 15 | 16 | toString () : string { 17 | const cycleIdentifiers = [] 18 | const cycleEvents = [] 19 | 20 | this.cycle.forEach(({ name, context }) => { 21 | cycleIdentifiers.push(name) 22 | 23 | if (cycleEvents[cycleEvents.length - 1] !== context) cycleEvents.push(context) 24 | }) 25 | 26 | return 'events: \n' + 27 | cycleEvents.map(event => '#' + event.id).join(' => ') + 28 | '\n\nidentifiers: \n' + 29 | cycleIdentifiers.join('\n') 30 | 31 | // return this.cycle.map(identifier => { 32 | // return identifier.name 33 | 34 | // // //@ts-ignore 35 | // // const sourcePoint : SourceLinePoint = identifier.SOURCE_POINT 36 | // // 37 | // // if (!sourcePoint) return identifier.name 38 | // // 39 | // // const firstEntry = sourcePoint.stackEntries[ 0 ] 40 | // // 41 | // // if (firstEntry) { 42 | // // return `${identifier}\n yielded at ${firstEntry.sourceFile}:${firstEntry.sourceLine}:${firstEntry.sourceCharPos || ''}` 43 | // // } else 44 | // // return identifier.name 45 | // }).join(' => \n') 46 | } 47 | } 48 | 49 | 50 | //--------------------------------------------------------------------------------------------------------------------- 51 | export class TransactionCycleDetectionWalkContext extends WalkContext { 52 | 53 | // baseRevision : Revision = undefined 54 | 55 | transaction : Transaction = undefined 56 | 57 | 58 | onCycle (node : Identifier, stack : WalkStep[]) : OnCycleAction { 59 | return OnCycleAction.Cancel 60 | } 61 | 62 | 63 | doCollectNext (from : Identifier, to : Identifier, toVisit : WalkStep[]) { 64 | let visit : VisitInfo = this.visited.get(to) 65 | 66 | if (!visit) { 67 | visit = { visitedAt : NOT_VISITED, visitEpoch : this.currentEpoch } 68 | 69 | this.visited.set(to, visit) 70 | } 71 | 72 | toVisit.push({ node : to, from, label : undefined }) 73 | } 74 | 75 | 76 | collectNext (from : Identifier, toVisit : WalkStep[]) { 77 | const latestEntry = this.transaction.getLatestEntryFor(from) 78 | 79 | if (latestEntry) { 80 | latestEntry.outgoingInTheFutureTransactionCb(this.transaction, outgoingEntry => { 81 | this.doCollectNext(from, outgoingEntry.identifier, toVisit) 82 | }) 83 | } 84 | 85 | // for (const outgoingIdentifier of visitInfo.getOutgoing().keys()) { 86 | // this.doCollectNext(from, outgoingIdentifier, toVisit) 87 | // } 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /docs_src/README.md: -------------------------------------------------------------------------------- 1 | [//]: # (The canonical source of this file is '/docs_src/README.md') 2 | [//]: # (Do not edit the /README.md directly) 3 | 4 | ChronoGraph 5 | =========== 6 | 7 | Chronograph is an open-source reactive state management system, implemented in TypeScript and developed at [Bryntum](https://www.bryntum.com/). It powers the business logic of the [Bryntum Gantt](https://www.bryntum.com/examples/gantt/advanced). 8 | 9 | ChronoGraph has the following features: 10 | 11 | - Cancelable transactions 12 | - O(1) undo/redo 13 | - Lazy/strict, sync/async computations 14 | - Data branching 15 | - Mixed computational unit (user input/calculated value) 16 | - Unlimited stack depth 17 | - Disciplined approach to cyclic computations 18 | - Entity/Relation framework 19 | 20 | And the following features are very feasible: 21 | 22 | - Possibility to split the whole computation into chunks (think `requestAnimationFrame`) 23 | - Possibility for breadth-first computation (think network latency) 24 | - Mapping to SQL 25 | - Mapping to GraphQL 26 | 27 | Reactive computations has become a popular trend recently, popularized by the React, Vue and Angular triade. However, all of the latter are user interface frameworks. 28 | 29 | ChronoGraph, in contrast, focuses on reactive computations, describing generic data graphs (such as Gantt project plans). It is designed to handle extremely large graphs - up to several hundred thousands "atoms". It also includes a small Entity/Relation framework, which maps to regular ES6 classes. 30 | 31 | 32 | 33 | Installation 34 | ============= 35 | 36 | ``` 37 | npm install @bryntum/chronograph --save 38 | ``` 39 | 40 | Documentation 41 | ============= 42 | 43 | You should be able to quickly pick up the base concept of reactivity from the [Basic features](https://bryntum.github.io/chronograph/docs/modules/_src_guides_basicfeatures_.html#basicfeaturesguide) guide. 44 | 45 | To find out about the remaining (and most interesting) features of ChronoGraph, continue to the [Advanced features](https://bryntum.github.io/chronograph/docs/modules/_src_guides_advancedfeatures_.html#advancedfeaturesguide) guide. 46 | 47 | The guides contain extensive references to the [API docs](https://bryntum.github.io/chronograph/docs/) 48 | 49 | The API surface is currently intentionally small and some features are not documented. Please [reach out](https://discord.gg/jErxFxY) if you need something specific. 50 | 51 | 52 | Benchmarks 53 | ========== 54 | 55 | ChronoGraph aims to have excellent performance. To reason about it objectively, we wrote a benchmark suite. 56 | More details in the [Benchmarks](https://bryntum.github.io/chronograph/docs/modules/_src_guides_benchmarks_.html#benchmarksguide) guide. 57 | 58 | Connect 59 | ======= 60 | 61 | We welcome all feedback. Please tell us what works well in ChronoGraph, what causes trouble and any other features you would like to see implemented. 62 | 63 | Please report any found bugs in the [issues tracker](https://github.com/bryntum/chronograph/issues) 64 | 65 | Ask questions in the [forum](https://bryntum.com/forum/viewforum.php?f=53) 66 | 67 | Chat live at [Discord](https://discord.gg/jErxFxY) 68 | 69 | Follow the [development blog](https://dev.to/chronograph/) 70 | 71 | 72 | COPYRIGHT AND LICENSE 73 | ================= 74 | 75 | MIT License 76 | 77 | Copyright (c) 2018-2020 Bryntum, Nickolay Platonov 78 | -------------------------------------------------------------------------------- /tests/cycle_resolver/060_sedwu_fixed_units.t.ts: -------------------------------------------------------------------------------- 1 | import { CalculateProposed, CycleResolution, CycleResolutionInput, Formula, CycleDescription } from "../../src/cycle_resolver/CycleResolver.js" 2 | 3 | declare const StartTest : any 4 | 5 | StartTest(t => { 6 | 7 | const StartDateVar = Symbol('StartDate') 8 | const EndDateVar = Symbol('EndDate') 9 | const DurationVar = Symbol('Duration') 10 | const EffortVar = Symbol('EffortVar') 11 | const UnitsVar = Symbol('UnitsVar') 12 | 13 | const startDateFormula = Formula.new({ 14 | output : StartDateVar, 15 | inputs : new Set([ DurationVar, EndDateVar ]) 16 | }) 17 | 18 | const endDateFormula = Formula.new({ 19 | output : EndDateVar, 20 | inputs : new Set([ DurationVar, StartDateVar ]) 21 | }) 22 | 23 | const durationFormula = Formula.new({ 24 | output : DurationVar, 25 | inputs : new Set([ StartDateVar, EndDateVar ]) 26 | }) 27 | 28 | const effortFormula = Formula.new({ 29 | output : EffortVar, 30 | inputs : new Set([ StartDateVar, EndDateVar, UnitsVar ]) 31 | }) 32 | 33 | const unitsFormula = Formula.new({ 34 | output : UnitsVar, 35 | inputs : new Set([ StartDateVar, EndDateVar, EffortVar ]) 36 | }) 37 | 38 | const endDateByEffortFormula = Formula.new({ 39 | output : EndDateVar, 40 | inputs : new Set([ StartDateVar, EffortVar, UnitsVar ]) 41 | }) 42 | 43 | const startDateByEffortFormula = Formula.new({ 44 | output : StartDateVar, 45 | inputs : new Set([ EndDateVar, EffortVar, UnitsVar ]) 46 | }) 47 | 48 | const fixedUnitsDescription = CycleDescription.new({ 49 | variables : new Set([ StartDateVar, EndDateVar, DurationVar, EffortVar, UnitsVar ]), 50 | formulas : new Set([ 51 | endDateByEffortFormula, 52 | durationFormula, 53 | effortFormula, 54 | unitsFormula, 55 | startDateByEffortFormula, 56 | startDateFormula, 57 | endDateFormula 58 | ]) 59 | }) 60 | 61 | 62 | const fixedUnitsResolutionContext = CycleResolution.new({ 63 | description : fixedUnitsDescription, 64 | defaultResolutionFormulas : new Set([ endDateFormula, endDateByEffortFormula, effortFormula ]) 65 | }) 66 | 67 | 68 | let input : CycleResolutionInput 69 | 70 | t.beforeEach(t => { 71 | input = CycleResolutionInput.new({ context : fixedUnitsResolutionContext }) 72 | }) 73 | 74 | 75 | t.it('Should update end date and effort - set units', t => { 76 | input.addPreviousValueFlag(StartDateVar) 77 | input.addPreviousValueFlag(EndDateVar) 78 | input.addPreviousValueFlag(DurationVar) 79 | input.addPreviousValueFlag(EffortVar) 80 | input.addPreviousValueFlag(UnitsVar) 81 | 82 | input.addProposedValueFlag(UnitsVar) 83 | 84 | const resolution = input.resolution 85 | 86 | t.isDeeply( 87 | resolution, 88 | new Map([ 89 | [ StartDateVar, CalculateProposed ], 90 | [ EndDateVar, endDateFormula.formulaId ], 91 | [ DurationVar, CalculateProposed ], 92 | [ EffortVar, effortFormula.formulaId ], 93 | [ UnitsVar, CalculateProposed ] 94 | ]) 95 | ) 96 | }) 97 | }) 98 | -------------------------------------------------------------------------------- /tests/event/events.t.ts: -------------------------------------------------------------------------------- 1 | import { AnyConstructor, Mixin } from "../../src/class/Mixin.js" 2 | import { Event } from "../../src/event/Event.js" 3 | import { Hook } from "../../src/event/Hook.js" 4 | 5 | declare const StartTest : any 6 | 7 | //--------------------------------------------------------------------------------------------------------------------- 8 | export class ManagedArray extends Mixin( 9 | [ Array ], 10 | (base : AnyConstructor, typeof Array>) => { 11 | 12 | class ManagedArray extends base { 13 | Element : Element 14 | 15 | slice : (start? : number, end? : number) => this[ 'Element' ][] 16 | 17 | // `spliceEvent` start 18 | $spliceEvent : Event<[ this, number, number, this[ 'Element' ][] ]> = undefined 19 | get spliceEvent () : Event<[ this, number, number, this[ 'Element' ][] ]> { 20 | if (this.$spliceEvent !== undefined) return this.$spliceEvent 21 | 22 | return this.$spliceEvent = new Event() 23 | } 24 | // `spliceEvent` end 25 | 26 | // `spliceHook` start 27 | $spliceHook : Hook<[ this, number, number, this[ 'Element' ][] ]> = undefined 28 | get spliceHook () : Hook<[ this, number, number, this[ 'Element' ][] ]> { 29 | if (this.$spliceHook !== undefined) return this.$spliceHook 30 | 31 | return this.$spliceHook = new Hook() 32 | } 33 | // `spliceHook` end 34 | 35 | push (...args : this[ 'Element' ][]) : number { 36 | this.spliceEvent.trigger(this, this.length, 0, args) 37 | this.spliceHook.trigger(this, this.length, 0, args) 38 | 39 | return super.push(...args) 40 | } 41 | 42 | 43 | pop () : this[ 'Element' ] { 44 | if (this.length > 0) { 45 | this.spliceEvent.trigger(this, this.length - 1, 1, []) 46 | this.spliceHook.trigger(this, this.length - 1, 1, []) 47 | } 48 | 49 | return super.pop() 50 | } 51 | } 52 | 53 | return ManagedArray 54 | }){} 55 | 56 | export interface ManagedArray { 57 | Element : Element 58 | } 59 | 60 | 61 | StartTest(t => { 62 | 63 | t.it('Listening to events should work', t => { 64 | const arr = new ManagedArray() 65 | 66 | let counter = 0 67 | 68 | const disposer = arr.spliceEvent.on((array, pos, howManyToRemove, newElements) => { 69 | counter++ 70 | 71 | t.isStrict(array, arr) 72 | t.isStrict(pos, 0) 73 | t.isStrict(howManyToRemove, 0) 74 | t.isDeeply(newElements, [ 11 ]) 75 | }) 76 | 77 | arr.push(11) 78 | 79 | t.is(counter, 1) 80 | 81 | disposer() 82 | 83 | arr.push(12) 84 | 85 | t.is(counter, 1) 86 | }) 87 | 88 | 89 | t.it('Should ignore duplicated listeners', t => { 90 | const arr = new ManagedArray() 91 | 92 | let counter = 0 93 | 94 | const listener = (array, pos, howManyToRemove, newElements) => { 95 | counter++ 96 | 97 | t.isStrict(array, arr) 98 | t.isStrict(pos, 0) 99 | t.isStrict(howManyToRemove, 0) 100 | t.isDeeply(newElements, [ 11 ]) 101 | } 102 | 103 | arr.spliceEvent.on(listener) 104 | arr.spliceEvent.on(listener) 105 | arr.spliceEvent.on(listener) 106 | 107 | arr.push(11) 108 | 109 | t.is(counter, 1) 110 | }) 111 | 112 | }) 113 | -------------------------------------------------------------------------------- /src/environment/Debug.ts: -------------------------------------------------------------------------------- 1 | import { Base } from "../class/Base.js" 2 | import { CI } from "../collection/Iterator.js" 3 | import { matchAll } from "../util/Helpers.js" 4 | 5 | //--------------------------------------------------------------------------------------------------------------------- 6 | export const DEBUG = false 7 | 8 | const emptyFn = (...args : any[]) : any => undefined 9 | 10 | export const DEBUG_ONLY = (func : T) : T => DEBUG ? func : emptyFn as any 11 | 12 | export const debug = DEBUG_ONLY((e : Error) => { 13 | debugger 14 | }) 15 | 16 | 17 | //--------------------------------------------------------------------------------------------------------------------- 18 | export const warn = DEBUG_ONLY((e : Error) => { 19 | if (typeof console !== 'undefined') console.warn(e) 20 | }) 21 | 22 | //--------------------------------------------------------------------------------------------------------------------- 23 | export class StackEntry extends Base { 24 | statement : string 25 | 26 | sourceFile : string 27 | sourceLine : number 28 | sourceCharPos : number 29 | } 30 | 31 | 32 | //--------------------------------------------------------------------------------------------------------------------- 33 | export class SourceLinePoint extends Base { 34 | exception : Error 35 | 36 | stackEntries : StackEntry[] = [] 37 | 38 | 39 | static fromError (e : Error) : SourceLinePoint { 40 | const res = SourceLinePoint.new({ 41 | exception : e, 42 | stackEntries : parseErrorStack(e.stack) 43 | }) 44 | 45 | return res 46 | } 47 | 48 | 49 | static fromThisCall () : SourceLinePoint { 50 | const sourceLinePoint = this.fromError(new Error()) 51 | 52 | sourceLinePoint.stackEntries.splice(0, 2) 53 | 54 | return sourceLinePoint 55 | } 56 | 57 | } 58 | 59 | //--------------------------------------------------------------------------------------------------------------------- 60 | // sample stack 61 | 62 | // Error 63 | // at exceptionCatcher (http://lh/bryntum-suite/SchedulingEngine/lib/ChronoGraph/environment/Debug.js:15:11) 64 | // at Function.fromCurrentCall (http://lh/bryntum-suite/SchedulingEngine/lib/ChronoGraph/environment/Debug.js:39:38) 65 | // at Object.get (http://lh/bryntum-suite/SchedulingEngine/lib/ChronoGraph/replica/Entity.js:31:73) 66 | // at MinimalGanttProject.set (http://lh/bryntum-suite/SchedulingEngine/lib/ChronoGraph/replica/Entity.js:222:23) 67 | // at MinimalGanttProject.set data [as data] (http://lh/bryntum-suite/SchedulingEngine/lib/Engine/chrono/ChronoModelMixin.js:48:31) 68 | // at MinimalGanttProject.construct (http://lh/bryntum-suite/SchedulingEngine/lib/Core/data/Model.js:290:17) 69 | // at MinimalGanttProject.construct (http://lh/bryntum-suite/SchedulingEngine/lib/Core/mixin/Events.js:236:15) 70 | // at MinimalGanttProject.construct (http://lh/bryntum-suite/SchedulingEngine/lib/Engine/chrono/ChronoModelMixin.js:21:19) 71 | // at MinimalGanttProject.construct (http://lh/bryntum-suite/SchedulingEngine/lib/Engine/quark/model/scheduler_basic/SchedulerBasicProjectMixin.js:53:19) 72 | // at new Base (http://lh/bryntum-suite/SchedulingEngine/lib/Core/Base.js:55:14)" 73 | 74 | 75 | 76 | 77 | const parseErrorStack = (stack : string) : StackEntry[] => { 78 | return CI(matchAll(/^ +at\s*(.*?)\s*\((https?:\/\/.*?):(\d+):(\d+)/gm, stack)) 79 | .map(match => StackEntry.new({ 80 | statement : match[ 1 ], 81 | sourceFile : match[ 2 ], 82 | sourceLine : Number(match[ 3 ]), 83 | sourceCharPos : Number(match[ 4 ]) 84 | })) 85 | .toArray() 86 | } 87 | -------------------------------------------------------------------------------- /src/collection/Collection.ts: -------------------------------------------------------------------------------- 1 | // import { AnyConstructor, Mixin } from "../class/Mixin.js" 2 | // 3 | // 4 | // export interface Collection { 5 | // [Symbol.iterator] () : IterableIterator 6 | // } 7 | // 8 | // 9 | // export interface OrderedForward extends Collection { 10 | // iterateForwardFrom () : IterableIterator 11 | // } 12 | // 13 | // 14 | // export interface OrderedBackward extends Collection { 15 | // iterateBackwardFrom () : IterableIterator 16 | // } 17 | // 18 | // 19 | // interface HKT { 20 | // readonly F : F 21 | // readonly A : A 22 | // } 23 | // 24 | // export interface Mappable { 25 | // fmap (func : (a : Element) => Result, collection : HKT) : HKT 26 | // } 27 | // 28 | // 29 | // 30 | // 31 | // //--------------------------------------------------------------------------------------------------------------------- 32 | // export const Indexed = >(base : T) => 33 | // 34 | // class Indexed extends base { 35 | // ElementT : any 36 | // 37 | // storage : this[ 'ElementT' ][] 38 | // 39 | // 40 | // get [Symbol.iterator] () : () => IterableIterator { 41 | // return this.storage[ Symbol.iterator ] 42 | // } 43 | // 44 | // 45 | // splice (start, ...args) : this[ 'ElementT' ][] { 46 | // return this.storage.splice(start, ...args) 47 | // } 48 | // 49 | // 50 | // * iterateAll () : IterableIterator { 51 | // return [ ...this ] 52 | // } 53 | // 54 | // 55 | // * iterateTo (index : number) : IterableIterator { 56 | // return [ ...this.storage.slice(0, index) ] 57 | // } 58 | // 59 | // 60 | // * iterateFrom (index : number) : IterableIterator { 61 | // return [ ...this.storage.slice(index) ] 62 | // } 63 | // 64 | // 65 | // * iterateTill (index : number) : IterableIterator { 66 | // 67 | // } 68 | // 69 | // 70 | // * iterateWhile (index : number) : IterableIterator { 71 | // 72 | // } 73 | // 74 | // 75 | // * iterateUntil (index : number) : IterableIterator { 76 | // 77 | // } 78 | // 79 | // 80 | // referenceToIndex () { 81 | // 82 | // } 83 | // 84 | // 85 | // referenceToBoxAtIndex () { 86 | // 87 | // } 88 | // 89 | // } 90 | // 91 | // export type Indexed = Mixin 92 | // 93 | // 94 | // 95 | // // //--------------------------------------------------------------------------------------------------------------------- 96 | // // export const TreeLeafNode = >(base : T) => 97 | // // 98 | // // class TreeLeafNode extends base { 99 | // // parent : TreeParentNode 100 | // // } 101 | // // 102 | // // export type TreeLeafNode = Mixin 103 | // // 104 | // // 105 | // // 106 | // // //--------------------------------------------------------------------------------------------------------------------- 107 | // // export const TreeParentNode = >(base : T) => 108 | // // 109 | // // class TreeParentNode extends base { 110 | // // ElementT : any 111 | // // 112 | // // children : TreeLeafNode[] 113 | // // } 114 | // // 115 | // // export type TreeParentNode = Mixin 116 | // // 117 | // // 118 | // // export type TreeNode = TreeLeafNode | TreeParentNode 119 | -------------------------------------------------------------------------------- /tests/index.ts: -------------------------------------------------------------------------------- 1 | declare let Siesta : any 2 | 3 | let project : any 4 | 5 | if (typeof process !== 'undefined' && typeof require !== 'undefined') { 6 | Siesta = require('siesta-lite') 7 | 8 | project = new Siesta.Project.NodeJS() 9 | } else { 10 | project = new Siesta.Project.Browser() 11 | } 12 | 13 | project.configure({ 14 | title : 'ChronoGraph Test Suite', 15 | isEcmaModule : true 16 | }) 17 | 18 | 19 | project.start( 20 | { 21 | group : 'Class', 22 | 23 | items : [ 24 | 'class/020_mixin.t.js', 25 | 'class/030_mixin_caching.t.js' 26 | ] 27 | }, 28 | { 29 | group : 'Iterator', 30 | 31 | items : [ 32 | 'collection/010_chained_iterator.t.js', 33 | ] 34 | }, 35 | { 36 | group : 'Graph', 37 | 38 | items : [ 39 | 'graph/010_walkable.t.js', 40 | 'graph/020_node.t.js', 41 | 'graph/030_cycle.t.js' 42 | ] 43 | }, 44 | { 45 | group : 'ChronoGraph', 46 | 47 | items : [ 48 | 'chrono/010_identifier_variable.t.js', 49 | 'chrono/011_lazy_identifier.t.js', 50 | 'chrono/012_impure_calculated_value.t.js', 51 | 'chrono/013_sync_calculation.t.js', 52 | 'chrono/013_async_calculation.t.js', 53 | 'chrono/015_listeners.t.js', 54 | 'chrono/016_recursion.t.js', 55 | 'chrono/017_identifier_listener.t.js', 56 | 'chrono/020_graph_branching.t.js', 57 | 'chrono/030_propagation.t.js', 58 | 'chrono/030_propagation_2.t.js', 59 | 'chrono/030_transaction_reject.t.js', 60 | 'chrono/030_iteration.t.js', 61 | 'chrono/031_garbage_collection.t.js', 62 | 'chrono/032_propagation_options.t.js', 63 | 'chrono/032_commit_async.t.js', 64 | 'chrono/033_cycle_info.t.js', 65 | 'chrono/040_add_remove.t.js', 66 | 'chrono/050_undo_redo.t.js', 67 | ] 68 | }, 69 | { 70 | group : 'Cycle resolver', 71 | 72 | items : [ 73 | 'cycle_resolver/010_memoizing.t.js', 74 | 'cycle_resolver/020_sed.t.js', 75 | 'cycle_resolver/030_sedwu_fixed_duration.t.js', 76 | 'cycle_resolver/040_sedwu_fixed_duration_effort_driven.t.js', 77 | 'cycle_resolver/050_sedwu_fixed_effort.t.js', 78 | 'cycle_resolver/060_sedwu_fixed_units.t.js', 79 | ] 80 | }, 81 | { 82 | group : 'chrono-userland', 83 | 84 | items : [ 85 | ] 86 | }, 87 | { 88 | group : 'Replica', 89 | 90 | items : [ 91 | 'replica/001_entity.t.js', 92 | 'replica/002_fields.t.js', 93 | 'replica/010_replica.t.js', 94 | 'replica/020_reference.t.js', 95 | 'replica/025_tree_node.t.js', 96 | 'replica/030_cycle_dispatcher_example.t.js', 97 | 'replica/033_cycle_info.t.js', 98 | 'replica/040_calculate_only.t.js' 99 | ] 100 | }, 101 | { 102 | group : 'Schema', 103 | 104 | items : [ 105 | 'schema/010_schema.t.js', 106 | ] 107 | }, 108 | { 109 | group : 'Visualization', 110 | 111 | items : [ 112 | // { 113 | // pageUrl : 'pages/cytoscape.html', 114 | // url : 'visualization/010_replica.t.js' 115 | // } 116 | ] 117 | }, 118 | { 119 | group : 'Util', 120 | 121 | items : [ 122 | 'util/uniqable.t.js' 123 | ] 124 | }, 125 | { 126 | group : 'Events', 127 | 128 | items : [ 129 | 'event/events.t.js' 130 | ] 131 | } 132 | ) 133 | -------------------------------------------------------------------------------- /tests/replica/001_entity.t.ts: -------------------------------------------------------------------------------- 1 | import { Base } from "../../src/class/Base.js" 2 | import { Entity } from "../../src/replica/Entity.js" 3 | import { entity } from "../../src/schema/Schema.js" 4 | 5 | declare const StartTest : any 6 | 7 | StartTest(t => { 8 | 9 | t.it('Entity, created lazily, subclass entry accessed first', async t => { 10 | class Author extends Entity.mix(Base) { 11 | } 12 | 13 | class SpecialAuthor extends Author { 14 | } 15 | 16 | t.notOk(Author.prototype.hasOwnProperty('$entity'), "Entity has not been created yet") 17 | t.notOk(SpecialAuthor.prototype.hasOwnProperty('$entity'), "Entity has not been created yet") 18 | 19 | // first access `$entity` of the subclass, then super 20 | t.ok(SpecialAuthor.$entity, "Entity has been created for the `SpecialAuthor` class") 21 | t.ok(Author.$entity, "Entity has been created for the `Author` class") 22 | 23 | t.is(SpecialAuthor.$entity.parentEntity, Author.$entity, "Correct entity inheritance") 24 | t.is(Author.$entity.parentEntity, null, "Correct entity inheritance") 25 | }) 26 | 27 | 28 | t.it('Entity, created lazily, superclass entry accessed first', async t => { 29 | class Author extends Entity.mix(Base) { 30 | } 31 | 32 | class SpecialAuthor extends Author { 33 | } 34 | 35 | // first access `$entity` of the super, then sub 36 | t.notOk(Author.prototype.hasOwnProperty('$entity'), "Entity has not been created yet") 37 | t.notOk(SpecialAuthor.prototype.hasOwnProperty('$entity'), "Entity has not been created yet") 38 | 39 | t.ok(Author.$entity, "Entity has been created for the `Author` class") 40 | t.ok(SpecialAuthor.$entity, "Entity has been created for the `SpecialAuthor` class") 41 | 42 | t.is(SpecialAuthor.$entity.parentEntity, Author.$entity, "Correct entity inheritance") 43 | t.is(Author.$entity.parentEntity, null, "Correct entity inheritance") 44 | }) 45 | 46 | 47 | t.it('Entity, created lazily, subclass entry accessed first, through instance', async t => { 48 | class Author extends Entity.mix(Base) { 49 | } 50 | 51 | class SpecialAuthor extends Author { 52 | } 53 | 54 | t.notOk(Author.prototype.hasOwnProperty('$entity'), "Entity has not been created yet") 55 | t.notOk(SpecialAuthor.prototype.hasOwnProperty('$entity'), "Entity has not been created yet") 56 | 57 | const specialAuthor = SpecialAuthor.new() 58 | const author = Author.new() 59 | 60 | // first access `$entity` of the subclass, then super 61 | t.ok(specialAuthor.$entity, "Entity has been created for the `SpecialAuthor` class") 62 | t.ok(author.$entity, "Entity has been created for the `Author` class") 63 | 64 | t.is(specialAuthor.$entity.parentEntity, Author.$entity, "Correct entity inheritance") 65 | t.is(author.$entity.parentEntity, null, "Correct entity inheritance") 66 | }) 67 | 68 | 69 | t.it('Entity, created lazily, subclass entry accessed first, through instance', async t => { 70 | @entity() 71 | class Author extends Entity.mix(Base) { 72 | } 73 | 74 | @entity() 75 | class SpecialAuthor extends Author { 76 | } 77 | 78 | // t.notOk(Author.prototype.hasOwnProperty('$entity'), "Entity has not been created yet") 79 | // t.notOk(SpecialAuthor.prototype.hasOwnProperty('$entity'), "Entity has not been created yet") 80 | 81 | const specialAuthor = SpecialAuthor.new() 82 | const author = Author.new() 83 | 84 | // first access `$entity` of the super, then sub 85 | t.ok(author.$entity, "Entity has been created for the `Author` class") 86 | t.ok(specialAuthor.$entity, "Entity has been created for the `SpecialAuthor` class") 87 | 88 | t.is(specialAuthor.$entity.parentEntity, Author.$entity, "Correct entity inheritance") 89 | t.is(author.$entity.parentEntity, null, "Correct entity inheritance") 90 | }) 91 | 92 | 93 | }) 94 | -------------------------------------------------------------------------------- /src/util/Helpers.ts: -------------------------------------------------------------------------------- 1 | //--------------------------------------------------------------------------------------------------------------------- 2 | // assume 32-bit platform (https://v8.dev/blog/react-cliff) 3 | import { CI } from "../collection/Iterator.js" 4 | 5 | export const MIN_SMI = -Math.pow(2, 30) 6 | export const MAX_SMI = Math.pow(2, 30) - 1 7 | 8 | //--------------------------------------------------------------------------------------------------------------------- 9 | export const uppercaseFirst = (str : string) : string => str.slice(0, 1).toUpperCase() + str.slice(1) 10 | 11 | 12 | //--------------------------------------------------------------------------------------------------------------------- 13 | export const isAtomicValue = (value : any) : boolean => Object(value) !== value 14 | 15 | 16 | //--------------------------------------------------------------------------------------------------------------------- 17 | export const typeOf = (value : any) : string => Object.prototype.toString.call(value).slice(8, -1) 18 | 19 | 20 | //--------------------------------------------------------------------------------------------------------------------- 21 | export const defineProperty = (target : T, property : S, value : T[ S ]) : T[ S ] => { 22 | Object.defineProperty(target, property, { value, enumerable : true, configurable : true }) 23 | 24 | return value 25 | } 26 | 27 | 28 | //--------------------------------------------------------------------------------------------------------------------- 29 | export const prototypeValue = (value : any) : PropertyDecorator => { 30 | 31 | return function (target : object, propertyKey : string | symbol) : void { 32 | target[ propertyKey ] = value 33 | } 34 | } 35 | 36 | 37 | //--------------------------------------------------------------------------------------------------------------------- 38 | export const copyMapInto = (sourceMap : Map, targetMap : Map) : Map => { 39 | for (const [ key, value ] of sourceMap) targetMap.set(key, value) 40 | 41 | return targetMap 42 | } 43 | 44 | 45 | //--------------------------------------------------------------------------------------------------------------------- 46 | export const copySetInto = (sourceSet : Set, targetSet : Set) : Set => { 47 | for (const value of sourceSet) targetSet.add(value) 48 | 49 | return targetSet 50 | } 51 | 52 | 53 | //--------------------------------------------------------------------------------------------------------------------- 54 | export const delay = (timeout : number) : Promise => new Promise(resolve => setTimeout(resolve, timeout)) 55 | 56 | 57 | //--------------------------------------------------------------------------------------------------------------------- 58 | export const matchAll = function* (regexp : RegExp, testStr : string) : Generator { 59 | let match : string[] 60 | 61 | while ((match = regexp.exec(testStr)) !== null) { 62 | yield match 63 | } 64 | } 65 | 66 | 67 | //--------------------------------------------------------------------------------------------------------------------- 68 | export const allMatches = function (regexp : RegExp, testStr : string) : string[] { 69 | return CI(matchAll(regexp, testStr)).map(match => CI(match).drop(1)).concat().toArray() 70 | } 71 | 72 | 73 | //--------------------------------------------------------------------------------------------------------------------- 74 | declare const regeneratorRuntime : any 75 | 76 | let isRegeneratorRuntime : boolean | null = null 77 | 78 | export const isGeneratorFunction = function (func : any) : boolean { 79 | if (isRegeneratorRuntime === null) isRegeneratorRuntime = typeof regeneratorRuntime !== 'undefined' 80 | 81 | if (isRegeneratorRuntime === true) { 82 | return regeneratorRuntime.isGeneratorFunction(func) 83 | } else { 84 | return func.constructor.name === 'GeneratorFunction' 85 | } 86 | } 87 | 88 | 89 | //--------------------------------------------------------------------------------------------------------------------- 90 | export const isPromise = function (obj : any) : obj is Promise { 91 | return obj && typeof obj.then === 'function' 92 | } 93 | -------------------------------------------------------------------------------- /tests/benchmark/shallow_changes.ts: -------------------------------------------------------------------------------- 1 | import { Benchmark } from "../../src/benchmark/Benchmark.js" 2 | import { deepGraphGen, deepGraphSync, GraphGenerationResult, mobxGraph, MobxGraphGenerationResult } from "./data.js" 3 | 4 | //--------------------------------------------------------------------------------------------------------------------- 5 | type PostBenchInfo = { 6 | totalCount : number 7 | result : number 8 | } 9 | 10 | //--------------------------------------------------------------------------------------------------------------------- 11 | class ShallowChangesChronoGraph extends Benchmark { 12 | 13 | gatherInfo (state : GraphGenerationResult) : PostBenchInfo { 14 | const { graph, boxes } = state 15 | 16 | return { 17 | totalCount : state.counter, 18 | result : graph.read(boxes[ boxes.length - 1 ]) 19 | } 20 | } 21 | 22 | 23 | stringifyInfo (info : PostBenchInfo) : string { 24 | return `Total calculation: ${info.totalCount}\nResult in last box: ${info.result}` 25 | } 26 | 27 | 28 | cycle (iteration : number, cycle : number, setup : GraphGenerationResult) { 29 | const { graph, boxes } = setup 30 | 31 | graph.write(boxes[ 0 ], iteration + cycle) 32 | graph.write(boxes[ 1 ], 15 - (iteration + cycle)) 33 | 34 | graph.commit() 35 | } 36 | } 37 | 38 | 39 | //--------------------------------------------------------------------------------------------------------------------- 40 | class ShallowChangesMobx extends Benchmark { 41 | 42 | gatherInfo (state : MobxGraphGenerationResult) : PostBenchInfo { 43 | const { boxes } = state 44 | 45 | return { 46 | totalCount : state.counter, 47 | result : boxes[ boxes.length - 1 ].get() 48 | } 49 | } 50 | 51 | 52 | stringifyInfo (info : PostBenchInfo) : string { 53 | return `Total calculation: ${info.totalCount}\nResult in last box: ${info.result}` 54 | } 55 | 56 | 57 | cycle (iteration : number, cycle : number, setup : MobxGraphGenerationResult) { 58 | const { boxes } = setup 59 | 60 | boxes[ 0 ].set(iteration + cycle) 61 | boxes[ 1 ].set(15 - (iteration + cycle)) 62 | 63 | // seems mobx does not have concept of eager computation, need to manually read all atoms 64 | for (let k = 0; k < boxes.length; k++) boxes[ k ].get() 65 | } 66 | } 67 | 68 | //--------------------------------------------------------------------------------------------------------------------- 69 | export const shallowChangesGen = ShallowChangesChronoGraph.new({ 70 | name : 'Shallow graph changes - generators', 71 | 72 | setup : async () => { 73 | return deepGraphGen(1300) 74 | } 75 | }) 76 | 77 | 78 | export const shallowChangesSync = ShallowChangesChronoGraph.new({ 79 | name : 'Shallow graph changes - synchronous', 80 | 81 | setup : async () => { 82 | return deepGraphSync(1300) 83 | } 84 | }) 85 | 86 | export const shallowChangesMobx = ShallowChangesMobx.new({ 87 | name : 'Shallow graph changes - Mobx', 88 | 89 | setup : async () => { 90 | return mobxGraph(1300) 91 | } 92 | }) 93 | 94 | 95 | //--------------------------------------------------------------------------------------------------------------------- 96 | export const shallowChangesGenBig = ShallowChangesChronoGraph.new({ 97 | name : 'Shallow graph changes - generators big', 98 | 99 | setup : async () => { 100 | return deepGraphGen(100000) 101 | } 102 | }) 103 | 104 | 105 | //--------------------------------------------------------------------------------------------------------------------- 106 | export const runAllShallowChanges = async () => { 107 | const runInfo = await shallowChangesGen.measureTillMaxTime() 108 | 109 | await shallowChangesSync.measureFixed(runInfo.cyclesCount, runInfo.samples.length) 110 | await shallowChangesMobx.measureFixed(runInfo.cyclesCount, runInfo.samples.length) 111 | 112 | await shallowChangesGenBig.measureTillMaxTime() 113 | } 114 | -------------------------------------------------------------------------------- /tests/chrono/014_parallel_propagation.t.ts: -------------------------------------------------------------------------------- 1 | import { ChronoGraph } from "../../src/chrono/Graph.js" 2 | import { Identifier } from "../../src/chrono/Identifier.js" 3 | 4 | declare const StartTest : any 5 | 6 | type GraphGenerationResult = { graph : ChronoGraph, boxes : Identifier[] } 7 | 8 | export const deepGraphGen = (atomNum : number = 1000) : GraphGenerationResult => { 9 | const graph : ChronoGraph = ChronoGraph.new() 10 | 11 | let boxes = [] 12 | 13 | for (let i = 0; i < atomNum; i++) { 14 | if (i <= 3) { 15 | boxes.push(graph.variableNamed(i, 1)) 16 | } 17 | else if (i <= 10) { 18 | boxes.push(graph.identifierNamed(i, function* (YIELD) { 19 | const input : number[] = [ 20 | yield boxes[0], 21 | yield boxes[1], 22 | yield boxes[2], 23 | yield boxes[3] 24 | ] 25 | 26 | return input.reduce((sum, op) => sum + op, 0) 27 | }, i)) 28 | } 29 | else if (i % 2 == 0) { 30 | boxes.push(graph.identifierNamed(i, function* (YIELD) { 31 | const input : number[] = [ 32 | yield boxes[this - 1], 33 | yield boxes[this - 2], 34 | yield boxes[this - 3], 35 | yield boxes[this - 4] 36 | ] 37 | 38 | return input.reduce((sum, op) => (sum + op) % 10000, 0) 39 | }, i)) 40 | } else { 41 | boxes.push(graph.identifierNamed(i, function* (YIELD) { 42 | const input : number[] = [ 43 | yield boxes[this - 1], 44 | yield boxes[this - 2], 45 | yield boxes[this - 3], 46 | yield boxes[this - 4] 47 | ] 48 | 49 | return input.reduce((sum, op) => (sum - op) % 10000, 0) 50 | }, i)) 51 | } 52 | } 53 | 54 | return { graph, boxes } 55 | } 56 | 57 | StartTest(t => { 58 | 59 | t.it('Should be possible to run several propagation in parallel', async t => { 60 | // const { graph, boxes } = deepGraphGen(100000) 61 | // 62 | // const observer1 = graph.observe(function * () { 63 | // yield boxes[ 5000 ] 64 | // yield boxes[ 5001 ] 65 | // yield boxes[ 5003 ] 66 | // }) 67 | 68 | // const observer2 = graph.observe(function * () { 69 | // yield boxes[ 5004 ] 70 | // yield boxes[ 5005 ] 71 | // yield boxes[ 5006 ] 72 | // }) 73 | // 74 | // const propagationCompletion1 = graph.propagateAsync({ observersFirst : true }) 75 | // 76 | // boxes[ 0 ].write(graph, 10) 77 | // 78 | // const propagationCompletion2 = graph.propagateAsync({ observersFirst : true }) 79 | 80 | // t.is(await propagationCompletion2.revision.previous === await propagationCompletion1.revision, '' 81 | 82 | // const iden1 = graph.addIdentifier(CalculatedValueSync.new({ 83 | // calculation (YIELD : SyncEffectHandler) : number { 84 | // count++ 85 | // 86 | // return YIELD(var1) + 1 87 | // } 88 | // })) 89 | // 90 | // const iden2 = graph.addIdentifier(CalculatedValueSync.new({ 91 | // calculation (YIELD : SyncEffectHandler) : number { 92 | // count++ 93 | // 94 | // return YIELD(iden1) + 1 95 | // } 96 | // })) 97 | // 98 | // const iden3 = graph.addIdentifier(CalculatedValueSync.new({ 99 | // calculation (YIELD : SyncEffectHandler) : number { 100 | // count++ 101 | // 102 | // return YIELD(iden2) + 1 103 | // } 104 | // })) 105 | // 106 | // graph.propagate() 107 | // 108 | // t.is(graph.read(iden1), 2, 'Correct value') 109 | // t.is(graph.read(iden2), 3, 'Correct value') 110 | // t.is(graph.read(iden3), 4, 'Correct value') 111 | // 112 | // t.is(count, 3, 'Calculated every identifier only once') 113 | }) 114 | }) 115 | -------------------------------------------------------------------------------- /benchmarks/object_creation.js: -------------------------------------------------------------------------------- 1 | const Mixin1 = (base) => 2 | class extends base { 3 | constructor () { 4 | super() 5 | 6 | this.a1 = [] 7 | 8 | this.b1 = 11 9 | 10 | this.c1 = new Set() 11 | } 12 | 13 | 14 | initialize () { 15 | super.initialize() 16 | 17 | this.a1 = [] 18 | 19 | this.b1 = 11 20 | 21 | this.c1 = new Set() 22 | } 23 | } 24 | 25 | 26 | const initialize1 = function () { 27 | this.a1 = [] 28 | 29 | this.b1 = 11 30 | 31 | this.c1 = new Set() 32 | } 33 | 34 | 35 | 36 | const Mixin2 = (base) => 37 | class extends base { 38 | constructor () { 39 | super() 40 | 41 | this.a2 = [] 42 | 43 | this.b2 = 11 44 | 45 | this.c2 = new Map() 46 | } 47 | 48 | 49 | initialize () { 50 | super.initialize() 51 | 52 | this.a2 = [] 53 | 54 | this.b2 = 11 55 | 56 | this.c2 = new Map() 57 | } 58 | } 59 | 60 | const initialize2 = function () { 61 | this.a2 = [] 62 | 63 | this.b2 = 11 64 | 65 | this.c2 = new Map() 66 | } 67 | 68 | 69 | 70 | const Mixin3 = (base) => 71 | class extends base { 72 | constructor () { 73 | super() 74 | 75 | this.a3 = {} 76 | 77 | this.b3 = 'asda' 78 | 79 | this.c3 = false 80 | } 81 | 82 | initialize () { 83 | super.initialize() 84 | 85 | this.a3 = {} 86 | 87 | this.b3 = 'asda' 88 | 89 | this.c3 = false 90 | } 91 | } 92 | 93 | const initialize3 = function () { 94 | this.a3 = {} 95 | 96 | this.b3 = 'asda' 97 | 98 | this.c3 = false 99 | } 100 | 101 | 102 | 103 | const Mixin4 = (base) => 104 | class extends base { 105 | constructor () { 106 | super() 107 | 108 | this.a4 = {} 109 | 110 | this.b4 = 'asda' 111 | 112 | this.c4 = false 113 | } 114 | 115 | initialize () { 116 | super.initialize() 117 | 118 | this.a4 = {} 119 | 120 | this.b4 = 'asda' 121 | 122 | this.c4 = false 123 | } 124 | } 125 | 126 | 127 | const initialize4 = function () { 128 | this.a4 = {} 129 | 130 | this.b4 = 'asda' 131 | 132 | this.c4 = false 133 | } 134 | 135 | 136 | 137 | const Mixin5 = (base) => 138 | class extends base { 139 | constructor () { 140 | super() 141 | 142 | this.a5 = {} 143 | 144 | this.b5 = 'asda' 145 | 146 | this.c5 = false 147 | } 148 | 149 | initialize () { 150 | super.initialize() 151 | 152 | this.a5 = {} 153 | 154 | this.b5 = 'asda' 155 | 156 | this.c5 = false 157 | } 158 | } 159 | 160 | const initialize5 = function () { 161 | this.a5 = {} 162 | 163 | this.b5 = 'asda' 164 | 165 | this.c5 = false 166 | } 167 | 168 | 169 | const Mixin6 = (base) => 170 | class extends base { 171 | constructor () { 172 | super() 173 | 174 | this.a6 = {} 175 | 176 | this.b6 = 'asda' 177 | 178 | this.c6 = false 179 | } 180 | 181 | initialize () { 182 | super.initialize() 183 | 184 | this.a6 = {} 185 | 186 | this.b6 = 'asda' 187 | 188 | this.c6 = false 189 | } 190 | } 191 | 192 | const initialize6 = function () { 193 | this.a6 = {} 194 | 195 | this.b6 = 'asda' 196 | 197 | this.c6 = false 198 | } 199 | 200 | 201 | 202 | class Base { 203 | initialize () { 204 | } 205 | } 206 | 207 | const cls = Mixin6(Mixin5(Mixin4(Mixin3(Mixin2(Mixin1(Base)))))) 208 | 209 | const count = 10000 210 | const instances = [] 211 | 212 | 213 | // ;[ 'a1', 'b1', 'c1', 'a2', 'b2', 'c2', 'a3', 'b3', 'c3' ].forEach(prop => cls.prototype[ prop ] = null) 214 | 215 | 216 | for (let i = 0; i < count; i++) instances.push(new cls()) 217 | 218 | 219 | for (let i = 0; i < count; i++) { 220 | const instance = Object.create(cls.prototype) 221 | 222 | instance.initialize() 223 | 224 | instances.push(instance) 225 | } 226 | 227 | 228 | for (let i = 0; i < count; i++) { 229 | const instance = {} 230 | 231 | initialize1.call(instance) 232 | initialize2.call(instance) 233 | initialize3.call(instance) 234 | initialize4.call(instance) 235 | initialize5.call(instance) 236 | initialize6.call(instance) 237 | 238 | instances.push(instance) 239 | } 240 | -------------------------------------------------------------------------------- /src/visualization/Cytoscape.ts: -------------------------------------------------------------------------------- 1 | import { ChronoGraph } from "../chrono/Graph.js" 2 | import { Identifier } from "../chrono/Identifier.js" 3 | import { Base } from "../class/Base.js" 4 | import { EntityIdentifier, FieldIdentifier } from "../replica/Identifier.js" 5 | 6 | declare const cytoscape, cytoscapeDagre, cytoscapeKlay, cytoscapeCoseBilkent : any 7 | 8 | export type Cytoscape = any 9 | export type CytoscapeId = string 10 | 11 | export class CytoscapeWrapper extends Base { 12 | graph : ChronoGraph = undefined 13 | container : any = undefined 14 | 15 | hideNodesWithoutOutgoingEdges : boolean = true 16 | 17 | ids : Map = new Map() 18 | 19 | $cytoscape : Cytoscape = undefined 20 | 21 | get cytoscape () : Cytoscape { 22 | if (this.$cytoscape !== undefined) return this.$cytoscape 23 | 24 | return this.$cytoscape = this.buildCytoScape() 25 | } 26 | 27 | 28 | renderTo (elem : any) { 29 | this.container = elem 30 | 31 | this.cytoscape 32 | } 33 | 34 | 35 | buildCytoScape () : Cytoscape { 36 | // cytoscape.use(cytoscapeDagre) 37 | cytoscape.use(cytoscapeKlay) 38 | // cytoscape.use(cytoscapeCoseBilkent) 39 | 40 | const cyto = cytoscape({ 41 | container: this.container, 42 | 43 | style: [ 44 | { 45 | selector: 'node', 46 | style: { 47 | 'label': 'data(name)', 48 | 'background-color': '#168BFF', 49 | } 50 | }, 51 | 52 | { 53 | selector: '.parent', 54 | style: { 55 | 'background-color': '#eee', 56 | } 57 | }, 58 | 59 | { 60 | selector: 'edge', 61 | style: { 62 | 'curve-style': 'bezier', 63 | 'width': 3, 64 | 'line-color': '#ccc', 65 | 'target-arrow-color': '#ccc', 66 | 'target-arrow-shape': 'triangle' 67 | } 68 | } 69 | ] 70 | }) 71 | 72 | const revision = this.graph.baseRevision 73 | 74 | let ID : number = 0 75 | 76 | const cytoIds = new Map() 77 | 78 | 79 | 80 | revision.scope.forEach((quark, identifier) => { 81 | // if (this.hideNodesWithoutOutgoingEdges && quark.getOutgoing().size === 0 && !quark.$outgoingPast) return 82 | 83 | // lazy nodes 84 | if (this.hideNodesWithoutOutgoingEdges && quark.value === undefined) return 85 | 86 | const cytoId = ID++ 87 | 88 | cytoIds.set(identifier, cytoId) 89 | 90 | const data : any = { group : 'nodes', data : { id : cytoId, name : identifier.name } } 91 | 92 | if (identifier instanceof FieldIdentifier) { 93 | const entityIden = identifier.self.$$ 94 | 95 | let entityId = cytoIds.get(entityIden) 96 | 97 | if (entityId === undefined) { 98 | entityId = ID++ 99 | 100 | cytoIds.set(entityIden, entityId) 101 | 102 | const cytoNode = cyto.add({ group: 'nodes', data : { id : entityId, name : entityIden.name }, classes : [ 'parent' ] }) 103 | } 104 | 105 | data.data.parent = entityId 106 | } 107 | 108 | if (identifier instanceof EntityIdentifier) { 109 | data.classes = [ 'parent' ] 110 | } 111 | 112 | const cytoNode = cyto.add(data) 113 | }) 114 | 115 | revision.scope.forEach((sourceQuark, identifier) => { 116 | sourceQuark.outgoingInTheFutureAndPastCb(revision, (targetQuark) => { 117 | cyto.add({ group: 'edges', data : { source: cytoIds.get(sourceQuark.identifier), target: cytoIds.get(targetQuark.identifier) } }) 118 | }) 119 | }) 120 | 121 | // cyto.layout({ name: 'dagre' }).run() 122 | cyto.layout({ name: 'klay' }).run() 123 | // cyto.layout({ name: 'cose-bilkent' }).run() 124 | 125 | return cyto 126 | } 127 | } 128 | 129 | 130 | -------------------------------------------------------------------------------- /src/lab/Meta.ts: -------------------------------------------------------------------------------- 1 | import { Base } from "../class/Base.js" 2 | import { AnyConstructor, Mixin } from "../class/Mixin.js" 3 | 4 | 5 | //--------------------------------------------------------------------------------------------------------------------- 6 | export interface Managed { 7 | readonly meta : MetaClass> 8 | } 9 | 10 | export type ManagedConstructor = AnyConstructor 11 | 12 | 13 | export class Property extends Base { 14 | name : string = undefined 15 | 16 | 17 | apply (target : Konstructor) { 18 | } 19 | 20 | 21 | getInitString (configName : string) : string { 22 | return `this.${this.name}=${configName}.${this.name};` 23 | } 24 | 25 | 26 | static decorator (this : T, props? : Partial>, cls? : T) : PropertyDecorator { 27 | 28 | return (target : InstanceType, propertyKey : string) : void => { 29 | const property = new (cls || this)() 30 | 31 | property.initialize(props) 32 | 33 | property.name = propertyKey 34 | 35 | target.meta.addProperty(property) 36 | } 37 | } 38 | } 39 | 40 | 41 | export class MetaClass extends Base { 42 | konstructor : Konstructor = undefined 43 | 44 | properties : Property[] = [] 45 | 46 | $initializer : (this : InstanceType, config? : Partial>) => any = undefined 47 | 48 | 49 | initialize> (props? : Partial) { 50 | super.initialize(props) 51 | 52 | if (!this.konstructor) { throw new Error('Required property `konstructor` missing during instantiation of ' + this) } 53 | 54 | // @ts-ignore 55 | this.instancePrototype.$meta = this 56 | } 57 | 58 | 59 | get initializer () : this[ '$initializer' ] { 60 | if (this.$initializer !== undefined) return this.$initializer 61 | 62 | let body : string = Array.from(new Set(this.properties)).map(property => property.getInitString('config')).join('') 63 | 64 | return this.$initializer = new Function('config', body) as this[ '$initializer' ] 65 | } 66 | 67 | 68 | get superclass () : AnyConstructor { 69 | return Object.getPrototypeOf(this.konstructor.prototype).constructor 70 | } 71 | 72 | 73 | get instancePrototype () : InstanceType { 74 | return this.konstructor.prototype 75 | } 76 | 77 | 78 | addProperty (property : Property) { 79 | this.properties.push(property) 80 | 81 | property.apply(this.konstructor) 82 | } 83 | } 84 | 85 | export const MetaClassC = (config : Partial>) : MetaClass => 86 | MetaClass.new(config) as MetaClass 87 | 88 | 89 | //--------------------------------------------------------------------------------------------------------------------- 90 | export class BaseManaged extends Mixin( 91 | [], 92 | (base : AnyConstructor) => { 93 | 94 | class BaseManaged extends base { 95 | $meta : any 96 | 97 | 98 | constructor (...args : any[]) { 99 | super(...args) 100 | 101 | this.meta.initializer.call(this) 102 | } 103 | 104 | 105 | get meta () : MetaClass { 106 | // @ts-ignore 107 | return this.constructor.meta 108 | } 109 | 110 | 111 | static get mixinsForMetaClass () : AnyConstructor[] { 112 | return [ MetaClass ] 113 | } 114 | 115 | 116 | static get meta () : MetaClass { 117 | const proto = this.prototype 118 | if (proto.hasOwnProperty('$meta')) return proto.$meta 119 | 120 | // @ts-ignore 121 | const metaClass = Mixin(this.mixinsForMetaClass, base => base) 122 | 123 | // @ts-ignore 124 | return proto.$meta = metaClass.new({ konstructor : this }) 125 | } 126 | } 127 | 128 | return BaseManaged 129 | }){} 130 | -------------------------------------------------------------------------------- /tests/chrono/015_listeners.t.ts: -------------------------------------------------------------------------------- 1 | import { ChronoGraph } from "../../src/chrono/Graph.js" 2 | 3 | declare const StartTest : any 4 | 5 | StartTest(t => { 6 | 7 | t.it('Basic', async t => { 8 | const graph : ChronoGraph = ChronoGraph.new() 9 | 10 | const var0 = graph.variable(0) 11 | const var1 = graph.variable(0) 12 | 13 | const iden1 = graph.identifier(function* () { 14 | return (yield var1) + 1 15 | }) 16 | 17 | const spyListener = t.createSpy() 18 | 19 | graph.addListener(iden1, spyListener) 20 | 21 | //------------------- 22 | graph.commit() 23 | 24 | t.expect(spyListener).toHaveBeenCalled(1) 25 | 26 | t.expect(spyListener.calls.argsFor(0)).toEqual([ 1 ]) 27 | 28 | //------------------- 29 | spyListener.calls.reset() 30 | 31 | graph.write(var1, 1) 32 | 33 | graph.commit() 34 | 35 | t.expect(spyListener).toHaveBeenCalled(1) 36 | 37 | t.expect(spyListener.calls.argsFor(0)).toEqual([ 2 ]) 38 | 39 | //------------------- 40 | spyListener.calls.reset() 41 | 42 | graph.write(var0, 1) 43 | 44 | graph.commit() 45 | 46 | t.expect(spyListener).toHaveBeenCalled(0) 47 | }) 48 | 49 | 50 | t.it('Should not trigger listener for "shadow" entries', async t => { 51 | const graph : ChronoGraph = ChronoGraph.new() 52 | 53 | const var0 = graph.variableNamed('var0', 0) 54 | const var1 = graph.variableNamed('var1', 10) 55 | 56 | const iden1 = graph.identifierNamed('iden1', function* () { 57 | return (yield var1) + 1 58 | }) 59 | 60 | const iden2 = graph.identifierNamed('iden2', function* () { 61 | return (yield iden1) + (yield var0) 62 | }) 63 | 64 | const spyListener = t.createSpy() 65 | 66 | graph.addListener(iden1, spyListener) 67 | 68 | //------------------- 69 | graph.commit() 70 | 71 | t.expect(spyListener).toHaveBeenCalled(1) 72 | 73 | //------------------- 74 | spyListener.calls.reset() 75 | 76 | graph.write(var0, 1) 77 | 78 | // this propagate will create a "shadowing" entry for the `iden1`, containing new edges and reference to previous quark 79 | graph.commit() 80 | 81 | t.expect(spyListener).toHaveBeenCalled(0) 82 | }) 83 | 84 | 85 | t.it('Should not trigger listener for the entries with the same value', async t => { 86 | const graph : ChronoGraph = ChronoGraph.new() 87 | 88 | const var0 = graph.variableNamed('var0', 0) 89 | const var1 = graph.variableNamed('var1', 10) 90 | 91 | const iden1 = graph.identifierNamed('iden1', function* () { 92 | return (yield var0) + (yield var1) 93 | }) 94 | 95 | const spyListener = t.createSpy() 96 | 97 | graph.addListener(iden1, spyListener) 98 | 99 | //------------------- 100 | graph.commit() 101 | 102 | t.expect(spyListener).toHaveBeenCalled(1) 103 | 104 | //------------------- 105 | spyListener.calls.reset() 106 | 107 | graph.write(var0, 5) 108 | graph.write(var1, 5) 109 | 110 | // this propagate will create a "shadowing" entry for the `iden1`, containing new edges and reference to previous quark 111 | graph.commit() 112 | 113 | t.expect(spyListener).toHaveBeenCalled(0) 114 | }) 115 | 116 | 117 | t.it('Should not trigger listener after the identifier removal', async t => { 118 | const graph : ChronoGraph = ChronoGraph.new() 119 | 120 | const var0 = graph.variableNamed('var0', 0) 121 | const var1 = graph.variableNamed('var1', 10) 122 | 123 | const iden1 = graph.identifierNamed('iden1', function* () { 124 | return (yield var0) + (yield var1) 125 | }) 126 | 127 | const spyListener = t.createSpy() 128 | 129 | graph.addListener(iden1, spyListener) 130 | 131 | //------------------- 132 | graph.commit() 133 | 134 | t.expect(spyListener).toHaveBeenCalled(1) 135 | 136 | //------------------- 137 | spyListener.calls.reset() 138 | 139 | graph.removeIdentifier(iden1) 140 | 141 | graph.write(var0, 5) 142 | graph.write(var1, 5) 143 | 144 | graph.commit() 145 | 146 | t.expect(spyListener).toHaveBeenCalled(0) 147 | }) 148 | 149 | }) 150 | -------------------------------------------------------------------------------- /tests/benchmark/deep_changes.ts: -------------------------------------------------------------------------------- 1 | import { Benchmark } from "../../src/benchmark/Benchmark.js" 2 | import { deepGraphGen, deepGraphGenShared, deepGraphSync, GraphGenerationResult, mobxGraph, MobxGraphGenerationResult } from "./data.js" 3 | 4 | //--------------------------------------------------------------------------------------------------------------------- 5 | type PostBenchInfo = { 6 | totalCount : number 7 | result : number 8 | } 9 | 10 | //--------------------------------------------------------------------------------------------------------------------- 11 | class DeepChangesChronoGraph extends Benchmark { 12 | 13 | gatherInfo (state : GraphGenerationResult) : PostBenchInfo { 14 | const { graph, boxes } = state 15 | 16 | return { 17 | totalCount : state.counter, 18 | result : graph.read(boxes[ boxes.length - 1 ]) 19 | } 20 | } 21 | 22 | 23 | stringifyInfo (info : PostBenchInfo) : string { 24 | return `Total calculation: ${info.totalCount}\nResult in last box: ${info.result}` 25 | } 26 | 27 | 28 | cycle (iteration : number, cycle : number, setup : GraphGenerationResult) { 29 | const { graph, boxes } = setup 30 | 31 | graph.write(boxes[ 0 ], iteration + cycle) 32 | 33 | graph.commit() 34 | // for (let k = 0; k < boxes.length; k++) graph.read(boxes[ k ]) 35 | } 36 | } 37 | 38 | 39 | //--------------------------------------------------------------------------------------------------------------------- 40 | class DeepChangesMobx extends Benchmark { 41 | 42 | gatherInfo (state : MobxGraphGenerationResult) : PostBenchInfo { 43 | const { boxes } = state 44 | 45 | return { 46 | totalCount : state.counter, 47 | result : boxes[ boxes.length - 1 ].get() 48 | } 49 | } 50 | 51 | 52 | stringifyInfo (info : PostBenchInfo) : string { 53 | return `Total calculation: ${info.totalCount}\nResult in last box: ${info.result}` 54 | } 55 | 56 | 57 | cycle (iteration : number, cycle : number, setup : MobxGraphGenerationResult) { 58 | const { boxes } = setup 59 | 60 | boxes[ 0 ].set(iteration + cycle) 61 | 62 | // seems mobx does not have concept of eager computation, need to manually read all atoms 63 | for (let k = 0; k < boxes.length; k++) boxes[ k ].get() 64 | } 65 | } 66 | 67 | 68 | //--------------------------------------------------------------------------------------------------------------------- 69 | export const deepChangesGenSmall = DeepChangesChronoGraph.new({ 70 | name : 'Deep graph changes - generators', 71 | 72 | setup : async () => { 73 | return deepGraphGen(1000) 74 | } 75 | }) 76 | 77 | 78 | export const deepChangesSyncSmall = DeepChangesChronoGraph.new({ 79 | name : 'Deep graph changes - synchronous', 80 | 81 | setup : async () => { 82 | return deepGraphSync(1000) 83 | } 84 | }) 85 | 86 | export const deepChangesMobxSmall = DeepChangesMobx.new({ 87 | name : 'Deep graph changes - Mobx', 88 | 89 | setup : async () => { 90 | return mobxGraph(1000) 91 | } 92 | }) 93 | 94 | //--------------------------------------------------------------------------------------------------------------------- 95 | export const deepChangesGenBig = DeepChangesChronoGraph.new({ 96 | name : 'Deep graph changes - generators big', 97 | 98 | // plannedMaxTime : 20000, 99 | // coolDownTimeout : 150, 100 | 101 | setup : async () => { 102 | return deepGraphGen(100000) 103 | } 104 | }) 105 | 106 | //--------------------------------------------------------------------------------------------------------------------- 107 | export const deepChangesGenBigShared = DeepChangesChronoGraph.new({ 108 | name : 'Deep graph changes - generators big, shared identifiers', 109 | 110 | // plannedMaxTime : 20000, 111 | // coolDownTimeout : 150, 112 | 113 | setup : async () => { 114 | return deepGraphGenShared(100000) 115 | } 116 | }) 117 | 118 | 119 | export const runAllDeepChanges = async () => { 120 | const runInfo = await deepChangesGenSmall.measureTillMaxTime() 121 | 122 | await deepChangesSyncSmall.measureFixed(runInfo.cyclesCount, runInfo.samples.length) 123 | await deepChangesMobxSmall.measureFixed(runInfo.cyclesCount, runInfo.samples.length) 124 | 125 | await deepChangesGenBig.measureTillMaxTime() 126 | await deepChangesGenBigShared.measureTillMaxTime() 127 | } 128 | -------------------------------------------------------------------------------- /src/lab/TreeNode.ts: -------------------------------------------------------------------------------- 1 | // import { Transaction } from "../chrono/Transaction.js" 2 | // import { AnyConstructor, Mixin } from "../class/BetterMixin.js" 3 | // import { Base } from "../class/Mixin.js" 4 | // import { calculate, Entity, field, write } from "./Entity.js" 5 | // import { reference } from "./Reference.js" 6 | // import { bucket } from "./ReferenceBucket.js" 7 | // import { bucket_tree_node, BucketMutation, BucketMutationPosition, TreeNodeBucketQuark } from "./TreeNodeBucket.js" 8 | // import { reference_tree_node, TreeNodeReferenceIdentifier } from "./TreeNodeReference.js" 9 | // 10 | // export class TreeNode extends Mixin( 11 | // [ Entity ], 12 | // (base : AnyConstructor) => { 13 | // 14 | // class TreeNode extends base { 15 | // @bucket_tree_node() 16 | // childrenOrdered : TreeNodeBucket 17 | // 18 | // @reference({ bucket : 'children'}) 19 | // parent : TreeNode 20 | // 21 | // @bucket() 22 | // children : Set 23 | // 24 | // @field({ lazy : true }) 25 | // parentIndex : number 26 | // 27 | // // @field({ lazy : true }) 28 | // // siblingPosition : { next : TreeNode, previous : TreeNode } 29 | // 30 | // // @field({ lazy : true }) 31 | // // globalPosition : { next : TreeNode, previous : TreeNode } 32 | // // 33 | // // @field({ lazy : true }) 34 | // // position : { next : TreeNode, previous : TreeNode } 35 | // 36 | // @reference_tree_node({ refType : 'next' }) 37 | // nextSibling : TreeNode 38 | // 39 | // @reference_tree_node({ refType : 'prev'}) 40 | // previousSibling : TreeNode 41 | // 42 | // 43 | // // @field() 44 | // // next : TreeNode 45 | // // 46 | // // @field() 47 | // // previous : TreeNode 48 | // 49 | // @calculate('parentIndex') 50 | // calculateParentIndex (Y) : number { 51 | // const previousSibling = Y(this.$.previousSibling) 52 | // 53 | // return previousSibling ? Y(previousSibling.$.parentIndex) + 1 : 0 54 | // } 55 | // 56 | // 57 | // @write('nextSibling') 58 | // write (transaction : Transaction, proposedValue : TreeNode) { 59 | // const previousValue = transaction.read(this.$.nextSibling) 60 | // 61 | // if (previousValue) { 62 | // 63 | // } 64 | // 65 | // if (proposedValue) { 66 | // const previousSibling = transaction.read(proposedValue.$.previousSibling) 67 | // 68 | // if (previousSibling) { 69 | // transaction.write(previousSibling.$.nextSibling, proposedValue) 70 | // } 71 | // 72 | // transaction.write(this.$.previousSibling, previousSibling) 73 | // 74 | // transaction.write(proposedValue.$.previousSibling, this) 75 | // } 76 | // 77 | // transaction.write(this.$.nextSibling, proposedValue) 78 | // 79 | // 80 | // // quark = quark || transaction.acquireQuarkIfExists(me) 81 | // // 82 | // // if (quark) { 83 | // // const proposedValue = quark.proposedValue 84 | // // 85 | // // if (proposedValue instanceof Entity) { 86 | // // me.getBucket(proposedValue).removeFromBucket(transaction, me.self) 87 | // // } 88 | // // } 89 | // // else if (transaction.baseRevision.hasIdentifier(me)) { 90 | // // const value = transaction.baseRevision.read(me, transaction.graph) as Entity 91 | // // 92 | // // if (value != null) { 93 | // // me.getBucket(value).removeFromBucket(transaction, me.self) 94 | // // } 95 | // // } 96 | // // 97 | // // super.write(me, transaction, quark, proposedValue) 98 | // } 99 | // 100 | // } 101 | // return TreeNode 102 | // }){} 103 | // 104 | // 105 | // export class TreeNodeBucket extends Base { 106 | // children : Set = new Set() 107 | // 108 | // childrenArray : TreeNode[] = [] 109 | // 110 | // 111 | // register (mutation : BucketMutation) { 112 | // const mutationNode = mutation.node 113 | // 114 | // if (this.children.has(mutationNode)) { 115 | // 116 | // } else { 117 | // 118 | // } 119 | // 120 | // 121 | // 122 | // if (!mutation.position.previous && !mutation.position.next) { 123 | // this.last = this.last 124 | // } 125 | // } 126 | // 127 | // } 128 | // 129 | -------------------------------------------------------------------------------- /src/lab/LinkedList.ts: -------------------------------------------------------------------------------- 1 | // import { AnyConstructor, Mixin } from "../class/BetterMixin.js" 2 | // import { DEBUG } from "../environment/Debug.js" 3 | // import { calculate, Entity, field } from "./Entity.js" 4 | // import { Replica } from "./Replica.js" 5 | // 6 | // export class LinkedListElement extends Mixin( 7 | // [ Entity ], 8 | // (base : AnyConstructor) => { 9 | // 10 | // class LinkedListElement extends base { 11 | // @field({ lazy : true }) 12 | // index : number 13 | // 14 | // @field() 15 | // list : LinkedList 16 | // 17 | // @field() 18 | // next : LinkedListElement 19 | // 20 | // @field() 21 | // previous : LinkedListElement 22 | // 23 | // @calculate('parentIndex') 24 | // calculateParentIndex (Y) : number { 25 | // const previousSibling : LinkedListElement = Y(this.$.previous) 26 | // 27 | // return previousSibling ? Y(previousSibling.$.index) + 1 : 0 28 | // } 29 | // 30 | // 31 | // // @write('next') 32 | // // write (Y, proposedValue : LinkedListElement) { 33 | // // 34 | // // } 35 | // 36 | // } 37 | // return LinkedListElement 38 | // }){} 39 | // 40 | // 41 | // export class LinkedList extends Mixin( 42 | // [ Entity ], 43 | // (base : AnyConstructor) => { 44 | // 45 | // class LinkedList extends base { 46 | // @field({ lazy : true }) 47 | // elements : Array 48 | // 49 | // // @field() 50 | // // cachedTillIndex : number = -1 51 | // 52 | // @field() 53 | // first : LinkedListElement = null 54 | // 55 | // 56 | // @calculate('elements') 57 | // calculateElements (Y) : Array { 58 | // let el = this.first 59 | // 60 | // const elements = [] 61 | // 62 | // while (el) { 63 | // if (DEBUG) if (el.list !== this) throw new Error("Invalid state") 64 | // 65 | // elements.push(el) 66 | // el = el.next 67 | // } 68 | // 69 | // return elements 70 | // } 71 | // 72 | // 73 | // @message_handler() 74 | // insertFirst (Y, elToInsert : LinkedListElement) { 75 | // elToInsert.previous = null 76 | // elToInsert.next = this.first 77 | // 78 | // if (this.first) { 79 | // this.first.previous = elToInsert 80 | // this.first = elToInsert 81 | // } 82 | // } 83 | // 84 | // 85 | // @message_handler() 86 | // insertFirstMany (Y, elsToInsert : LinkedListElement[]) { 87 | // elsToInsert.forEach((el, index) => { 88 | // Y.$(el).list = this 89 | // 90 | // if (index > 0) { 91 | // el.previous = elsToInsert[ index - 1 ] 92 | // } else 93 | // el.previous = null 94 | // 95 | // if (index < elsToInsert.length) { 96 | // el.next = elsToInsert[ index + 1 ] 97 | // } else 98 | // el.next = null 99 | // }) 100 | // 101 | // if (elsToInsert.length) { 102 | // if (this.first) this.first.previous = elsToInsert[ elsToInsert.length - 1 ] 103 | // 104 | // Y.$(elsToInsert[ elsToInsert.length - 1 ]).next = this.first 105 | // 106 | // this.first = elsToInsert[ 0 ] 107 | // } 108 | // } 109 | // 110 | // 111 | // // // insertAfter(null, ...) means insert into beginning 112 | // // insertAfter (elAfter : LinkedListElement, elsToInsert : LinkedListElement[]) : LinkedList[] { 113 | // // elToInsert.list = elAfter.list 114 | // // 115 | // // if (!elAfter) { 116 | // // this.insertFirst(el) 117 | // // } 118 | // // 119 | // // return [] 120 | // // } 121 | // // 122 | // // 123 | // // splice (index : number, howMany : number, elements : LinkedListElement[]) : LinkedListElement[] { 124 | // // 125 | // // 126 | // // return [] 127 | // // } 128 | // 129 | // } 130 | // return LinkedList 131 | // }){} 132 | // 133 | // 134 | // const list = new LinkedList() 135 | // 136 | // const el = new LinkedListElement() 137 | // 138 | // const replica = Replica.new() 139 | // 140 | // replica.addEntities([ list, el ]) 141 | // 142 | // list.insertFirst(replica.onYieldSync, el) 143 | // 144 | // list.insertFirstMany(replica.onYieldSync, [ el ]) 145 | -------------------------------------------------------------------------------- /tests/chrono/032_commit_async.t.ts: -------------------------------------------------------------------------------- 1 | import { ChronoGraph } from "../../src/chrono/Graph.js" 2 | import { CalculatedValueGen } from "../../src/chrono/Identifier.js" 3 | import { delay } from "../../src/util/Helpers.js" 4 | 5 | declare const StartTest : any 6 | 7 | const randomDelay = () => delay(Math.random() * 50) 8 | 9 | StartTest(t => { 10 | 11 | t.it('Should support the asynchronous calculations flow', async t => { 12 | const graph : ChronoGraph = ChronoGraph.new() 13 | 14 | const i1 = graph.variableNamed('i1', 0) 15 | const i2 = graph.variableNamed('i2', 10) 16 | const i3 = graph.variableNamed('i3', 0) 17 | 18 | const c1 = graph.addIdentifier(CalculatedValueGen.new({ 19 | sync : false, 20 | name : 'c1', 21 | *calculation () { 22 | yield randomDelay() 23 | 24 | return (yield i1) + (yield i2) 25 | } 26 | })) 27 | 28 | const c2 = graph.addIdentifier(CalculatedValueGen.new({ 29 | sync : false, 30 | name : 'c2', 31 | *calculation () { 32 | yield randomDelay() 33 | 34 | return (yield c1) + 1 35 | } 36 | })) 37 | 38 | const c3 = graph.addIdentifier(CalculatedValueGen.new({ 39 | name : 'c3', 40 | sync : false, 41 | *calculation () { 42 | yield randomDelay() 43 | 44 | return (yield c2) + (yield i3) 45 | } 46 | })) 47 | 48 | await graph.commitAsync() 49 | 50 | // ---------------- 51 | const nodes = [ i1, i2, i3, c1, c2, c3 ] 52 | 53 | t.isDeeply(nodes.map(node => graph.get(node)), [ 0, 10, 0, 10, 11, 11 ], "Correct result calculated #1") 54 | 55 | // ---------------- 56 | graph.write(i1, 5) 57 | graph.write(i2, 5) 58 | 59 | const c1v = graph.get(c1) 60 | 61 | t.isInstanceOf(c1v, Promise, "Returns promise for async calculation") 62 | 63 | t.is(await c1v, 10, 'Promise resolved correctly') 64 | 65 | // ---------------- 66 | t.isInstanceOf(graph.get(c2), Promise, "Currently c2 will always be marked as potentially dirty with promise on read") 67 | 68 | await graph.commitAsync() 69 | 70 | t.isDeeply(nodes.map(node => graph.get(node)), [ 5, 5, 0, 10, 11, 11 ], "Correct result calculated #1") 71 | 72 | graph.write(i3, 1) 73 | 74 | t.isDeeply([ i1, i2, i3, c1, c2 ].map(node => graph.get(node)), [ 5, 5, 1, 10, 11 ], "Correct result calculated #2") 75 | 76 | const c3v = graph.get(c3) 77 | 78 | t.isInstanceOf(c3v, Promise, "Returns promise for async calculation") 79 | 80 | t.is(await c3v, 12, 'Promise resolved correctly') 81 | }) 82 | 83 | 84 | t.it('Repeating calls to `commitAsync` should wait till previous one to complete', async t => { 85 | const graph : ChronoGraph = ChronoGraph.new() 86 | 87 | const i1 = graph.variableNamed('i1', 0) 88 | const i2 = graph.variableNamed('i2', 10) 89 | const i3 = graph.variableNamed('i3', 0) 90 | 91 | const c1 = graph.addIdentifier(CalculatedValueGen.new({ 92 | sync : false, 93 | name : 'c1', 94 | *calculation () { 95 | yield randomDelay() 96 | 97 | return (yield i1) + (yield i2) 98 | } 99 | })) 100 | 101 | const c2 = graph.addIdentifier(CalculatedValueGen.new({ 102 | sync : false, 103 | name : 'c2', 104 | *calculation () { 105 | yield randomDelay() 106 | 107 | return (yield c1) + 1 108 | } 109 | })) 110 | 111 | const c3 = graph.addIdentifier(CalculatedValueGen.new({ 112 | name : 'c3', 113 | sync : false, 114 | *calculation () { 115 | yield randomDelay() 116 | 117 | return (yield c2) + (yield i3) 118 | } 119 | })) 120 | 121 | await graph.commitAsync() 122 | 123 | // ---------------- 124 | const nodes = [ i1, i2, i3, c1, c2, c3 ] 125 | 126 | t.isDeeply(nodes.map(node => graph.get(node)), [ 0, 10, 0, 10, 11, 11 ], "Correct result calculated #1") 127 | 128 | // ---------------- 129 | graph.write(i1, 1) 130 | graph.write(i2, 2) 131 | 132 | graph.commitAsync() 133 | 134 | await graph.commitAsync() 135 | 136 | t.isDeeply(nodes.map(node => graph.get(node)), [ 1, 2, 0, 3, 4, 4 ], "Correct result calculated #2") 137 | }) 138 | 139 | }) 140 | -------------------------------------------------------------------------------- /src/guides/Benchmarks.md: -------------------------------------------------------------------------------- 1 | ChronoGraph benchmarks 2 | ====================== 3 | 4 | ChronoGraph aims to have excellent performance. To reason about it objectively, we wrote a benchmark suite. 5 | 6 | To run it, clone the repository, then run the following command in the package directory: 7 | 8 | ```plaintext 9 | > npm i 10 | > npx tsc 11 | > node -r esm ./tests/benchmark/suite.js --expose-gc 12 | ``` 13 | 14 | We currently benchmark the following: 15 | 16 | - `Deep graph changes - generators`
17 | A graph with 1000 nodes, every node except few initial ones depends on 4 preceding nodes. Nodes uses generators functions. 18 | A change is performed in one of the initial nodes, which affects the whole graph. 19 | - `Deep graph changes - synchronous`
20 | A graph with 1000 nodes, every node except few initial ones depends on 4 preceding nodes. Nodes uses synchronous functions. 21 | A change is performed in one of the initial nodes, which affects the whole graph. 22 | - `Deep graph changes - Mobx`
23 | A graph with 1000 nodes, every node except few initial ones depends on 4 preceding nodes. Nodes uses synchronous functions. 24 | A change is performed in one of the initial nodes, which affects the whole graph. We forcefully read from all nodes, 25 | because it seems the `keepAlive` option (which is an analog of strict identifier in ChronoGraph) in Mobx does not work. 26 | 27 | The numbers are not in ChronoGraph favor, yet. We'll be working on improving the results. 28 | Consider that Mobx is at version 5 and ChronoGraph at 1. Mobx also does not support the immutability (undo/redo, data branching), 29 | unlimited stack depth and asynchronous calculations. 30 | - `Deep graph changes - generators big`
31 | A graph with 100000 nodes, every node except few initial ones depends on 4 preceding nodes. Nodes uses generator functions. 32 | A change is performed in one of the initial nodes, which affects the whole graph. 33 | Mobx does not support the dependency chains of this length, so no comparable number. 34 | - `Deep graph changes - generators big, shared identifiers`
35 | A graph with 100000 nodes, every node except few initial ones depends on 4 preceding nodes. Nodes uses generator functions. 36 | A change is performed in one of the initial nodes, which affects the whole graph. 37 | Nodes with the same calculation functions uses shared "meta" instance. This optimization is already implemented in the [[Replica]] layer. 38 | - `Shallow graph changes - generators`
39 | A graph with 1000 nodes, every node except few initial ones depends on 4 preceding nodes. Nodes uses generator functions. 40 | A change is performed in one of the initial nodes, which affects only few initial nodes. 41 | - `Shallow graph changes - synchronous`
42 | A graph with 1000 nodes, every node except few initial ones depends on 4 preceding nodes. Nodes uses synchronous functions. 43 | A change is performed in one of the initial nodes, which affects only few initial nodes. 44 | - `Shallow graph changes - Mobx`
45 | A graph with 1000 nodes, every node except few initial ones depends on 4 preceding nodes. Nodes uses synchronous functions. 46 | A change is performed in one of the initial nodes, which affects only few initial nodes. 47 | - `Shallow graph changes - generators big`
48 | A graph with 100000 nodes, every node except few initial ones depends on 4 preceding nodes. Nodes uses synchronous functions. 49 | A change is performed in one of the initial nodes, which affects only few initial nodes. 50 | - `Graph population 100k - generators`
51 | Instantiation of graph with 100000 identifiers, using generator functions. 52 | - `Graph population 100k - generators`
53 | Instantiation of graph with 100000 identifiers, using synchronous functions. 54 | - `Graph population 100k - Mobx`
55 | Instantiation of graph with 100000 identifiers, using synchronous functions. 56 | - `Replica population 125k`
57 | Instantiation of replica with 5000 entities, each with 25 fields (125000) identifiers, using synchronous functions. 58 | 59 | Some reference numbers (results will be different on your machine): 60 | 61 | ```plaintext 62 | Deep graph changes - generators: 2.692ms ±0.009 63 | Deep graph changes - synchronous: 2.588ms ±0.068 64 | Deep graph changes - Mobx: 1.46ms ±0.034 65 | Deep graph changes - generators big: 455.75ms ±5.82 66 | Deep graph changes - generators big, shared identifiers: 343.545ms ±8.687 67 | Shallow graph changes - generators: 1.919ms ±0.009 68 | Shallow graph changes - synchronous: 2.086ms ±0.019 69 | Shallow graph changes - Mobx: 0.441ms ±0.021 70 | Shallow graph changes - generators big: 245.464ms ±6.417 71 | Graph population 100k - generators: 154.15ms ±4.198 72 | Graph population 100k - synchronous: 147.143ms ±4.751 73 | Graph population 100k - Mobx: 188.278ms ±7.598 74 | Replica population 125k: 229.583ms ±9.558 75 | ``` 76 | 77 | 78 | ## COPYRIGHT AND LICENSE 79 | 80 | MIT License 81 | 82 | Copyright (c) 2018-2020 Bryntum, Nickolay Platonov 83 | -------------------------------------------------------------------------------- /src/replica/Identifier.ts: -------------------------------------------------------------------------------- 1 | import { ChronoGraph } from "../chrono/Graph.js" 2 | import { CalculatedValueGen, CalculatedValueSync, Identifier, Variable } from "../chrono/Identifier.js" 3 | import { AnyConstructor, Mixin } from "../class/Mixin.js" 4 | import { EntityMeta } from "../schema/EntityMeta.js" 5 | import { Field } from "../schema/Field.js" 6 | import { Entity } from "./Entity.js" 7 | import { ReadMode, Replica } from "./Replica.js" 8 | 9 | 10 | export interface PartOfEntityIdentifier { 11 | self : Entity 12 | } 13 | 14 | 15 | //--------------------------------------------------------------------------------------------------------------------- 16 | /** 17 | * Mixin, for the identifier that represent a field of the entity. Requires the [[Identifier]] (or its subclass) 18 | * as a base class. See more about mixins: [[Mixin]] 19 | */ 20 | export class FieldIdentifier extends Mixin( 21 | [ Identifier ], 22 | (base : AnyConstructor) => 23 | 24 | class FieldIdentifier extends base implements PartOfEntityIdentifier { 25 | /** 26 | * Reference to the [[Field]] this identifier represents 27 | */ 28 | field : Field = undefined 29 | 30 | /** 31 | * Reference to the [[Entity]] this identifier represents 32 | */ 33 | self : Entity = undefined 34 | 35 | // temp storage for value for the phase, when identifier is created, but has not joined any graph 36 | // is cleared during the 1st join to the graph 37 | DATA : this[ 'ValueT' ] = undefined 38 | 39 | // standaloneQuark : InstanceType 40 | 41 | 42 | // readFromGraphDirtySync (graph : Checkout) { 43 | // if (graph) 44 | // return graph.readDirty(this) 45 | // else 46 | // return this.DATA 47 | // } 48 | 49 | 50 | // returns the value itself if there were no affecting writes for it 51 | // otherwise - promise 52 | getFromGraph (graph : Replica) : this[ 'ValueT' ] | Promise { 53 | if (graph) { 54 | if (graph.readMode === ReadMode.Current) return graph.get(this) 55 | if (graph.readMode === ReadMode.Previous) return graph.activeTransaction.readPrevious(this) 56 | if (graph.readMode === ReadMode.ProposedOrPrevious) graph.activeTransaction.readProposedOrPrevious(this) 57 | 58 | return graph.activeTransaction.readCurrentOrProposedOrPrevious(this) 59 | } else 60 | return this.DATA 61 | } 62 | 63 | 64 | readFromGraph (graph : Replica) : this[ 'ValueT' ] { 65 | if (graph) 66 | return graph.read(this) 67 | else 68 | return this.DATA 69 | } 70 | 71 | 72 | writeToGraph (graph : Replica, proposedValue : this[ 'ValueT' ], ...args : this[ 'ArgsT' ]) { 73 | if (graph) 74 | graph.write(this, proposedValue, ...args) 75 | else 76 | this.DATA = proposedValue 77 | } 78 | 79 | 80 | leaveGraph (graph : ChronoGraph) { 81 | const entry = graph.activeTransaction.getLatestStableEntryFor(this) 82 | 83 | if (entry) this.DATA = entry.getValue() 84 | 85 | super.leaveGraph(graph) 86 | } 87 | 88 | 89 | toString () : string { 90 | return this.name 91 | } 92 | }){} 93 | 94 | export type FieldIdentifierConstructor = typeof FieldIdentifier 95 | 96 | export class MinimalFieldIdentifierSync extends FieldIdentifier.mix(CalculatedValueSync) {} 97 | export class MinimalFieldIdentifierGen extends FieldIdentifier.mix(CalculatedValueGen) {} 98 | export class MinimalFieldVariable extends FieldIdentifier.mix(Variable) {} 99 | 100 | 101 | //--------------------------------------------------------------------------------------------------------------------- 102 | /** 103 | * Mixin, for the identifier that represent an entity as a whole. Requires the [[Identifier]] (or its subclass) 104 | * as a base class. See more about mixins: [[Mixin]] 105 | */ 106 | export class EntityIdentifier extends Mixin( 107 | [ Identifier ], 108 | (base : AnyConstructor) => 109 | 110 | class EntityIdentifier extends base implements PartOfEntityIdentifier { 111 | /** 112 | * [[EntityMeta]] instance of the entity this identifier represents 113 | */ 114 | entity : EntityMeta = undefined 115 | 116 | /** 117 | * Reference to the [[Entity]] this identifier represents 118 | */ 119 | self : Entity = undefined 120 | 121 | 122 | // entity atom is considered changed if any of its incoming atoms has changed 123 | // this just means if it's calculation method has been called, it should always 124 | // assign a new value 125 | equality () : boolean { 126 | return false 127 | } 128 | 129 | 130 | toString () : string { 131 | return `Entity identifier [${ this.self }]` 132 | } 133 | }){} 134 | 135 | export class MinimalEntityIdentifier extends EntityIdentifier.mix(CalculatedValueGen) {} 136 | -------------------------------------------------------------------------------- /tests/cycle_resolver/040_sedwu_fixed_duration_effort_driven.t.ts: -------------------------------------------------------------------------------- 1 | import { CalculateProposed, CycleResolution, CycleResolutionInput, Formula, CycleDescription } from "../../src/cycle_resolver/CycleResolver.js" 2 | 3 | declare const StartTest : any 4 | 5 | StartTest(t => { 6 | 7 | const StartDateVar = Symbol('StartDate') 8 | const EndDateVar = Symbol('EndDate') 9 | const DurationVar = Symbol('Duration') 10 | const EffortVar = Symbol('EffortVar') 11 | const UnitsVar = Symbol('UnitsVar') 12 | 13 | const startDateFormula = Formula.new({ 14 | output : StartDateVar, 15 | inputs : new Set([ DurationVar, EndDateVar ]) 16 | }) 17 | 18 | const endDateFormula = Formula.new({ 19 | output : EndDateVar, 20 | inputs : new Set([ DurationVar, StartDateVar ]) 21 | }) 22 | 23 | const durationFormula = Formula.new({ 24 | output : DurationVar, 25 | inputs : new Set([ StartDateVar, EndDateVar ]) 26 | }) 27 | 28 | const effortFormula = Formula.new({ 29 | output : EffortVar, 30 | inputs : new Set([ StartDateVar, EndDateVar, UnitsVar ]) 31 | }) 32 | 33 | const unitsFormula = Formula.new({ 34 | output : UnitsVar, 35 | inputs : new Set([ StartDateVar, EndDateVar, EffortVar ]) 36 | }) 37 | 38 | const fixedDurationEffortDrivenDescription = CycleDescription.new({ 39 | variables : new Set([ StartDateVar, EndDateVar, DurationVar, EffortVar, UnitsVar ]), 40 | formulas : new Set([ 41 | startDateFormula, 42 | endDateFormula, 43 | durationFormula, 44 | unitsFormula, 45 | effortFormula 46 | ]) 47 | }) 48 | 49 | const fixedDurationEffortDrivenResolutionContext = CycleResolution.new({ 50 | description : fixedDurationEffortDrivenDescription, 51 | // fixed duration, effort-driven 52 | defaultResolutionFormulas : new Set([ endDateFormula, unitsFormula ]) 53 | }) 54 | 55 | 56 | let input : CycleResolutionInput 57 | 58 | t.beforeEach(t => { 59 | input = CycleResolutionInput.new({ context : fixedDurationEffortDrivenResolutionContext }) 60 | }) 61 | 62 | 63 | t.it('Should apply keep flags - set start date, keep duration', t => { 64 | input.addPreviousValueFlag(StartDateVar) 65 | input.addPreviousValueFlag(EndDateVar) 66 | input.addPreviousValueFlag(DurationVar) 67 | input.addPreviousValueFlag(EffortVar) 68 | input.addPreviousValueFlag(UnitsVar) 69 | 70 | input.addProposedValueFlag(EffortVar) 71 | 72 | const resolution = input.resolution 73 | 74 | t.isDeeply( 75 | resolution, 76 | new Map([ 77 | [ StartDateVar, CalculateProposed ], 78 | [ EndDateVar, endDateFormula.formulaId ], 79 | [ DurationVar, CalculateProposed ], 80 | [ EffortVar, CalculateProposed ], 81 | [ UnitsVar, unitsFormula.formulaId ] 82 | ]) 83 | ) 84 | }) 85 | 86 | 87 | t.it('Should apply keep flags - set start date, keep duration', t => { 88 | input.addPreviousValueFlag(StartDateVar) 89 | input.addPreviousValueFlag(EndDateVar) 90 | input.addPreviousValueFlag(DurationVar) 91 | input.addPreviousValueFlag(EffortVar) 92 | input.addPreviousValueFlag(UnitsVar) 93 | 94 | input.addProposedValueFlag(EffortVar) 95 | 96 | const resolution = input.resolution 97 | 98 | t.isDeeply( 99 | resolution, 100 | new Map([ 101 | [ StartDateVar, CalculateProposed ], 102 | [ EndDateVar, endDateFormula.formulaId ], 103 | [ DurationVar, CalculateProposed ], 104 | [ EffortVar, CalculateProposed ], 105 | [ UnitsVar, unitsFormula.formulaId ] 106 | ]) 107 | ) 108 | }) 109 | 110 | 111 | t.it('Should resolve to calculate end date and units', t => { 112 | input.addPreviousValueFlag(StartDateVar) 113 | input.addPreviousValueFlag(DurationVar) 114 | input.addPreviousValueFlag(EffortVar) 115 | input.addPreviousValueFlag(UnitsVar) 116 | 117 | input.addProposedValueFlag(StartDateVar) 118 | input.addProposedValueFlag(DurationVar) 119 | input.addProposedValueFlag(EffortVar) 120 | input.addProposedValueFlag(UnitsVar) 121 | 122 | const resolution = input.resolution 123 | 124 | t.isDeeply( 125 | resolution, 126 | new Map([ 127 | [ StartDateVar, CalculateProposed ], 128 | [ EndDateVar, endDateFormula.formulaId ], 129 | [ DurationVar, CalculateProposed ], 130 | [ EffortVar, CalculateProposed ], 131 | [ UnitsVar, unitsFormula.formulaId ] 132 | ]) 133 | ) 134 | }) 135 | 136 | }) 137 | -------------------------------------------------------------------------------- /tests/graph/010_walkable.t.ts: -------------------------------------------------------------------------------- 1 | import { Base } from "../../src/class/Base.js" 2 | import { WalkableBackward, WalkableBackwardNode, WalkableForwardNode, WalkBackwardContext, WalkForwardContext } from "../../src/graph/Node.js" 3 | 4 | declare const StartTest : any 5 | 6 | class WalkerForwardNode extends WalkableForwardNode.mix(Base) { 7 | id : number 8 | NodeT : WalkableForwardNode 9 | outgoing : Map = new Map() 10 | } 11 | class WalkerBackwardNode extends WalkableBackwardNode.mix(Base) { 12 | id : number 13 | NodeT : WalkableBackwardNode 14 | incoming : Map = new Map() 15 | } 16 | 17 | 18 | // For optimization purposes, walker FIRST goes into the LAST "next" walkable node in the `forEachNext` 19 | // so we reverse to get the "expected" order 20 | const edges = (...nodes : T[]) => new Map(nodes.reverse().map(node => [ node, null ])) 21 | 22 | StartTest(t => { 23 | 24 | t.it('Minimal walk forward', t => { 25 | const node5 = WalkerForwardNode.new({ id : 5, outgoing : edges() }) 26 | 27 | const node3 = WalkerForwardNode.new({ id : 3, outgoing : edges(node5) }) 28 | const node4 = WalkerForwardNode.new({ id : 4, outgoing : edges(node3) }) 29 | const node2 = WalkerForwardNode.new({ id : 2, outgoing : edges(node3, node4) }) 30 | 31 | const node1 = WalkerForwardNode.new({ id : 1, outgoing : edges(node2) }) 32 | 33 | const walkPath = [] 34 | const topoPath = [] 35 | 36 | WalkForwardContext.new({ 37 | onNode : (node : WalkerForwardNode) => { 38 | walkPath.push(node.id) 39 | }, 40 | 41 | onTopologicalNode : (node : WalkerForwardNode) => { 42 | topoPath.push(node.id) 43 | } 44 | }).startFrom([ node1 ]) 45 | 46 | // For optimization purposes, walker FIRST goes into the LAST "next" walkable node in the `forEachNext` 47 | t.isDeeply(walkPath, [ 1, 2, 3, 5, 4 ], 'Correct walk path') 48 | t.isDeeply(topoPath, [ 5, 3, 4, 2, 1 ], 'Correct topo path') 49 | }) 50 | 51 | 52 | t.it('Walk with cycle forward', t => { 53 | const node1 = WalkerForwardNode.new({ id : 1 }) 54 | const node2 = WalkerForwardNode.new({ id : 2 }) 55 | const node3 = WalkerForwardNode.new({ id : 3 }) 56 | 57 | node1.addEdgeTo(node2) 58 | node2.addEdgeTo(node3) 59 | node3.addEdgeTo(node1) 60 | 61 | const walkPath = [] 62 | 63 | let cycleFound = false 64 | 65 | WalkForwardContext.new({ 66 | onNode : (node : WalkerForwardNode) => { 67 | walkPath.push(node.id) 68 | }, 69 | 70 | onCycle : (node : WalkerForwardNode) : any => { 71 | cycleFound = true 72 | 73 | t.isDeeply(walkPath, [ 1, 2, 3 ], 'Correct walk path') 74 | 75 | t.is(node, node1, 'Cycle points to node1') 76 | } 77 | }).startFrom([ node1 ]) 78 | 79 | t.ok(cycleFound, "Cycle found") 80 | }) 81 | 82 | 83 | t.it('Minimal walk backward', t => { 84 | const node1 = WalkerBackwardNode.new({ id : 1, incoming : edges() }) 85 | const node2 = WalkerBackwardNode.new({ id : 2, incoming : edges(node1) }) 86 | const node4 = WalkerBackwardNode.new({ id : 4, incoming : edges(node2) }) 87 | const node3 = WalkerBackwardNode.new({ id : 3, incoming : edges(node2, node4) }) 88 | const node5 = WalkerBackwardNode.new({ id : 5, incoming : edges(node3) }) 89 | 90 | const walkPath = [] 91 | const topoPath = [] 92 | 93 | WalkBackwardContext.new({ 94 | onNode : (node : WalkerBackwardNode) => { 95 | walkPath.push(node.id) 96 | }, 97 | 98 | onTopologicalNode : (node : WalkerBackwardNode) => { 99 | topoPath.push(node.id) 100 | } 101 | }).startFrom([ node5 ]) 102 | 103 | t.isDeeply(walkPath, [ 5, 3, 2, 1, 4 ], 'Correct walk path') 104 | t.isDeeply(topoPath, [ 1, 2, 4, 3, 5 ], 'Correct topo path') 105 | }) 106 | 107 | 108 | t.it('Walk with cycle backward', t => { 109 | const node1 = WalkerBackwardNode.new({ id : 1 }) 110 | const node2 = WalkerBackwardNode.new({ id : 2 }) 111 | const node3 = WalkerBackwardNode.new({ id : 3 }) 112 | 113 | node1.addEdgeFrom(node2) 114 | node2.addEdgeFrom(node3) 115 | node3.addEdgeFrom(node1) 116 | 117 | const walkPath = [] 118 | 119 | let cycleFound = false 120 | 121 | WalkBackwardContext.new({ 122 | onNode : (node : WalkerBackwardNode) => { 123 | walkPath.push(node.id) 124 | }, 125 | 126 | onCycle : (node : WalkerBackwardNode) : any => { 127 | cycleFound = true 128 | 129 | t.isDeeply(walkPath, [ 1, 2, 3 ], 'Correct walk path') 130 | 131 | t.is(node, node1, 'Cycle points to node1') 132 | } 133 | }).startFrom([ node1 ]) 134 | 135 | t.ok(cycleFound, "Cycle found") 136 | }) 137 | }) 138 | 139 | -------------------------------------------------------------------------------- /tests/chrono/030_propagation_2.t.ts: -------------------------------------------------------------------------------- 1 | import { HasProposedValue, ProposedOrPrevious } from "../../src/chrono/Effect.js" 2 | import { ChronoGraph } from "../../src/chrono/Graph.js" 3 | 4 | declare const StartTest : any 5 | 6 | StartTest(t => { 7 | 8 | t.it('Should be smart about counting incoming edges from different walk epoch', async t => { 9 | const graph : ChronoGraph = ChronoGraph.new() 10 | 11 | const i1 = graph.variableNamed('i1', 0) 12 | const i2 = graph.variableNamed('i2', 10) 13 | const i3 = graph.variableNamed('i3', 0) 14 | 15 | const c1 = graph.identifierNamed('c1', function* () { 16 | return (yield i1) + (yield i2) 17 | }) 18 | 19 | const c2 = graph.identifierNamed('c2', function* () { 20 | return (yield c1) + 1 21 | }) 22 | 23 | const c3 = graph.identifierNamed('c3', function* () { 24 | return (yield c2) + (yield i3) 25 | }) 26 | 27 | graph.commit() 28 | 29 | // ---------------- 30 | const nodes = [ i1, i2, i3, c1, c2, c3 ] 31 | 32 | t.isDeeply(nodes.map(node => graph.read(node)), [ 0, 10, 0, 10, 11, 11 ], "Correct result calculated #1") 33 | 34 | // ---------------- 35 | // these writes will give `c3` entry `edgesFlow` 1 36 | graph.write(i1, 5) 37 | graph.write(i2, 5) 38 | 39 | // this will bump the walk epoch 40 | t.is(graph.read(c1), 10, 'Correct value') 41 | 42 | // this write will give `c3` entry +1 to edge flow, but in another epoch, so if we clear the `edgesFlow` on new epoch 43 | // the total flow will be 1, and `c3` quark would be eliminated when `c2` did not change 44 | // we were clearing the edgeFlow on epoch change, however this is a counter-example for such clearing 45 | // TODO needs some proper solution for edgesFlow + walk epoch combination 46 | graph.write(i3, 1) 47 | 48 | t.isDeeply(nodes.map(node => graph.read(node)), [ 5, 5, 1, 10, 11, 12 ], "Correct result calculated #2") 49 | }) 50 | 51 | 52 | t.it('Should ignore eliminated quarks from previous calculations, which still remains in stack', async t => { 53 | const graph : ChronoGraph = ChronoGraph.new() 54 | 55 | const i1 = graph.variableNamed('i1', 0) 56 | const i2 = graph.variableNamed('i2', 10) 57 | const i3 = graph.variableNamed('i3', 0) 58 | 59 | const c1 = graph.identifierNamed('c1', function* () { 60 | return (yield i1) + (yield i2) 61 | }) 62 | 63 | const c2 = graph.identifierNamed('c2', function* () { 64 | return (yield c1) + 1 65 | }) 66 | 67 | const c3 = graph.identifierNamed('c3', function* () { 68 | return (yield c2) + 1 69 | }) 70 | 71 | graph.commit() 72 | 73 | // ---------------- 74 | const nodes = [ i1, i2, i3, c1, c2, c3 ] 75 | 76 | t.isDeeply(nodes.map(node => graph.read(node)), [ 0, 10, 0, 10, 11, 12 ], "Correct result calculated #1") 77 | 78 | // ---------------- 79 | // these writes will create an entry for `c3` 80 | graph.write(i1, 5) 81 | graph.write(i2, 5) 82 | graph.write(i3, 1) 83 | 84 | // this read will eliminate the entry for `c3` w/o computing it, since its only dependency `c2` didn't change 85 | // but, it will remain in the stack, with edgesFlow < 0 86 | // thus, at some point it will be processed by the transaction, possibly eliminating the "real" `c3` entry created 87 | // by the following write 88 | t.is(graph.read(c3), 12, 'Correct value') 89 | 90 | graph.write(i2, 4) 91 | 92 | graph.commit() 93 | 94 | t.isDeeply(nodes.map(node => graph.read(node)), [ 5, 4, 1, 9, 10, 11 ], "Correct result calculated #2") 95 | }) 96 | 97 | 98 | // TODO this should "just work" causes troubles in edge cases in engine 99 | t.xit('Should recalculate atoms, depending on the presence of the proposed value', async t => { 100 | const graph : ChronoGraph = ChronoGraph.new() 101 | 102 | const i1 = graph.variableNamed('i1', 0) 103 | const i2 = graph.variableNamed('i2', 10) 104 | 105 | const c1 = graph.identifierNamed('c1', function* () { 106 | return yield ProposedOrPrevious 107 | }) 108 | 109 | graph.write(c1, 11) 110 | 111 | let counter = 0 112 | 113 | const c2 = graph.identifierNamed('c2', function* () { 114 | counter++ 115 | 116 | const has = yield HasProposedValue(c1) 117 | 118 | return has ? 1 : yield i2 119 | }) 120 | 121 | graph.commit() 122 | 123 | // ---------------- 124 | const nodes = [ i1, i2, c1, c2 ] 125 | 126 | t.isDeeply(nodes.map(node => graph.read(node)), [ 0, 10, 11, 1 ], "Correct result calculated #1") 127 | 128 | t.is(counter, 1) 129 | 130 | // // ---------------- 131 | graph.write(i2, 20) 132 | counter = 0 133 | 134 | graph.commit() 135 | 136 | t.is(counter, 1) 137 | 138 | t.isDeeply(nodes.map(node => graph.read(node)), [ 0, 20, 11, 20 ], "Correct result calculated #1") 139 | }) 140 | }) 141 | -------------------------------------------------------------------------------- /src/lab/TreeNodeReference.ts: -------------------------------------------------------------------------------- 1 | // import { ChronoGraph } from "../chrono/Graph.js" 2 | // import { CalculatedValueSync, Levels } from "../chrono/Identifier.js" 3 | // import { Quark, QuarkConstructor } from "../chrono/Quark.js" 4 | // import { Transaction } from "../chrono/Transaction.js" 5 | // import { AnyConstructor, ClassUnion, identity, isInstanceOf, Mixin } from "../class/BetterMixin.js" 6 | // import { CalculationSync } from "../primitives/Calculation.js" 7 | // import { Field, Name } from "../schema/Field.js" 8 | // import { prototypeValue } from "../util/Helpers.js" 9 | // import { Entity, FieldDecorator, generic_field } from "./Entity.js" 10 | // import { FieldIdentifier, FieldIdentifierConstructor } from "./Identifier.js" 11 | // import { TreeNode } from "./TreeNode.js" 12 | // import { TreeNodeBucketIdentifier } from "./TreeNodeBucket.js" 13 | // 14 | // //--------------------------------------------------------------------------------------------------------------------- 15 | // export type ResolverFunc = (locator : any) => Entity 16 | // 17 | // 18 | // //--------------------------------------------------------------------------------------------------------------------- 19 | // export class TreeNodeReferenceField extends Mixin( 20 | // [ Field ], 21 | // (base : AnyConstructor) => 22 | // 23 | // class TreeNodeReferenceField extends base { 24 | // identifierCls : FieldIdentifierConstructor = MinimalTreeNodeReferenceIdentifier 25 | // 26 | // refType : 'next' | 'prev' 27 | // 28 | // resolver : ResolverFunc 29 | // }){} 30 | // 31 | // 32 | // //--------------------------------------------------------------------------------------------------------------------- 33 | // export const reference_tree_node : FieldDecorator = 34 | // (fieldConfig?, fieldCls = TreeNodeReferenceField) => generic_field(fieldConfig, fieldCls) 35 | // 36 | // 37 | // //--------------------------------------------------------------------------------------------------------------------- 38 | // export class TreeNodeReferenceIdentifier extends Mixin( 39 | // [ FieldIdentifier ], 40 | // (base : AnyConstructor) => { 41 | // 42 | // class TreeNodeReferenceIdentifier extends base { 43 | // @prototypeValue(Levels.DependsOnlyOnUserInput) 44 | // level : number 45 | // 46 | // field : TreeNodeReferenceField = undefined 47 | // 48 | // ValueT : TreeNode 49 | // 50 | // self : TreeNode 51 | // 52 | // proposedValueIsBuilt : boolean = true 53 | // 54 | // @prototypeValue(Mixin([ CalculationSync, Quark, Map ], identity)) 55 | // quarkClass : QuarkConstructor 56 | // 57 | // 58 | // getBucket (entity : TreeNode) : TreeNodeBucketIdentifier { 59 | // return entity.$.childrenOrdered as TreeNodeBucketIdentifier 60 | // } 61 | // 62 | // 63 | // buildProposedValue (me : this, quark : Quark, transaction : Transaction) : this[ 'ValueT' ] { 64 | // const proposedValue = quark.proposedValue 65 | // 66 | // if (proposedValue === null) return null 67 | // 68 | // const value : TreeNode = isInstanceOf(proposedValue, TreeNode) ? proposedValue : me.resolve(proposedValue) 69 | // 70 | // if (value) { 71 | // me.getBucket(value).register(me, transaction) 72 | // } 73 | // 74 | // return value 75 | // } 76 | // 77 | // 78 | // resolve (locator : any) : TreeNode | null { 79 | // const resolver = this.field.resolver 80 | // 81 | // return resolver ? resolver.call(this.self, locator) : null 82 | // } 83 | // 84 | // 85 | // leaveGraph (graph : ChronoGraph) { 86 | // // here we only need to remove from the "previous", "stable" bucket, because 87 | // // the calculation for the removed treeNodeReference won't be called - the possible `proposedValue` of treeNodeReference will be ignored 88 | // const value = graph.activeTransaction.readProposedOrPrevious(this) as TreeNode 89 | // 90 | // if (value != null) { 91 | // this.getBucket(value).unregister(this, graph.activeTransaction) 92 | // } 93 | // 94 | // super.leaveGraph(graph) 95 | // } 96 | // 97 | // 98 | // write (me : this, transaction : Transaction, quark : InstanceType, proposedValue : this[ 'ValueT' ]) { 99 | // quark = quark || transaction.acquireQuarkIfExists(me) 100 | // 101 | // if (quark) { 102 | // const proposedValue = quark.proposedValue 103 | // 104 | // if (proposedValue instanceof TreeNode) { 105 | // me.getBucket(proposedValue).unregister(me, transaction) 106 | // } 107 | // } 108 | // else if (transaction.baseRevision.hasIdentifier(me)) { 109 | // const value = transaction.baseRevision.read(me, transaction.graph) as TreeNode 110 | // 111 | // if (value != null) { 112 | // me.getBucket(value).unregister(me, transaction) 113 | // } 114 | // } 115 | // 116 | // super.write(me, transaction, quark, proposedValue) 117 | // } 118 | // } 119 | // 120 | // return TreeNodeReferenceIdentifier 121 | // }){} 122 | // 123 | // 124 | // export class MinimalTreeNodeReferenceIdentifier extends TreeNodeReferenceIdentifier.mix(FieldIdentifier.mix(CalculatedValueSync)) {} 125 | -------------------------------------------------------------------------------- /benchmarks/prototype-compactification.js: -------------------------------------------------------------------------------- 1 | class Base { 2 | method1 () { 3 | return 1 4 | } 5 | 6 | method2 () { 7 | return 1 8 | } 9 | 10 | method3 () { 11 | return 1 12 | } 13 | 14 | method4 () { 15 | return 1 16 | } 17 | } 18 | 19 | class Classic { 20 | method1 () { 21 | return 1 22 | } 23 | 24 | method2 () { 25 | return 1 26 | } 27 | 28 | method3 () { 29 | return 1 30 | } 31 | 32 | method4 () { 33 | return 1 34 | } 35 | } 36 | 37 | 38 | class Override1 extends Classic { 39 | method1 () { 40 | return super.method1() + 1 41 | } 42 | 43 | method2 () { 44 | return super.method2() + 1 45 | } 46 | 47 | method3 () { 48 | return super.method3() + 1 49 | } 50 | 51 | method4 () { 52 | return super.method4() + 1 53 | } 54 | } 55 | 56 | class Override2 extends Override1 { 57 | method1 () { 58 | return super.method1() + 1 59 | } 60 | 61 | method2 () { 62 | return super.method2() + 1 63 | } 64 | 65 | method3 () { 66 | return super.method3() + 1 67 | } 68 | 69 | method4 () { 70 | return super.method4() + 1 71 | } 72 | } 73 | 74 | class Override3 extends Override2 { 75 | method1 () { 76 | return super.method1() + 1 77 | } 78 | 79 | method2 () { 80 | return super.method2() + 1 81 | } 82 | 83 | method3 () { 84 | return super.method3() + 1 85 | } 86 | 87 | method4 () { 88 | return super.method4() + 1 89 | } 90 | } 91 | 92 | class Override4 extends Override3 { 93 | method1 () { 94 | return super.method1() + 1 95 | } 96 | 97 | method2 () { 98 | return super.method2() + 1 99 | } 100 | 101 | method3 () { 102 | return super.method3() + 1 103 | } 104 | 105 | method4 () { 106 | return super.method4() + 1 107 | } 108 | } 109 | 110 | class Override5 extends Override4 { 111 | method1 () { 112 | return super.method1() + 1 113 | } 114 | 115 | method2 () { 116 | return super.method2() + 1 117 | } 118 | 119 | method3 () { 120 | return super.method3() + 1 121 | } 122 | 123 | method4 () { 124 | return super.method4() + 1 125 | } 126 | } 127 | 128 | class Override6 extends Override5 { 129 | method1 () { 130 | return super.method1() + 1 131 | } 132 | 133 | method2 () { 134 | return super.method2() + 1 135 | } 136 | 137 | method3 () { 138 | return super.method3() + 1 139 | } 140 | 141 | method4 () { 142 | return super.method4() + 1 143 | } 144 | } 145 | 146 | class Override7 extends Override6 { 147 | method1 () { 148 | return super.method1() + 1 149 | } 150 | 151 | method2 () { 152 | return super.method2() + 1 153 | } 154 | 155 | method3 () { 156 | return super.method3() + 1 157 | } 158 | 159 | method4 () { 160 | return super.method4() + 1 161 | } 162 | } 163 | 164 | const Mixin1 = (base) => 165 | class Mixin1 extends base { 166 | method1 () { 167 | return super.method1() + 1 168 | } 169 | } 170 | 171 | const Mixin2 = (base) => 172 | class Mixin2 extends base { 173 | method2 () { 174 | return super.method2() + 1 175 | } 176 | } 177 | 178 | const Mixin3 = (base) => 179 | class Mixin3 extends base { 180 | method3 () { 181 | return super.method3() + 1 182 | } 183 | } 184 | 185 | const Mixin4 = (base) => 186 | class Mixin4 extends base { 187 | method4 () { 188 | return super.method4() + 1 189 | } 190 | } 191 | 192 | 193 | const MixinAll = (base) => 194 | class MixinAll extends base { 195 | method1 () { 196 | return super.method1() + 1 197 | } 198 | method2 () { 199 | return super.method2() + 1 200 | } 201 | method3 () { 202 | return super.method3() + 1 203 | } 204 | method4 () { 205 | return super.method4() + 1 206 | } 207 | } 208 | 209 | const Cls1Sparse = 210 | Mixin1( 211 | Mixin2( 212 | Mixin3( 213 | Mixin4( 214 | 215 | Mixin1( 216 | Mixin2( 217 | Mixin3( 218 | Mixin4( 219 | 220 | Mixin1( 221 | Mixin2( 222 | Mixin3( 223 | Mixin4( 224 | 225 | Mixin1( 226 | Mixin2( 227 | Mixin3( 228 | Mixin4( 229 | 230 | Mixin1( 231 | Mixin2( 232 | Mixin3( 233 | Mixin4( 234 | 235 | Mixin1( 236 | Mixin2( 237 | Mixin3( 238 | Mixin4( 239 | 240 | Mixin1( 241 | Mixin2( 242 | Mixin3( 243 | Mixin4( 244 | Base 245 | )))))))))))))))))))))))))))) 246 | 247 | const Cls2Compact = 248 | MixinAll( 249 | MixinAll( 250 | MixinAll( 251 | MixinAll( 252 | MixinAll( 253 | MixinAll( 254 | MixinAll( 255 | Base 256 | ))))))) 257 | 258 | 259 | const count = 10000 260 | const instancesSparse = [] 261 | const instancesCompact = [] 262 | const instancesClassic = [] 263 | 264 | for (let i = 0; i < count; i++) instancesSparse.push(new Cls1Sparse()) 265 | for (let i = 0; i < count; i++) instancesCompact.push(new Cls2Compact()) 266 | for (let i = 0; i < count; i++) instancesClassic.push(new Override7()) 267 | 268 | 269 | let res = 0 270 | 271 | for (let i = 0; i < count; i++) { 272 | const instance = instances[ i ] 273 | 274 | res += instance.method1() + instance.method2() + instance.method3() + instance.method4() 275 | } 276 | 277 | --------------------------------------------------------------------------------