├── 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\]\]!' "README.md"
17 | sed -i -e 's!\[\[AdvancedFeaturesGuide[|]Advanced features\]\]!' "README.md"
18 | sed -i -e 's!\[API docs\][(]\./globals.html[)]!' "README.md"
19 | sed -i -e 's!\[\[BenchmarksGuide[|]Benchmarks\]\]!' "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 |
--------------------------------------------------------------------------------