SkillAgentSearch skills...

Ravioli

Stop doing spaghetti code.

Install / Use

/learn @dagatsoin/Ravioli
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Ravioli

Stop doing spaghetti code.

Ravioli are modular spaghetti bolognese. Also, it does not spread when you are hurry.

TLDR;

Installation

$npm install --save mobx @warfog/ravioli

Bon appétit.

Monorepo Structure

This repository is organized as a Yarn workspace:

ravioli/
├── packages/
│   └── ravioli/          # Core @warfog/ravioli package
├── exemples/             # Example projects (outside workspace)
│   ├── example-rpg/
│   ├── example-json-config/
│   ├── counter-client-server/
│   ├── counter-graphql/
│   └── counter-request-scoped/
└── doc/                  # Documentation (outside workspace)

Development

# Install dependencies
yarn install

# Build the package
yarn build

# Run tests
yarn test

# Watch mode for development
yarn build:watch

Basic exemple:

const user = createContainer<{ name: string }>()
    // How the model is mutated
    .addAcceptor("setName", {
        mutator(data, { name }: { name: string }) {
            data.name = name;
        },
    })
    // Map an action will be trigger the mutation (with the same argument)
    .addActions({ rename: "setName" })
    // Create an instance
    .create({ name: "Fraktos" });

// You can use it with MobX
autorun(() => console.log(user.representationRef.current.name))

// Trigger a "rename" action
user.actions.rename({ name: "Fraktar" });
// Will log:
// Fraktos
// Fraktar

API

API documentation is auto generated and available in a separate doc

Examples

All examples use the local distribution build. Build the main package first from the root:

yarn build

Then run the examples:

  • example-rpg - RPG game example with turn-based combat

    cd exemples/example-rpg && npm start
    
  • example-json-config - JSON-based container configuration example

    cd exemples/example-json-config && npm start
    
  • counter-request-scoped - Request-scoped container with NAP persistence and stepId hydration

    cd exemples/counter-request-scoped && npm run dev
    

Note: Examples are outside the workspace and use the compiled output from packages/ravioli/dist/.

Deep dive

What problems does Ravioli solve?

  • Model code spreading all over UI code.
  • Unclear limits on what is private and what is public.
  • Answer the question: Where do I put this code?

The goal of Ravioli is to code quick but not dirty.

Ravioli is an attempt for small teams or junior developer to organize their ideas to achieve great things like a todo list or a MMORPG.

Ravioli implements the SAM pattern, which helps you to cut down your software development by enforcing a temporal logic mindset and a strong separation of concern between business level and functional level.

All along your development with Ravioli, you will handle your issues with a bunch of good practices:

  • separate functional from business code
  • think you app as a state machine (finite or not)
  • write small, atomic, pure and focused business functions
  • write abstract and meaningful actions for your user

"Ravioli makes the world a better place by providing a reactive and declarative framework with the simplicity of a mutable world backed by the security of an immutable plumbing" An anonymous antousiast coming from the futur

Is ravioli for you?

  • :heavy_check_mark: you are a junior dev looking for a great developement experience
  • :heavy_check_mark: you have to develop a temporal logic app (tabletop game, form with steps, health data app, ...)
  • :heavy_check_mark: you are lead dev who is looking for a simple solution to onboard junior dev on project with Separation of Concern in mind.

A Ravioli step cycle

SAM and Ravioli are based on temporal logic. Each action starts a new step, like a tick for a clock. Here how it works:

  ____________________________________________
  |                                            |             ^  ^  ^  ^            
  |        ___________Model___________         |             |  |  |  |            
  v       |            Data           |        v               Notify             next
Action -> |         Acceptor(s)       | -> Compute next --->   changes  ----->   action
  ^       |___________________________|      State          to observers        predicate  
  |                                                                                 |    
  |_________________________________________________________________________________|    
  1. First something/someone triggers an action from the view.
  2. The action composes a proposal and presents it to the model.
  3. The model's acceptors accept/reject each part of the proposal and do the appropriate mutations.
  4. If the model is changed a new representation of the model is created.
  5. All observers of this container will react to the new representation. (eg. UI is updated)
  6. This new representation may need to trigger some automatic actions. If so, each automatic action will lead to a new step (if some changes are made).

