Nucleoid has reached 1.0.0 version, but lots of milestones ahead.
Join open source at

What is Nucleoid?

Nucleoid is an open source (Apache 2.0), a runtime environment that allows declarative programming written in ES6 (JavaScript) syntax. Since statements are declarative, the runtime provides logical integrity, plasticity and persistency as hiding technical details.

The runtime cumulatively stores each statement and builds in the state so that doesn't require external data storage like RDBMS. This design eliminates complexity of the architecture and gains high performance since no network communication required, especially, in locking situations.

Declarative Runtime System

The declarative runtime system isolates a behavior definition of a program from its technical instructions and executes declarative statements, which represent logical intention without carrying any technical details. In this paradigm, there is no segregation regarding what data is or not, instead approaches how data is related with others so that any type of data including business rules can be added without requiring any additional actions such as compiling, configuring, restarting as a result of plasticity.

Syntax and Semantics

In declarative programming, as its name suggests, it runs based on definition of syntax, where a syntax is often followed by developers in order to achieve behaviors in specifications. For example:

> a = 1
> b = a * 10

= represents assignment of expression to a variable. In Nucleoid runtime, it follows formal logic as semantics in ES6 syntax. So, if the expression contains another variable, it is considered as dependency, and by the design, whenever the variable is changed, the dependent variable also has to change along with, otherwise it breaks its logical integrity.

However, in imperative programming, for the same example, when a is changed, it changes memory location where a refers, but the change doesn't alter variable b because in the context of IP, it is just a representation of memory location.

Control Flow

An important difference in declarative programming oppose to imperative is who manages the control flow. In IP, a programmer has full control of instructions that runs on CPU through programming runtime, but in declarative programming, it is based on semantics, and in Nucleoid, it follows formal logic as semantics.

Thanks to its plasticity, the runtime is able to adjust control flow as receives more statements, so that externalized configuration files are optional.

Persistency

Nucleoid runtime cumulatively stores statements in order as received so that the runtime doesn't require external database. This feature is enabled by declarative programming as a result of plasticity and lowers complexity of the system along with gaining better performance since there is no network communication required.

Variable

Coding in Nucleoid doesn't require file or compiling, the runtime dynamically accepts and runs statements.

At the end of transaction, the runtime stores on disk and makes available in memory so that it doesn't require external database or cache server.

Variable holds a value:

> var a = 1

or initiates an object:

> var student = new Student()

if a definition of variable contains another variable, it will be considered as dependency of the variable so the runtime will adjust the state accordingly.

var keyword is optional.

> x = 1
> y = x + 2
> x = 2
> y
4

However, if there is a circular dependency, it throws an error:

> num1 = 10
> num2 = num1 * 10
> num1 = num2 * 10
Circular Dependency

undefined variable

if undefined variable is part of another variable definition, it throws an error:

> d =  2 * r
r is not defined

However, for properties, as long as its object is initialized, it is sufficient to declare a statement:

> storage = new Storage()
> minimum = storage.capacity * 0.1
> minimum // minimum is undefined, but declared.
>
> storage.capacity = 100
> minimum
10

Object

Object is initialized with new keyword. If defined without variable name, the runtime automatically assigns unique one.

> account = new Account()
> account
{}
> new Account()
account1
> account1
{}

Object level declaration can be defined as:

> student = new Student()
> student.fullName = student.firstName + " " + student.lastName
> student.firstName = "F"
> student.lastName = "L"
> student.fullName
F L

Class

Class is defined as:

> class Order {}
> order = new Order()

if initiating object without defining class, throws an error:

> location = new location()
Location is not defined

Class level declaration can be defined as:

Using class name in declaration indicates this is class level declaration and applies to all instances even though initiated before.

> class Shape {}
> Shape.angle = (Shape.edge - 2) * 180
> shape1 = new Shape()
> shape1.edge = 4
> shape1.angle
360

The declaration can be defined after initiating object, still gives same result:

> class Shape {}
> shape1 = new Shape()
> shape1.edge = 4
> Shape.angle = (Shape.edge - 2) * 180 // Applies to all instances of Shape
> shape1.angle
360

If

If declaration can be defined as:

> m = false
> if(m == true) { n = m && true }
> n
undefined
> m = true
> n
true

Global variables can be defined in block, no need to declare before.

Same as previous, class level or object level declaration can be added in if statement:

> class Capacity {}
> if (Capacity.spare / Capacity.available > 0.5) {
    Capacity.total = Capacity.available +  Capacity.spare
  } else if (Capacity.spare / Capacity.available > 0.1) {
    Capacity.total = Capacity.available +  Capacity.spare * 2
  } else {
    Capacity.total = Capacity.available + Capacity.spare * 3
  }

Block

Block statements are defined as:

> {
    user1.username = '@username';
    user1.password = '1234567';
    user1.createdAt = Date.now();
  }

Important difference using a block is all dependencies resolve and adjust at the end of block instead of at the end of statement.

