Ravioli
Stop doing spaghetti code.
Install / Use
/learn @dagatsoin/RavioliREADME
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
| |
|_________________________________________________________________________________|
- First something/someone triggers an action from the view.
- The action composes a proposal and presents it to the model.
- The model's acceptors accept/reject each part of the proposal and do the appropriate mutations.
- If the model is changed a new representation of the model is created.
- All observers of this container will react to the new representation. (eg. UI is updated)
- 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:
- Accept or reject a proposal mutation (with a validator)
- 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
node-connect
352.9kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
111.5kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
352.9kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
352.9kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