The main difference with other lib/framework are:

A clear Separation Of Concern between Model and View.

View and Model isolation. The view (as the compiled representation of the model) has no read/write access to the model. This clear isolation makes the component like a black box which the user interacts with through some actions.

  • At the core of component, there is the model. Aka some data and some mutation rules. This data and mutations are completely private and are not usable from the View. This is where you will define the internal stats of a player, a todolist, etc.

  • What you see, as an observer of a container, is called the Representation. It could be anything, a JSON, some HTML, a 3D model etc. It is how the container wants to be seen from the exterior. It allows your code and semantic to be totally agnostic about how the representation will be consumed.

  • The View is what the final user perceives of the container. It is out of the scope of a Ravioli component. It is like a piece of Art, depending of the observer, you won't see the same thing. For example, a battle field could be represented as a JSON and the View could be a First Person Shooter or a Real Time Strategy game.

Each component has its own state machine (either finite or not)

This allows only certains actions when the component is in specific state.

For example: Player can be Alive or Dead and its actions can be mapped as:

  • Alive: hit, move
  • Dead: revive, spawn in cimetery

Those states being isolated, there is no chance that a Dead Player shots an Alive Player.

Ravioli is based on temporal logic.

Each component has its own logic clock. Each step is like the tick of a clock. A step starts with the trigger of an action and ends with a new representation. From the exterior of a component you have access to the tick with stepId which is an incrementing integer.

Actions are not mutations.

Ravioli actions are like in real life: an attempt to change the state of something without any guarantee of success.

In such, an action does not mutate the model but presents a proposal which the model will accept/reject in part or whole.

function hit() {
  return [{
    type: UPDATE_STAT
    payload: {
      field: "hp",
      amount: -3
    }
  }]
}

An action is some high level functional code which uses some low level business code.

Actions is like a client which uses a public API. The API is the available mutations, an atomic set of command publicly accessible, for example SET_HP and DROP_ITEM which can be composed together in a more functional drinkHealthPotion.

function drinkHealthPotion(itemId: number) {
  return [{
    type: DROP_ITEM,
    payload: { itemId }
  }, {
    type: UPDATE_STAT
    payload: {
      field: "hp",
      amount: 10
    }
  }]
}

Ravioli parts

Model

The model contains the business data and methods and lives in a private scope. The external world has no read access to it.

Data

The model data must me serializable.

{
  name: "Fraktar",
  health: 1,
  inventory: [{
    id: 0,
    quantity: 1
  }],
  isConnected: false
}

Acceptors

The acceptors is the imperatvie code which mutate the data.

An acceptor has two roles:

  1. Accept or reject a proposal mutation (with a validator)
  2. Do the mutation (with a mutator)

Condition

The role of the condition is to status is a mutation is acceptable or not. It is a simple pure function which takes the model and the payload and returns a boolean.

// Rules for health points mutations.
const checkHP = (data => payload => (
  data.health > 0 && // the player is alive
  payload.hp < MAX_POINT // the health points is legit
)

If the mutation passes the condition, it will be marked as accepted and passed down to the mutator.

Mutator

This is where the mutation happens. A mutator is a simple unpure function. It returns nothing and mutates the model data.

const setHpMutator = data => payload => (
  data.health += payload.hp
) 

Control State

This notion directly comes for the State Machine pattern. A Control State is a stable state your app can reach.

For example a Todo of a todo list can be Complete or UnComplete. A character of a video game can be Alive, Dead or Fighting.

Also, SAM and Ravioli do not enforce the concept of a FINITE state machine. You can have multiple control states in a row. Eg a app state can be ['STARTED', 'RUNNING_ONLINE', 'FETCHING']

A control state predicate is a pure function of the model.

const isAlive = data => data.health > 0

The component state exposes the current control states of the model in an array. myApp.state.controlStates // ['STARTED', 'RUNNING_ONLINE', 'FETCHING']

Action

An action is a

Related Skills

View on GitHub
GitHub Stars9
CategoryDevelopment
Updated2mo ago
Forks1

Languages

TypeScript

Security Score

70/100

Audited on Jan 23, 2026

No findings