For example, if there is a predefined declaration as if(!/.{3,6}/.test(User.password)) { throw 'INVALID_PASSWORD' }", it will wait at the end of block.

However, in this example, it will throw at the end of statement:

> user1.username = '@username';
> user1.password = '1234567';
INVALID_PASSWORD

Let

let keyword creates local variable used during process of a block, and ends as exiting the block:

> {
    let area = Math.pow(radius, 2) * 3.14;
    volume = area * 5;
  }

Let variable cannot be referred outside of the block, but can be used in nested block statement like if statement:

> {
    let interest = annual / 12 ;

    if (interest > 5) {
      rate = 'HIGH'
    }
  }

Function

Functions can be used as parameter:

> Students.filter(s => s.location == 'CT')
> [{"location":"CT","name":"Student 1"}]

Functions shouldn't alter state of any variable, side effect may occur.

Or function call can be part of definition:

> count = list.filter( n => n % 2 ).length
> count
3
> list.push(3)
> count
4

delete

Variables can be deleted as:

> t = 1
> q = t + 1
> delete q
> q
q is not defined

throw

throw can be used as:

> item.description = 'abcdefghijklmnop'
> if(Item.description.length > 10) {
    throw 'LONG_DESCRIPTION'
  }

throw can be only used inside of a block.

If there is any uncaught exception, the runtime rollbacks the transaction.

Data Structure

Data structures are also defined in declarative syntax. Let's say name is a variable, and by program requirements, name must be:

  • less than 10 characters
  • first character is upper case
  • contains no underscore character

so, this can be 3 separate declarations:

if( name.length > 10 ) {
  throw "INVALID_SIZE"
}
if( ! /[A-Z]/.test( name.charAt(0) )) {
  throw "INVALID_FIRST_CHARACTER"
}
if( name.indexOf("_") > -1 ) {
  throw "INVALID_SPECIAL_CHARACTER"
}

Relationships

Relationships of objects are defined similar to database's relationships, but it requires to define in declarative syntax.

One-to-One

One-to-one's defined as referring object's property to another object instance.

> class Driver {}
> class Vehicle {}

> driver1 = new Driver();
> vehicle1 = new Vehicle();
> driver1.vehicle = vehicle1;

Bidirectional relationships requires additional declaration in order to keep both side synced, so not recommended unless absolutely required, associative entity may be used as alternative.

Still all the declarations are applicable to the property:

> Vehicle.type = "CAR"
> driver1.vehicle.type
"CAR"

One-to-Many

One-to-Many is defined in three ways:

List as in One's side

It is a list created as property:

> class Customer {}
> class Order {}

> Customer.orders = [];

> customer1 = new Customer();
> order1 = new Order();
> customer1.orders.push(order1);

Property as in Many's side

It is a property created, which refers to other instance:

> class Employee {}
> class Project {}

> employee1 = new Employee()
> project1 = new Project();
> project1.employee = employee1;

Both of first 2 options are not bidirectional.

Associative Entity

In this case, both objects will be registered in associative object:

> class User {}
> class Company {}
> class Registration {}

> if ( Registrations.filter( r => r.user == User ).length > 1 ) {
    throw "USER_ALREADY_REGISTERED"
  }

> user1 = new User();
> company1 = new Company();

> registration1 = new Registration();
> registration1.company = company1;
> registration1.user = user1;

Having a declaration of if ( Registrations.filter( r => r.user == User ).length > 1 ){ .. } adds One-to-Many constraint. In this case, registering user1 to another company throws "USER_ALREADY_REGISTERED":

> company2 = new Company();
> registration2 = new Registration();
> registration2.company = company2
> registration2.user = user1;
> "USER_ALREADY_REGISTERED"

Many-to-Many

Many-to-Many is relatively straightforward as only possible with associative entity without carrying any additional constraint.

> class Passenger {}
> class Flight {}
> class Ticket {}

> passenger1 = new Passenger();
> flight1 = new Flight();

> ticket1 = new Ticket();
> ticket1.passenger = passenger1
> ticket1.flight = flight1;

> flight2 = new Flight();

> ticket2 = new Ticket();
> ticket2.passenger = passenger1
> ticket2.flight = flight2;

Queries

Queries is done with functional programming.

The runtime stores each instance into its class list like driver1 = new Driver() will be part of Drivers.

One-to-One

> Drivers.filter( d=> d.state == "GA").filter( d => d.vehicle.year > 2010)
// Finds drivers in GA state with card younger than 2010

One-to-Many

> Orders.filter( o => o.price > 100 && o.customer.id == 192)
// Finds orders with bigger than $100 prize of customer with id 192

Other direction

> Customers.find( c=> c.id == 192).orders.filter( o=>o.price > 100)

Many-to-Many

Tickets.filter( t => t.passenger.id == 6912 && t.flight.destination == "LA")
// Finds ticket of passenger with id 6912 for destination to FL
_ □ 
Ctrl + Enter
 +Report