Viper internal¶
Introduction¶
Viper is a runtime designed with a strong usage of metadata to implement many features like Type, Value, Collection, Function, Serialization, Database, Remote Procedure Call, Network Service, Remote Database, Scripting in Python and Web interoperability with JSON and Bson.
Viper was designed to simplify the way scripting language and C++ work together through metadata. The dynamic aspect of a script interpreter uses the dynamic aspect of the metadata defined in Viper for Type, Value and Function to implement a strong typed data access and function call in a fast and lazy way.
P_Viper (the Python C/API binding of Viper) is like a safe strong-typed layer over Python required for C++ interoperability. P_Viper raises an exception immediately when you use the wrong type, but P_Viper is smart enough to use native Python value and collection as input since the metadata drives everything in Viper.
This document presents the various ideas found in the implementation of the Viper Runtime. Viper is straightforward to understand, and the code is almost self-explanatory (like a pseudocode). The style used is academic, so the runtime could be ported (line by line) to another imperative programming language like swift and C#.
Coding Style¶
Viper doesn't use complicated C++ features. Most of the classes are immutable and the memory
management is handled by std::shared_ptr<T>.
This approach allows a shared ownership policy with the Python's objects. It's the foundation of the Python interop used by the seamless wrapper.
File Naming Convention¶
The name of the file mimics the declaration of C++ namespace.
namespace Viper { class Type { ... }}--> Viper_Type.hppnamespace Viper::DSMP { class Checker {...}}--> Viper_DSMP_Checker.hpp
File Content¶
Only one C++ definition per header file.
Object Instantiation¶
To impose the use of std::shared_ptr<T> required by the C++/Python interop, a
constructor is always private, and a static method make(...) is used to instantiate an
object. This function ensures that parameters are valid or raise an exception, constructs
intermediate values, then calls the private constructor which should only initialize
instance members (no logic / no exception).
Immutability¶
Since almost class are immutable and accessible by a shared pointer, we can use a public
const member. const is used in the east coast revolution approach.
- we used
std::shared<T const> const & v(logical/east coast) - we don't use
const std::shared<const T> & v(english dialect/west coast).
Explicit T const an Immutability¶
Since almost all classes are immutable, we use std::shared<T> const & v
(implicit immutability) and reserve std::shared<T const> const & for explicit
immutability.
Exceptions vs Error¶
Viper defines a single std::exception Viper::Error, and introduces the notion of
host, process, component, domain and message. When an error is raised,
host, process, component, domain and message are filled by the offended method and a detailed
description is constructed to describe the context.
In this example:
[GraphEditor@bma.home]:P_Viper:DecoderErrors:0:expected type 'long', got 'str' while decoding 'checkValue.Vector.at(0).Vec.at(2).UInt32'.
- Process =
GraphEditor@bma.home(process@host) - Component =
P_Viper - Domain =
DecoderErrors - Error Code =
0 - Message =
expected type 'long', got 'str' while decoding 'checkValue.Vector.at(0).Vec.at(2).UInt32
Error are transported by the RPC infrastructure. An error raised from a remote function is captured on the server and throws on the client.
Error declinations for a class or a feature are defined in the file
<something>Errors.hpp
Examples of Errors files.
- Viper_Key.h --> Viper_KeyErrors.hpp
- Viper_Path.h --> Viper_PathErrors.hpp
- Viper_Function.h --> Viper_FunctionErrors.hpp
- Viper_Stream.h --> Viper_StreamErrors.hpp
- ...
Minimal Exposition and Anonymous Namespace¶
To reduce the exposition of the implementation, we use anonymous namespace to define inner
class, and we try to expose a feature as a function, since functions are composable.
// Viper_DefinitionsEncoder.hpp
namespace Viper {
class Definitions;
class StreamCodecInstancing;
namespace DefinitionsEncoder {
Blob encode(std::shared_ptr<Definitions const> const & definitions,
std::shared_ptr<StreamCodecInstancing> const & codec);
}
} // ns
Only the forward declarations required by the function feature are exposed in the header
file, and in the implementation file, we introduce an anonymous namespace to define the
class that really implements the feature. The exposed function use the inner class by
calling the principal method.
// Viper_DefinitionsEncoder.cpp
namespace Viper::DefinitionsEncoder {
namespace { // ans
class Encoder final {
...
};
} // ans
Blob encode(std::shared_ptr<Definitions const> const & definitions,
std::shared_ptr<StreamCodecInstancing> const & codec) {
Encoder encoder {definitions, codec};
encoder.writeDefinitions();
return encoder.endEncoding();
}
} // ans
The Type System¶
The type system is explained in Digital_Substrate_Model.
The type system is a dynamic implementation of various types available in the STL. The
type system supports bool, integers, reals, tuple<T0, ...>,Optional<T>,vector<T>,
set<T>, map<K, V>, variant<T0, ...> and other types introduced by the DSM.
Since the runtime is dynamic, the runtime needs to check the type very often and
in a rapid way. We replace the use of std::dynamic_cast<T> by a simple
TypeCode to check the type and then we use std::static_cast<T>.
Type codes are generally used in a big switch case, so the compiler can generate a jump table.
All types derive from Viper::Type and implement the pure virtual methods.
namespace Viper {
class Type {
public:
TypeCode const typeCode;
// MARK: - Life Cycle
Type(TypeCode code): typeCode {code} {};
virtual ~Type() = default;
// MARK: - Representation
virtual std::string representation() const = 0;
virtual std::string representationIn(NameSpace const & nameSpace) const = 0;
// MARK: - Description
virtual std::string description() const = 0;
virtual std::string descriptionIn(NameSpace const & nameSpace) const = 0;
// MARK: - Equality
virtual bool isEqual(std::shared_ptr<Type> const & other) const = 0;
// MARK: - Compare
virtual Ordered compare(std::shared_ptr<Type> const & other) const = 0;
};
} // ns
Types are totally ordered by the TypeCode.
Specific types define the method cast for convenience.
static std::shared_ptr<TypeSet> cast(std::shared_ptr<Type> const & type);
Generic Types¶
All generic container declare at least a member elementType for their generic
parameters.
ex: TypeVector<T> <--> std::vector<T>)
class TypeVector final : public Type {
public:
std::shared_ptr<Type> const elementType;
...
static std::shared_ptr<TypeVector> make(std::shared_ptr<Type> const & elementType);
static std::shared_ptr<TypeVector> cast(std::shared_ptr<Type> const & type);
}
ex: TypeSet<T> <--> std::set<T>
class TypeSet final : public Type {
public:
std::shared_ptr<Type> const elementType;
...
}
ex: Map<K, V> <--> std::map<K, V>
class TypeMap final : public Type {
public:
std::shared_ptr<Type> const keyType;
std::shared_ptr<Type> const elementType;
...
}
Data Model, Definitions and RuntimeID.¶
Data modeling is carried out using the DSM (Digital Substrate Model) language. This DSL (Domain-Specific Language) allows you to define the different elements involved in the design of a data model. This DSM makes it possible to define aggregates of information which are linked by unique key to construct complex structured data.
To define a structured, flexible data model, the DSM introduces basically five fundamental notions.
- Namespace: a space where types are defined.
- Concept: an abstract thing (like an abstract Type)
- Key: a way to identify the instantiation of a Concept (like a UUID<Concept>)
- Document: a piece of information expressible in the type system (ex: struct Document { ... })
- Attachment: a way to associate a Document with a Key (like a map
)
Identifier terminology:
- Namespace ID: the uuid of the namespace used as the seed to generate other Runtime ID.
- Runtime ID: the uuid generated by the runtime for a concept, a club, a struct, an enum or an attachment computed from its definition.
A Viper::Definitions is a catalog of Viper::TypeConcept, Viper::TypeClub,
Viper::TypeStructure , Viper::TypeEnumeration, or Viper::TypeAttachment.
Viper::Definitionscan be merged and extracted. This capability is the foundation of the service system.
A Runtime ID is required to register those types and only Viper::Definitions
can make those types. During the registration of a type, various checks are done to
detect possible corruption of the type system and the Runtime ID is generated.
class TypeStructure {
...
private:
friend Definitions;
static std::shared_ptr<TypeStructure> make(NameSpace const & nameSpace,
std::shared_ptr<TypeStructureDescriptor> const & descriptor);
...
}
The Value System¶
The value system is a dynamic version of various classes found in the C++ STL. However, the C++ STL use a value semantic and Viper uses a reference semantic.
Viper is like a strong type version of a subset of the Python runtime implemented with the C++ STL. Values must be explicitly copied with
Value::copy, like in Python withcopy.deepcopy.
Value¶
Since the runtime is dynamic, the runtime needs to check the type of value. This is done by
the member type(). All value inherit from Viper::Value
and implement the pure virtual methods.
namespace Viper {
class Type;
class Value {
public:
virtual ~Value() = default;
// MARK: - Type
std::shared_ptr<Type> const type() const = 0;
// MARK: - Representation
virtual std::string representation() const = 0;
// MARK: - Description
virtual std::string description() const = 0;
virtual std::string descriptionIn(NameSpace const & nameSpace) const = 0;
// MARK: - Hash & Equality
virtual std::size_t hash() const = 0;
virtual bool isEqual(std::shared_ptr<Value const> const & other) const = 0;
// MARK: - Compare
virtual Ordered compare(std::shared_ptr<Value const> const & other) const = 0;
// MARK: - Helper
static std::shared_ptr<Value> make(std::shared_ptr<Type> const & type);
static std::shared_ptr<Value> copy(std::shared_ptr<Value const> const & value);
};
} // ns
A value can be constructed from a type with the universal constructor
Viper::Value::create(...).
For each type there is a corresponding value.
Viper::TypeUInt8-->Viper::ValueInt8Viper::TypeFloat-->Viper::ValueFloatViper::TypeVector-->Viper::ValueVectorViper::TypeMap-->Viper::ValueMap
Since primitive values (ints, reals, uuid, blob) are immutable the
Viper::Value::copy(...) do nothing. The reference counter is just incremented by the
semantic of the std::shared_ptr<T>.
Each generic container uses the corresponding C++ STL containers.
Performance are drastically reduced, since the instance is accessible by a std::shared_ptr<T>
(allocated in the heap with an atomic reference counter), the type
is checked at runtime and a virtual method call is used to collaborate with the
C++ STL container API for value equality and hash.
Set<T>copy the element inserted, andMap<K, V>copy the inserted Key to ensure that the STL container is used correctly (value semantic) (no mutation of an element in the set, and no mutation for the key).In most cases, an element of a set and a key are immutable types, so the copy is just an incrementation of the reference counter. This strong behavior is expressed by the use of a
std::shared_ptr<Value const>in the API.
class Set {
...
void add(std::shared_ptr<Value const> const & value);
struct cmp {
// instance passed as a const reference of shared_ptr<>.
bool operator()(std::shared_ptr<Value const> const & a, std::shared_ptr<Value const> const & b) const {
return isLess(a->compare(b)); // Virtual function call for the method compare.
}
};
std::set<std::shared_ptr<Value const>, cmp> _storage; // Container adaptation through struct cmp.
...
}
class Map {
...
void set(std::shared_ptr<Value const> const & key, std::shared_ptr<Value> const & value);
...
void remove(std::shared_ptr<Value const> const & key);
bool contains(std::shared_ptr<Value const> const & key) const;
...
struct cmp {
bool operator()(std::shared_ptr<Value const> const & a, std::shared_ptr<Value const> const & b) const {
return isLess(a->compare(b));
}
};
std::map<std::shared_ptr<Value const>, std::shared_ptr<Value>, cmp> _storage;
...
}
Key¶
A key implements the notion for an instance of a concept.
The tuple (instanceId, runtimeId) uniquely identify an instance.
Concept are possibly related with a parent concept by using the DSM keyword
is a. A key can be up-casted and down-casted to a parent key or to a
children key.
When a key is created the member Viper::Key::typeConcept keeps the original concept
type at the instantiation point. When you upcast or downcast a key, only the member
Viper::Key::typeKey changes to represent the actual concept used by the key.
class ValueKey final : public Value {
public:
std::shared_ptr<TypeKey> const typeKey;
std::shared_ptr<TypeConcept> const typeConcept;
UUId const instanceId;
...
// MARK: - Conversion
bool hasParentKey() const;
std::shared_ptr<ValueKey> toParentKey() const;
std::shared_ptr<ValueKey> toConceptKey() const;
std::shared_ptr<ValueKey> toClubKey(std::shared_ptr<TypeClub> const & typeClub) const;
std::shared_ptr<ValueKey> toAnyConcept() const;
bool isMember(std::shared_ptr<TypeConcept> const & targetConcept) const;
std::shared_ptr<ValueKey> toMemberKey(std::shared_ptr<TypeConcept> const & targetConcept) const;
std::shared_ptr<ValueKey> toKey(std::shared_ptr<TypeKey> const & typeKey) const;
...
}
TypeAnyConcept is not the parent concept of all concepts. it's a container for all key types.
Path¶
A Path is used to locate a piece of information in a composed value (like a struct within another struct...).
A Path is used by the Commit Evaluator to implement the collaborative mutators.
A Path is not typed; it's just a way to iteratively traverse a Viper::Value
by resolving the current value at the current Viper::PathComponent until the end of
the Path.
The Function System¶
The function system is used to expose a collection of functions to the dynamic runtime.
A Viper::Function is registered in a Viper::FunctionPool and Viper::FunctionPool are registered
in a Viper::Service. A service can be promoted to a Viper::RPCServiceClientHandler to
implement a public micro Service.
A function is an abstraction and a concrete function implements the pure virtual method
checkedCall(...).
// Viper
class Function {
public:
std::shared_ptr<FunctionPrototype> const prototype;
std::string const documentation;
Function(std::shared_ptr<FunctionPrototype> const & prototype,
std::string const & documentation = {});
virtual ~Function() = default;
std::shared_ptr<Value> call(std::vector<std::shared_ptr<Value>> const & args) const;
std::string representation() const;
protected:
virtual std::shared_ptr<Value> checkedCall(std::vector<std::shared_ptr<Value>> const & args) const = 0;
};
The concrete implementation for a C++ lambda with <functional>.
class FunctionLambda final : public Function {
public:
std::shared_ptr<FunctionPrototype> const prototype;
std::function<std::shared_ptr<Value>(std::vector<std::shared_ptr<Value>> const &)> const function;
...
}
The C++ generated code for the function int64 add(int64 a, int64 b) declared in a
function pool named Tools.
// MARK: - Tools::Add
class F_add final: public Viper::Function {
public:
static std::shared_ptr<F_add> make() {
std::vector<Viper::FunctionPrototype::Parameter> parameters;
parameters.push_back({"a", ValueType::type_int64()});
parameters.push_back({"b", ValueType::type_int64()});
auto const returnType {ValueType::type_int64()};
return std::shared_ptr<F_add>{new F_add {
Viper::FunctionPrototype::make("add", parameters, returnType),
"Return a + b."
}};
}
std::shared_ptr<Viper::Value> checkedCall(std::vector<std::shared_ptr<Viper::Value>> const & args) const override {
auto const a {ValueDecoder::decode_int64(Viper::Int64::cast(args.at(0)))};
auto const b {ValueDecoder::decode_int64(Viper::Int64::cast(args.at(1)))};
return ValueEncoder::encode(PoolBridge::Tools::add(a, b));
}
private:
F_add(std::shared_ptr<Viper::FunctionPrototype> const & prototype, std::string const & documentation)
: Viper::Function {prototype, documentation} {}
};
Parsing DSM Definitions¶
Viper::DSMP is the private namespace for parsing the DSM language.
How it works...
1) The content of all DSM files are concatenated in a single content with
Viper::DSMBuilder::append(...).
2) The parser transforms this content to an AST representation of
Viper::DSMP::Node*.
3) Semantic checkers are run on the AST representation and add their errors with
Viper::DSMParseReport::add(...).
4) The AST representation is converted to a Viper::DSMDefinitions if no errors are
reported.
// Viper_DSMHelper.cpp
std::shared_ptr<DSMDefinitions> parse_(std::shared_ptr<DSMBuilder> const & builder,
std::shared_ptr<DSMParseReport> const & report) {
ANTLRInputStream input_stream(builder->source());
DSMLexer lexer(&input_stream);
CommonTokenStream token_stream(&lexer);
DSMParser parser(&token_stream);
...
tree::ParseTree * tree {parser.definitions()};
auto const v {parser.getNumberOfSyntaxErrors()};
if (v != 0) {
return nullptr;
}
auto const listener {new Listener};
tree::ParseTreeWalker walker;
walker.walk(listener, tree);
auto const definitions {listener->definitions};
checkDefinitionsSemantic(definitions, builder, report);
if (report->hasError()) {
return nullptr;
}
return Converter::convert(definitions);
}
DSM Definitions¶
a Viper::DSMDefinitions is only created if the parsing of the DSM content is valid.
Many immutable Viper::DSM* classes are required for a DSM Definitions. Those classes
can be recursively traversed to converter a DSM Definitions to other representations
like GraphViz, ULM classes, C++ implementations...
Viper::DSMDefinitions and Viper::DSMDefinitionEncoder are used to transmit the DSM
Definitions to the code generator kibo-1.2.0.jar written in java.
a Viper::DSMDefinition can regenerate a DSL representation (DSM Language).
The generated representation can be in plain text or in HTML.
The Codec System¶
Viper defines the notion of codec for converting an instanced object to a binary
representation. This binary representation is used for persistence, copy/paste, C++/Value
interoperability, Remote Procedure Call, and also to exchange data with external tools like
kibo.
There are many encoders and decoders:
- Viper::TypeEncoder/Decoder
- Viper::ValueEncoder/Decoder
- Viper::PathEncoder/Decoder
- Viper::DefinitionEncoder/Decoder
- Viper::FunctionPrototypeEncoder/Decoder
- Viper::ServiceFunctionPoolEncoder/Decoder
- Viper::DSMDefinitionsEncoder/Decoder
- ...
An encoder takes an instance and returns a Blob. A decoder takes a
Blob and returns an instance.
Encoder/Decoder are based on Writer/Reader to write/read a Stream.
StreamCodec, StreamEncoder and StreamDecoder¶
Viper implements a Viper::StreamBinaryEncoder and a Viper::StreamTokenBinaryEncoder.
The Viper::StreamTokenBinaryEncoder uses a Viper::StreamBinaryEncoder but inserts a
Token before using a method of the Viper::StreamWriting interface. The Stream is
Tokenized and a loose of synchronization between an encoder and decoder is detected
immediately.
Viper implements Viper::StreamBinaryCodec and Viper::StreamTokenBinaryCodec.
Viper::StreamTokenBinaryEncoderwas useful during the development of Viper. We continue to useViper::StreamTokenBinaryEncoder/Decoderwhen we exchange data with an insecure world.
Json/Bson Encoder and Decoders¶
To interop with the Web, we use.
Viper::JSonDSMDefinitionsEncoderto transmit DSM Definitions.Viper::JSonDSMDefinitionsEncoderto receive DSM Definitions.Viper::JSonValueEncoderto transmit a value.Viper::JSonValueDecoderto receive a value.
Bson is also supported.
The RPC system¶
Viper defines various classes to implement the Remote Procedure Call Infrastructure.
A Viper::RPConnection is a specialized BSD Socket for transmitting and receiving a
Viper::RPCMessage which contain the binary representation for an instance of
a derived class of Viper::RPCPacket registered in a Viper::RPCProtocol.
The RPC infrastructure usages are:
- Access and synchronize Commit databases.
- Browse and access Database in a Remote Database Repository.
- Browse and call Function in Remote Service.
The three associated protocols are defined in Viper::RPCProtocols
namespace Viper {
class RPCProtocol;
namespace RPCProtocols {
std::shared_ptr<RPCProtocol const> remoteCommitDatabasing();
std::shared_ptr<RPCProtocol const> remoteDatabasing();
std::shared_ptr<RPCProtocol const> remoteService();
}} // ns
General purpose packets are shared by protocols, and the shared common feature are implemented in:
Viper::RPCSideClientCallhandles the call/return for the client side.Viper::RPCSideServerCallhandles the call/return for the server side.Viper::StreamReadingHelperimplements the deserialization of the packet payload.Viper::StreamWritingHelperimplements the serialization of the packet payload.
The RPC Infrastructure use the Codec StreamTokenBinary to encode
Viper::RPCMessage
RPCPacket¶
All packets inherit from Viper::RPCPacket and implement the pure virtual method write.
namespace Viper {
class StreamCodecInstancing;
class RPCPacket {
public:
UUId const code;
std::string const name;
RPCPacket(UUId const & code, std::string name);
virtual ~RPCPacket() = default;
virtual void write(std::shared_ptr<StreamWriting> const & writing) const = 0;
virtual std::string representation() const;
};
} // ns
Packets are logically discriminated:
Viper::RPacketCall<something>to call a specific function.Viper::RPacketReturn<something>to return a specific value.
Packet are identified by a packetId (UUId) and each packet defines a specific payload.
Hashing and hasher¶
Viper::Hashing defines the interface for hashing.
namespace Viper {
class Hashing {
public:
virtual ~Hashing() = default;
virtual void update(void const * data, std::size_t size) = 0;
virtual void reset() = 0;
virtual std::string name() const = 0;
virtual std::size_t digestSize() const = 0;
virtual std::size_t blockSize() const = 0;
virtual Blob digest() const = 0;
virtual std::string hexDigest() const = 0;
};
Concretes implementations are Hasher:
Viper::HashCRC32Viper::HashMD5Viper::SHA1Viper::SHA3Viper::SHA256
SQLite Modules¶
SQLite3 usages are split in modules to be reused in various database implementations.
Viper::SQlitethe C++ wrapper that adapts C type and raise Error.Viper::SQliteStatementthe C++ wrapper that adapts C type and raise Error.Viper::SQliteBlobthe C++ wrapper that adapts C type and raise Error.Viper::SQliteTableMetadatathe metadata API used by Database/Commit Database.Viper::SQliteTableBlobthe blob API used by Database/Commit Database.Viper::SQliteTableAttachmentthe Instance/Document API use by Database.Viper::SQliteTableCommitthe Commit API use by Commit database.
Database¶
A Database is a Key-Value database that associates a Document with a Key.
The Primary Key is the tuple formed by (attachmentRuntimeId, instanceId, runtimeId), and the Document is a blob that contains the encoded binary representation_ of the Document (see The codec system).
The implementation is trivial since we delegate the features to the reusable modules
Viper::SQliteTableMetadata, Viper::SQliteTableAttachment and Viper::SQliteTableBlob.
Databasing Interface¶
The Viper::Databasing interface defines the Low Level API for an abstract Database.
The concrete implementation are Viper::DatabaseSQLite for a local SQLite3 Database and
Viper::DatabaseRemote for a remote repository of Viper::Database.
An application can use this interface to abstract the location of a Database.
Commit¶
Commit is a technology that persists fine-grained mutations of values in a transactional approach.
CommitState & CommitMutableState¶
Values are accessible from an isolated readonly Viper::CommitState through the interface
Viper::CommitGetting and mutations only occurs inside a Viper::CommitMutableState
through the interface Viper::CommitMutating.
To achieve fine-grained collaborative mutations at any structural level,
Commit is based on Path.
Viper::CommitMutatinginherits fromViper::CommitGetting, so we can access a modified value in aViper::CommitMutableStateduring mutations.
Mutations, Commands and Value¶
When a method of the interface Viper::CommitMutating is called, the arguments are
captured in a specific command adapted for the method signature. For each method,
there is a Viper::CommitCommandType but many methods have the same signature and use
Viper::CommitCommandPathValue.
All commands inherit from Viper::CommitCommand to capture the tuple
(attachmentRuntimeId, instanceId, runtimeId).
To reconstruct a Value the Viper::CommitState use the
Viper::CommitEvaluator to apply all CommitCommand to a new default initialized value.
Persistence of Mutations¶
To persist the history of all tracked mutations produced in the life cycle of an application, a DAG of Commit is persisted in a Commit Database.
A Commit has a Viper::CommitHeader that identify and describe the Node
in the DAG and an optional binary encoded representation of all
Viper::CommitCommand if the Viper::CommitType of the Node
is a Viper::CommitType::Mutations.
Each Commit is identified by a commitId and references it's parent
Commit with parentCommitId to form the DAG of Commit.
To query arbitrary values at a specific Commit, the Commit Engine create a new
Viper::CommitState by fetching the persisted data for all Commits involved
by the required Commit, to construct a list of Viper::CommitEvalAction.
A Viper::CommitState is then initialized with the list of Viper::CommitEvalAction
and implements the Viper::CommitGetting interface with the help of the
Viper::CommitEvaluator.
When we query a specific value, the Viper::CommitState/CommitMutableState
use the Viper::CommitEvaluator to apply all Viper::CommitCommand of the list of
Viper::CommitEvalAction and return an optional reconstructed value.
The return value is optional since the value may not exist for this Commit.
CommitDatabasing Interface¶
Viper::CommiDatabasing is the interface use to save and fetch the data associated for a
Commit.
There is an implementation for a local SQLite3 Database Viper::CommitDatabaseSQLite
and an implementation for a remote database Viper::CommiteDatabaseRemote.
This interface is used by Viper::CommitDatabase and by the Viper::CommitSynchronizer
to synchronize Commit databases.
The interface hides the location of the Database (local or remote).
Database Synchronization¶
Synchronizing means 'merge unknown definitions and add missing commits and blobs.'
Since commits and blob are immutable, the synchronization process is trivial, but to extend the embedded definitions, we need to compute the new definitions inside an exclusive transaction.
void CommitSQLiteDatabase::extendDefinitions(std::shared_ptr<Definitions const> const & other) {
try {
_db->beginTransaction(DatabaseTransactionMode::Exclusive);
auto const defs {Definitions::make()};
defs->extend(definitions());
defs->extend(other);
SQLiteHelper::updateDefinitions(_db, _codec, defs);
_db->commit();
} catch (Error const & /*e*/) {
_db->rollback();
throw;
} catch (std::exception const & /*e*/) {
_db->rollback();
throw;
}
}
How it works:
1) Fetch unknown source definitions, commits and blobs and add into the target. 2) Push unknown target definitions, commits and blobs and add into the source.
The algorithm is implemented in Viper::CommitSynchronizer with the help of
Viper::CommitSyncronizerHelper.
- Commits are incrementally added by traversing the topological sorted list of Commit to add.
- Small Blobs referenced by a Commit are packed together to reduce the RPC traffic.
Database Conversion¶
Viper::DatabaseToCommitDatabaseConverter converts a Viper::Database to a
Viper::CommitDatabase by creating a document in the first Viper::CommitMutableState
for all the documents found in the Viper::Database.
Viper::CommitDatabaseToDatabaseConverter converts a Viper::CommitDatabase
at a specific commit to a Viper::Database by creating a new document
for all documents available in the Viper::CommitState.
Application Model¶
Redux inspires the application model recommended for the commit technology.
How it works.
1) Initialize a CommitStore with a CommitDatabase. 2) Dispatch a mutating function. 3) Persist the mutations into a new Commit. 4) Use the new Commit. 5) Update the undo/redo stack. 6) Notify the application that a new State is present and may update the GUI of represented objects.
This application model is implemented by the class Viper::CommitStoreBase
and a concrete Store need to implement the pure virtual methods database(),
state(), notifier() and reset().
The Viper::CommitStoreBase has a rich API to:
- Manage the Commit DAG (commit mutations, enable/disable commit, merge heads...)
- Dispatch all sort of executable code (C++ lambda, Commit Function, collaborative mutators...)
- Manage the undo/redo stack (canUndo, canRedo, undo, redo...)
Dispatching¶
Dispatching means 'executing an algorithm that mutates the current state in an isolated context.'
-
If the algorithm has run without raising an exception, the mutations are commited to the database within a transaction, and the application is notified that a new state in present, and may update the GUI of all represented objects.
-
If the algorithm has raised an exception, then the application is notified that an error has occurred. (It's just a nop from the point of view of the state).
Here is the code from Viper::CommitStoreBase that illustrate the many steps involved to
dispatch a mutating function with the collaboration of the virtual methods
for a concrete store.
// The dispatch of a C++ lambda
void CommitStoreBase::dispatch(std::string const & label,
std::function<void(std::shared_ptr<CommitMutating>const&)> const & function) {
if (!database()) // use the pure virtual method database()
throw CommitStoreErrors::noDatabase(module, "dispatch");
try { // 2a) Handle exceptions
auto const ms {mutableState()}; // 2b) Create a new CommitMutableState from the current CommitState.
function(ms); // 2c) Call the function by injecting the CommitMutating interface.
commitMutations(label, ms); // 3,4,5,6) Persist the mutations in a new labeled Commit.
} catch(Viper::Error const & e) { // 6) Notify the application for an Error (CATCH_NOTIFY_ERROR expanded)
notifyDispatchError(e);
} catch(std::exception const & e) {
notifyDispatchError(GeneralErrors::basic(module, e.what()));
}
}
std::shared_ptr<CommitMutableState> CommitStoreBase::mutableState() const {
return CommitMutableState::make(state()); // use the virtual methods state() to get the current CommitState.
}
void CommitStoreBase::commitMutations(std::string const & label, std::shared_ptr<CommitMutableState> const & mutableState) {
if (!database()) // use the virtual method database()
throw CommitStoreErrors::noDatabase(module, "commitMutations");
auto const newCommitId {database()->commitMutations(label, mutableState)}; // 3) create a new Commit
useCommit(newCommitId); // 4) use the new Commit
_undoStack.set(state()->commitId); // 5) Update the undo/redo stack.
notifyStateDidChange(); // 6) Notify the application for a new State.
}
void CommitStoreBase::notifyStateDidChange() {
if (auto const n {notifier()}) // 6a) use the virtual method to get the concrete Notifier
n->notifyStateDidChange(); // 6b) notify the application for a new state
}
void CommitStoreBase::notifyDispatchError(Error const & e) {
if (auto const n {notifier()}) // 6a) use the virtual method to get the concrete Notifier
n->notifyDispathError(e); // 6c) notify the application for an error
}
The dispatch idiom variants:
void dispatch(std::string const & label,
std::function<void(std::shared_ptr<CommitMutating>const&)> const & function);
void dispatchPool(std::string const & label,
std::shared_ptr<CommitFunctionPool> const & pool,
std::string const & funcName,
std::vector<std::shared_ptr<Value>> const & args);
void dispatchSet(std::string const & label,
std::shared_ptr<Attachment> const & attachment,
std::shared_ptr<Key> const & key,
std::shared_ptr<Value const> const & value);
void dispatchDiff(std::string const & label,
std::shared_ptr<Attachment> const & attachment,
std::shared_ptr<Key> const & key,
std::shared_ptr<Value const> const & value,
bool recursive);
void dispatchUpdate(std::string const & label,
std::shared_ptr<Attachment> const & attachment,
std::shared_ptr<Key> const & key,
std::shared_ptr<Path const> const & path,
std::shared_ptr<Value const> const & value);
void dispatchEnableCommit(CommitId const & commitId, bool enabled);
Notifying the application¶
The concrete implementation of the notifier() methods returns a concrete
notifier that implement the Viper::CommitStoreBaseNotifying interface to communicate
with the notification system used by the GUI of the application.
ex: A StoreNotifier based on the NotificationCenter for AppKit (Apple).
class StoreNotifier: public Viper::CommitStoreBaseNotifying {
public:
static std::shared_ptr<StoreNotifier> make();
void notifyDatabaseDidOpen() override;
void notifyDatabaseDidClose() override;
void notifyDatabaseWillReset() override;
void notifyDefinitionsDidChange() override;
void notifyStateDidChange() override;
void notifyDispatchError(Viper::Error const & e) override;
...
private:
StoreNotifier() = default;
};
std::shared_ptr<StoreNotifier> StoreNotifier::make() {
return std::shared_ptr<StoreNotifier> {new StoreNotifier()};
}
void StoreNotifier::notifyDatabaseDidOpen() {
[NSNotificationCenter.defaultCenter postNotificationName:DSCommitStoreBaseDatabaseDidOpenNotification object:nil];
}
void StoreNotifier::notifyDatabaseDidClose() {
[NSNotificationCenter.defaultCenter postNotificationName:DSCommitStoreBaseDatabaseDidCloseNotification object:nil];
}
void StoreNotifier::notifyDatabaseWillReset() {
[NSNotificationCenter.defaultCenter postNotificationName:DSCommitStoreBaseDatabaseWillResetNotification object:nil];
}
void StoreNotifier::notifyDefinitionsDidChange() {
[NSNotificationCenter.defaultCenter postNotificationName:DSCommitStoreBaseDefinitionsDidChangeNotification object:nil];
}
void StoreNotifier::notifyStateDidChange() {
[NSNotificationCenter.defaultCenter postNotificationName:DSCommitStoreBaseStateDidChangeNotification object:nil];
}
void StoreNotifier::notifyError(Viper::Error const & e) {
auto const alert {[NSAlert alertWithError:error_convert(e)]};
[alert beginSheetModalForWindow:NSApp.windows[0] completionHandler:nil];
}
...