Overview of URSYS Architecture - dsriseah/ursys GitHub Wiki

Note

This is a work-in-progress draft

Architectural Goals

We want to separate UI code (e.g. React) from the ViewModel/Controller code to achieve GUI framework agnostic user interfaces in our application model.

The gist of this architecture is to consolidate APIs for data and UI transformations in smaller modules so they are easier to understand. There are some protections in place to force developers to not reuse property names and think-through their operational groups, actions, and shareable state.

A challenge for programmers is having a common set of concepts and principles with which to think systematically about application operations, user interface events, and how to separate them cleanly in modular code. Related to this are the multiple types of data, derived data, view models, and user interface handling are separate and distinct entities without creating undue duplication of effort.

skeleton code example link

Data Modules

Off the top of my head, there are three main kinds of modules in our apps.

DataCore Modules

These use the dc*-naming convention. These are written to be "pure" without any dependencies, managing an independent set of persisted core data as well as directed derived data. There can be multiple datacore modules, each coverring a different data domain.

The API for a datacore module is for performing CRUD operations on the pure data model. Typically, the datacore CRUD operations are only handled by application logic that coordinates the user interface with data operations.

For read-only use, a NOTIFICATIONS interface is provided. Data is retrieved as a set that is guaranteed to be the latest changes. There is an interface to also get changes since last call as well as complete dataset.

Datacore modules are also self-initializing and self-syncing with the persisted source, using a lifecycle interface that initializes independently before the front-end code even instantiates. In general, datacore modules use a "write then notify" loop. Using the CRUD API for a datacore results in two processes

  • (1) writes the data request to the persistent data store (2) receives the confirmation of successful write.
  • (3) Separately, the persistent data store sends a network notification that data has changed, and this is received by the datacore module which then (4) updates itself and its derived data structure before (5) broadcasting the change message locally so (6) interested subscribers can do their data fetch.

Examples of datacore architecture is implicit in all of our simulation-based realtime data as the piece manager classes (GEMSTEP), nodes and edges database modules for graphs (NETCREATE, MEME).

code examples link

AppCore Modules

These use the ac- module naming pattern, and similarly to datacore modules implement a very specific set of operations related to the application. Like with datacore, there can be multiple appcore modules, each importing one or more datacore modules with which to interact with.

AppCore modules are bridges between the user interface code (e.g. React) and data through DataCore modules. It is the only module that can talk to both Data and Front End, similar to the controller in an MVC architecture but encapsulating API-level ViewModel concepts.

In our current implementation, an AppCore module incorporates both a UI State Manager class which allows one to create groups of React-compatible state objects with a notification system. Each AppCore module initializes a State Manager to handle its own housekeeping. The AppCore is responsible for transforming data from the DataCore modules it accesses into renderable data for the Front End. It also handle Front End Operations that are sent to the AppCore, allowing the AppCore to coordinate multiple datasets to handle complicated operations.

Like DataCore modules, AppCore modules are scoped to a conceptually coherent application-level set of operations, supporting the data and event translations. They can be used to expose an API that is the model of how the application works as a set of named operations and unique properties.

The way that the user interface communicates its updates to the relevant appcore is by publishing state to the appcore, rather than updated it itself. Similar to the datacore loop, the cycle is two separate processes:

  1. the UI handles an event, converts it to a "state update" after doing any necessary tranformations
  2. The UI published the state change to an appcore module that understands it or calls an API method on the appcore that is written to take an event object and process it.

Note

The UI code does not update its state object itself, instead subscribing to the AppCore's state manager instance to receive renderable state change. The subscriber function is what sets the component state based on what is in the state change (e.g. this.setState(change) in React)

The AppCore receives the state, then applies the appropriate state changes to the UI State Manager object. The State Manager propagates back to the UI component(s) that have subscribed to this AppCore. The subscribed function receives the state object and does the UI state update there, which then drives the rerender.

The Appcore, since it's centralizing handling of state, can then perform additional operations by requesting followup changes. AppCores can communicate with other AppCores by using the URSYS messaging system if necessary, or by loading a read-only instance of UI State Manager instance that corresponds to other AppCores since they are all named and unique.

code examples link

Independent Application State Managers

There are multiple layers of state that are used to conditionally render and perform operations on data. The UI renders a representation of data, and operations transform data.

  • The data related to visuals is a derived data in that it transforms the pure data into what the UI framework needs to draw it to the screen, but they are not equivalent. A simple example is a UI text widget that displays a number, requiring a string. The underlying data representation, though, is stored as a number. There is a translation operation that has to handle this conversion appropriately.
  • The operations require their own data in the form of application state that determines things like "what page should I be displaying", "enabled/disabled buttons", "what operations are allowed, disallowed", "progression through a transactional operation", and "mode of operation". Having a collection of switches centralized and associated with each type of data, along with supporting code, can help clarify what happens where.

To support the AppCore modules, we have a **StateManager **class which implements a React-compatible flat state object of primitive values and arrays of values. In addition to implementing the property dictionary, the StateManager also has a notifications API so interested code modules can be informed of changes made through its change API. Each StateManager instance is initialized and owned by an AppCore module, but multiple modules can also access its internal state in a read-only way for rendering purposes. The StateManager also enforces uniqueness of state property names across all instances of StateManager to force programmers to not lazily reuse property names or use inconsistent letter casing standards. This helps with clarity and code refactoring.

code examples link

Module Types

To make this all work seamlessly, there's a particular way that each type of module is declared as a dependency.

  • DataCore modules must be independent with no other dependencies other than non-application libraries. This ensures that any non-DataCore module can import them without fear of circular dependencies. DataCore modules are the source of data truth and transformation.
  • AppCore modules may only import DataCore modules. They can be imported by any other module to access the AppCore API. In practice, that includes both front-end user interface elements (e.g. a React component) and non-DataCore operation modules (e.g. a manager of some kind that needs to read state of a particular application runtime property). AppCore are the source of operational truth and control that can be performed by a user interface, providing two way transformation utilities between UI-provided viewmodel data and DataCore-managed CRUD operations.
  • StateManager instances are managed by an AppCore, but can be independently fetched by name by non-DataCore modules or other AppCore modules other than the one that is initializing it. These instances expose the state change operations and notifications API as a managed group of properties; the AppCore is a wrapper that handles the logic of how to apply state changes through an Application-level operational API.
  • User Interface Components import one or more AppCore interfaces to gain access to the data and derived viewdata it needs to render. They also use AppCore modules to accept operational requests like changing data or application operation. The UI layer does not do any processing of its own; ideally it's a dumb front end that separates data manipulation parameters from visual state properties unique to the particular user interface framework being used for the front end.
  • Manager Modules are code that manage resources or operations that can be self-contained conceptually. They likely make use of DataCore modules to implement their own special stuff, except they don't need to worry about UI level stuff. If UI is needed, then an appropriate AppCore that imports the relevant DataCore modules can be used to provide that bridge, allowing the manager module to be written independent of UI needs.
  • Lifecycle Modules are code that control the overall run state of the application, coordinating low level operation like "initialize", "load assets", "load level", and so forth. This is a special topic unto itself.

Module Interconnection

The above section discusses direct dependencies through import and other means. However, URSYS provides the messaging system which allows programmers to decouple modules from each other through the same interface. DataCore and AppCore modules may make use of the messaging system to broadcast interesting state changes to subscribers anywhere in the system. The validity of this data is ensured through the LifeCycle system which strictly controls validity of data and the operations that produce data such that when one's code runs, it doesn't have to worry about annoying asynchronous programming structure.

code examples link

The same interface also works across the network, providing asynchronous transactions CALL (data sent, data received in return) as well as SIGNAL, SEND, and PING methods. More detail can be found in URSYS Network Concepts.

⚠️ **GitHub.com Fallback** ⚠️