ckb-js / kuai Goto Github PK
View Code? Open in Web Editor NEWA protocol and framework for building universal dapps on Nervos CKB
License: MIT License
A protocol and framework for building universal dapps on Nervos CKB
License: MIT License
Construct a build system similar to https://nx.dev/ or https://hardhat.org/ which covers basic usage of a framework
https://github.com/ckb-js/kuai/blob/develop/packages/samples/mvp-dapp/src/main.ts#L35
Running on Node.js 18, the address will be http://:::3000
because an IPv6 address is returned by server.address()
Ref: nodejs/node#40537
We can reference the design of #85
This feature is similar to Exception filters, which filters known errors and wrapped exceptions in the desired format.
Now in the MVP dapp, unknown exceptions, like those thrown from a third-party dependency, will be returned directly, then the response is unpredictable(string, custom structure, etc.). With the filter, all exceptions will be converted into a uniform error format.
At the early stage, we have a MVP DApp(#54) to verify data manipulation on a cell.
Now we are going to support on-chain contracts in kuai, so the MVP DApp should also cover the on-chain verification.
CI: https://github.com/ckb-js/kuai/actions/runs/4599392372
Seems that rejected by the server? Need help from @PainterPuppets
format
npm script and run it with staging files on committing;prettier
to remove semicolons.It's an issue that will achieve #189
Store
modelStore
modelStore
model from plain cells with the same dataLanguage
C, Rust
Compile
You can use any tool you like to compile the contract. However, capsule can make deployment easier.
Deploy & Upgrade
You can use tools such as capsule and ckb-cli to deploy / upgrade the contract. You can also write your own tools using sdk such as lumos, ckb-sdk-rust, etc.
Using capsule:
capsule build
There are several ways to deploy or upgrade contracts.
capsule [todo]
deploy
capsule deploy ...
upgrade
ckb-cli [todo]
deploy
ckb-cli
wallet transfer ...
upgrade
write your own tools using sdk
If Store
's set function can return inputs
and outputs
, it can't deduct the tx fee when returned. How about letting the tx fee calculate by developers in business code?
On the other hand, shall we let the set
function can be calling with chains? Like store.set().set().set()
? If so, we maybe need to provide another function to get the inputs
and outputs
, ex. store.set().set().set().submit()
Can you help me provide any suggestions?
Now the route in the mvp dapp is declared in an imperative way(
kuai/packages/samples/mvp-dapp/src/app.controller.ts
Lines 89 to 92 in 91a5ad3
It could be adjusted in a declarative way, as nest.js did(https://docs.nestjs.com/controllers#routing). This optimization could be implemented in IO layers
The .bit project currently has four main features
.bit Alias: https://docs.did.id/developers/dotbit-alias
.bit Namespaces: https://docs.did.id/developers/records-key-namespace
.bit schemas: https://github.com/dotbitHQ/das-types/tree/master/schemas
subDID: https://www.did.id/subdid
General docs: https://github.com/dotbitHQ/das-contracts/tree/master/docs
At the stage of MVP, we need to verify whether the capacity abstraction provided by kuai
can satisfy the DApp design, i.e., if the developer could add, delete, modify, and read cells of the dapp solely by Store
. Thus we choose the namespace
feature as the main function of the MVP DApp.
The detail of namespace
could be found in .bit document(https://docs.did.id/developers/records-key-namespace), while its schema can be found at https://github.com/dotbitHQ/das-types/blob/master/schemas/cell.mol#L263-L277
flowchart TD
input([Input]) --> is_store_live{Is store live?}
is_store_live -->|True| is_reading{Is reading a key?}
is_store_live -->|False| A(Kuai) --> C[/Activate a store with pattern/] --> is_reading
is_reading -->|True| reading[/reading the key from the live store/]
reading --> is_found{Is the key found in the store?}
is_found -->|True| output([Output])
is_found -->|False| D[/Activate a store with pattern/] --> E{Is new store activated?}
E --> |True| reading
E --> |False| output
is_reading -->|False| is_adding{Is adding a key?}
is_adding -->|True| F{Is store capacity enough?}
F --> |True| add_key[/Add key in the store/]
F --> |False| G[/Expand the store/]
G --> H{Is the store expanded?}
H --> |True| F
H --> |False| output
add_key --> output
is_adding -->|False| is_deleting{Is deleting a key?}
is_deleting --> |True| I[/Locate a store contains the key, similar to reading a key from store/]
I --> J[/Clear the specified key/] --> output
is_deleting --> |False| is_updating{Is updating a key?}
is_updating --> |True| K{Is store capacity enough?}
K --> |True| L[/Update the specified key/] --> output
K --> |False| M[/Expand the store and update the key/] --> output
is_updating --> |False| output
@kuai
is the most ideal scope for the kuai
project, with that the cli
could be referenced as @kuai/cli
.
However, the scope is unavailable on npm registry so we have to pick another one. Now @ckb-js
is accessible, how about naming our packages in the pattern @ckb-js/kuai-*
(only in package.json) @yanguoyu @felicityin @PainterPuppets
Document: #52
This idea describes a way to store all states off-chain, but as UTxO, developers may have the need to store states directly in cells, such as NFT, which is more suitable for UTxO
We can try to explore some variants of the merkle-like solution, so that states also exist directly in the on-chain as cells
Contract
modelContract
modelname
, symbol
, tokenURI
, totalSupply
amount
token tokenId
from from
to to
Token
models into oneFor merging the cells' data in Store
, I think we should consider the following things.
lodash.merge
get
, developers can get with merged data or appointed OutputPoint
set
, developers can merge all cells or update appointed OutputPoint
In my mind, here is my design to achive these:
mergeState
in Store
class Store<
StorageT extends ChainStorage,
StructSchema extends StorageSchema<GetState<StorageT>>,
Option = never,
> extends Actor<GetStorageStruct<StructSchema>, MessagePayload<StoreMessage>> {
protected mergeState: GetStorageStruct<StructSchema>
}
class Store<
StorageT extends ChainStorage,
StructSchema extends StorageSchema<GetState<StorageT>>,
Option = never,
> extends Actor<GetStorageStruct<StructSchema>, MessagePayload<StoreMessage>> {
useLatest: boolean = false
mergeCellsData(current: GetStorageStruct<StructSchema>, last: GetStorageStruct<StructSchema>): StructSchema {
if (this.useLatest) {
return current
}
const customizer = <T>(objValue: T, srcValue: T) => {
if (Array.isArray(objValue)) {
return objValue.concat(srcValue)
}
}
return mergeWith(current, last, customizer)
}
}
mergeState
when addState
or removeState
private addState({ cell, witness }: UpdateStorageValue) {
if (this.cellPattern && !this.cellPattern({ cell, witness })) {
// ignore cells from resource binding if pattern is not matched
return
}
if (cell.outPoint) {
const outPoint = outPointToOutPointString(cell.outPoint)
const value = this.deserializeCell({ cell, witness })
if (this.schemaPattern && !this.schemaPattern(value)) {
return
}
this.states[outPoint] = value
this.chainData[outPoint] = { cell, witness }
this.mergeState = this.mergeCellsData(value, this.mergeState)
}
}
private removeState(keys: OutPointString[]) {
keys.forEach((key) => {
delete this.states[key]
delete this.chainData[key]
})
Object.values(this.states).reduce((pre, cur) => this.mergeCellsData(pre, cur), {})
}
get
and set
support calls without delivering OuputPoint
get(paths?: StorePath): GetFullStorageStruct<StructSchema> {
// get value from this.mergeState
}
set(value: any, paths?: StorePath) {
if (this.useLatest) {
// get the latest cell to update and add capacity from old cells
return
}
//if update all, merge all cells to a new cell
// if updating one key, find the cell and use updated data, if capacity is not enough maybe use other cells.
}
This question comes from https://github.com/ckb-js/kuai/pull/181/files/37a82372b9fe57f1dbde0eda4764f0656d15b217#r1162301306
The schemaOptions
in store
is not mandatory(https://github.com/ckb-js/kuai/blob/develop/packages/models/src/store/store.ts#L53) and {}
will be returned if it's undefined
or not an object(https://github.com/ckb-js/kuai/blob/develop/packages/models/src/store/store.ts#L128)
In what case will the schema
be empty
Runtime of Kuai has been divided into 5 components(#7) and this issue is to elaborate the architecture and technical design of the Input/Output component, better to include use cases, interfaces, MVP or PoC description, internal architecture, and technical design.
Feel free to add any updates in this issue and the final document will be appended in the top message.
samples/mvp-dapp
directory in https://github.com/ckb-js/kuai/We can reference the design of #2
This issue is from #1 and will add a default compile
npm script to build the application. With this instruction, all applications generated by kuai-cli share the same workflow of compilation.
Runtime of Kuai has been divided into 5 components(#7) and this issue is to elaborate the architecture and technical design of the CKB-independent component, better to include use cases, interfaces, MVP or PoC description, internal architecture, and technical design.
Feel free to add any updates in this issue and the final document will be appended in the top message.
We can reference the design of #3
I have some ideas about the error code for mpv or kuai, can we use something like the following to define it? I would like to see what people think.
Application ID (three-digit number) - Module ID (three-digit number) - Error Type (one-letter) - Error Code (five-digit number)
For example: 001-001-E-00001
The meaning of each category is as follows:
Application ID: Indicates which application the error belongs to and can be represented by a three-digit number. For example, 001
represents application MVP1, and 002
represents application MVP2.
Module ID: Indicates which functional module the error belongs to in the application, and can be represented by a three-digit number. For example, 001
represents the login module, and 002
represents the market module.
Error Type: Indicates which type of error the error belongs to and can be represented by a one-letter code. For example, P
represents parameter errors, B
represents business errors, and O
represents other errors.
Error Code: Represents the specific error under the error type and can be represented by a five-digit number. For example, 00001
represents a signature error, and 00002
represents a cell not found error.
The benefit of using this error code is that it can quickly locate errors and display them in a friendly way.
Runtime of Kuai has been divided into 5 components(#7) and this issue is to elaborate the architecture and technical design of the Database component, better to include use cases, interfaces, MVP or PoC description, internal architecture, and technical design.
Feel free to add any updates in this issue and the final document will be appended in the top message.
PRD: #54
UI will be a replicated data.did.it
working with omnilock
https://data.did.id/magickbase.bit
Now in the mvp dapp, an instance of model is getting by method defined in the controller
kuai/packages/samples/mvp-dapp/src/app.controller.ts
Lines 67 to 87 in 91a5ad3
It returns an instance if one has been registered or instantiates one if there's no live model.
This logic could be encapsulated in the registry, so users don't have to write it in different dapps repeatedly.
Cell model is UTxO-like, and it is hard for us as application developers to get working with it because of shared state, capacity management, and script combination.
An initial idea is that the Kuai framework provides a runtime that is used to help developers write CRUD-style programs that manage off-chain state and generate proofs to update the merkle roots on-chain, i.e., state is stored off-chain and only one merkle root is stored on-chain.
To implement the feature mentioned above, we first need to do some feasibility study, like
Store
modelContract
model for overridingStore
As this issue said, the goal of the database layer is to allow dapps to migrate between databases at a lower cost (dapps won't migrate databases often but it's a prominent characteristic of this layer), so it's better to support multi databases, not just PostgreSql.
In addition, we should not decide which database to use for developers. Developers should decide it according to their own scenarios, because there are many factors that will affect the database selection.
Resource binding service in #7
There is currently some code in kuai-core that is not covered by the tests. More tests need to be added to cover these code
Some things that should be doing:
kuai init
cli testAs a build system, kuai needs to be able to perform tasks such as compiling, testing, starting nodes, etc. To better manage and expand these tasks, kuai needs a task system. In order to manage and expand these tasks, kuai needs a task system.
To achieve the above goals, the task system needs to have the following features
KuaiContext
, which may have config/anything, usually the context is a singleton (providing the ability to create multiple contexts manually)npx kuai TASK_NAME
Clarifying how many modules there are and what the system architecture looks like first helps us understand how the system works
KuaiContext
: Load configurations, plugins, tasks, etc. and initialize other modules.TaskSystem
RuntimeEnvironment
: Execution/management of task status, and task lifecycle.Task
: Specific logicOverrideTask
: extends from Task
, for override taskKuai CLI
: Parsing command line arguments, and providing good cli interaction and output. C4Component
title Architecture of kuai project context
Container(ctx, "Kuai Context", "", "Load configurations, plugins, tasks, etc. and initialize other modules.")
Container(cli, "CLI", "", "Parsing command line arguments, and providing good cli interaction and output.")
Container_Boundary(st, "Task system") {
Component(kre, "Kuai Runtime Environment", "", "Execution/management of task status, and task lifecycle.")
Component(task, "Task", "", "Specific logic")
Component(otask, "OverrideTask", "", "Task override")
Rel(kre, task, "Manage & Run")
Rel(otask, task, "Override")
}
Rel(ctx, kre, "init")
Rel(cli, ctx, "call")
KuaiContext
is the entry point for the whole system, responsible for loading config, task, etc... And to prevent multiple KuaiContext
at runtime, it should be a singleton pattern, and a new instance can be created by KuaiContext.createKuaiContext()
.
classDiagram
class KuaiContext {
+ RuntimeEnvironment env
+ TaskLoader tasksLoader
+ Extender[] extenders
+ setRuntimeEnvironment(RuntimeEnvironment env)
createKuaiContext()$
getKuaiContext()$
loadConfigAndTasks()$
}
Possible usage
// entry.ts
import { KuaiContext, RuntimeEnvironment } from 'kuai/core'
const ctx = KuaiContext.createKuaiContext();
const config = loadConfigAndTasks();
const env = new RuntimeEnvironment(
config,
ctx.tasksLoader.getTasks(),
ctx.extenders,
);
ctx.setRuntimeEnvironment(env)
classDiagram
class TaskParam {
String name
String type
String description
T defaultValue
Boolean isOptional
validator(value): Boolean
}
class Task {
String name
String description
Record~string, TaskParam~ params
Boolean isSubTask
action(args, env, runSuper)
}
class OverrideTask {
Task parentTask
}
class RuntimeEnvironment {
- Extender extenders
+ Record~string, Task~ tasks
+ run(name, args)
}
Task --o TaskParam: has
OverrideTask --|> Task : extends
RuntimeEnvironment --o Task : has
In the task system, any logic can be a Task
, which should have a name as the task identifier, a description , params defined, and a task handler.
In order to implement task override, an OverrideTask
is needed, which has an additional property of parentTask
compared to Task
. When the action
of OverrideTask
is run, the action
of parentTask
will be passed as runSuper
, so that the override of task can be implemented
We now have a simple task definition that allows us to run some logic, but the goal of the task system is to have some simple tasks that combine to accomplish a complex task, so we need a struct that manages all the tasks
RuntimeEnvironment
is a module to help us manage the execution of Task
and to share the context between Task
. When a task is executed, this is passed into the action of task as env
so that the context can be shared between tasks
When KuaiContext
calls loadConfigAndTasks
to initialize the project, it will run the kuai.config.ts
file. you can put the custom task in it, or refer to the custom task's file in kuai.config.ts
, so that the task can be injected into KuaiContext
. and these injected tasks will be passed as parameters when KuaiContext
initializes RuntimeEnvironment
.
Some helper functions are also provided in kuai/core
to help simplify the code
Possible usage
// kuai/core/helper
function task(name, description) {
const ctx = KuaiContext.getKuaiContext();
const loader = ctx.tasksLoader;
return loader.addTask(name, description)
}
// custom-task.ts
import { task } from 'kuai/core'
task(TASK_NAME)
.addParam("paramA", "a custom param", 'default value')
.action(async (args, env, runSuper) => {
// do somethingโฆ
})
// kuai.config.ts
import 'custom-plugin.ts'
When a Task
with the same name is registered multiple times, the subsequent Task
is loaded as an OverrideTask
, and the previous Task is saved as a parentTask
// custom-override.ts
import { task } from 'kuai/core'
task(TASK_NAME)
.action(async (args, env, runSuper) => {
// do something before parent taskโฆ
await runSuper()
// do something after parent taskโฆ
})
There is a run
method in RuntimeEnvironment
, which will find the matched task from tasks
, and when running it will check if the Task
is an OverrideTask
, if it is, it will pass the action of its parentTask
as runSuper
parameter to the action
of the task
Extending RuntimeEnvironment
is to add/change some values of RuntimeEnvironment
when initializing Kuai.
For example, we add a hi
method to RuntimeEnvironment
, and then call this hi
through the task, so that we can achieve the effect of extending the Kuai.
Possible usage
// kuai/core/helper
extendsRuntimeEnvironment(extender: (env: RuntimeEnvironment) => void) {
const ctx = KuaiContext.getKuaiContext();
ctx.extenders.push(extender);
}
// kuai.config.ts / custom-plugin.ts
import { extendsRuntimeEnvironment } from 'kuai/core'
extendsRuntimeEnvironment((env) => {
env.hi = () => {
console.log('hi')
}
})
task("hi", async (args, env) => {
env.hi()
});
Will yield:
$ npx kuai hi
hi
// custom-type.ts
import "kuai/types/runtime";
declare module "kuai/types/runtime" {
export interface RuntimeEnvironment {
hi: () => void;
}
}
CLI
facilitates users to run some scripts and functions in kuai environment, such as test
/ start node
/ deploy contract
etc.
The cli is mainly implemented by commander.js
, and the way to use cli in kuai is like this
$ npx kuai help
Kuai version 0.0.1
Usage: kuai [GLOBAL OPTIONS] <TASK> [TASK OPTIONS]
GLOBAL OPTIONS:
-h, --help Show help
-v, --version Show version number
AVAILABLE TASKS:
help Prints this message
node Starts a JSON-RPC ckb server
test Run tests
In this article we focus on how the task system will integrate with cli
Our task itself has a name and we want to be able to run the task by typing kuai TASK_NAME
directly, so this requires the cli module to dynamically load commands based on the task
The name
of the Task
will be used as the name of the command, and the params
will be parsed as options
Possible usage
// kuai/core/cli.ts
import { Command } from 'commander';
import { loadCommandFromTasks } from './helper';
const program = new Command();
const ctx = KuaiContext.getKuaiContext();
const commands = loadCommandFromTasks(ctx.tasksLoader.getTasks());
commands.forEach((command) => {
program.addCommand(command);
});
program.parse();
From the tsconfig.json
we can see that output
directory is set to dist
(https://github.com/ckb-js/kuai/blob/develop/packages/samples/mvp-dapp/tsconfig.json#L6), which is listed in .gitignore
. While there's an empty directory named build
(https://github.com/ckb-js/kuai/tree/develop/packages/samples/mvp-dapp/build) reserved in samples/mvp-dapp
. My question is that what's the difference between build
and dist
, and why is dist
ignored in git
(https://github.com/ckb-js/kuai/blob/develop/packages/samples/mvp-dapp/.gitignore#L161) but build
is not.
Same question to the libs
empty directory(https://github.com/ckb-js/kuai/tree/develop/packages/samples/mvp-dapp/libs)
This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.
These updates are currently rate-limited. Click on a checkbox below to force their creation now.
These updates have all been created already. Click a checkbox below to force a retry/rebase of any.
@typescript-eslint/eslint-plugin
, @typescript-eslint/parser
)@typescript-eslint/eslint-plugin
, @typescript-eslint/parser
)These are blocked by an existing closed PR and will not be recreated unless you click a checkbox below.
packages/samples/mvp-dapp/contract/contracts/kuai-mvp-contract/Cargo.toml
ckb-std 0.10.0
no-std-compat 0.4.1
serde 1.0
packages/samples/mvp-dapp/contract/types/Cargo.toml
cfg-if 1.0
molecule 0.7.2
no-std-compat 0.4.1
serde 1.0
serde_json 1.0.96
docker/mvp-dapp/docker-compose.yml
docker/mvp-dapp/Dockerfile
packages/docker-node/ckb/Dockerfile
nervos/ckb v0.113.1
.github/workflows/bump-version.yml
actions/checkout v4@b4ffde65f46336ab88eb53be808477a3936bae11
actions/setup-node v4
crazy-max/ghaction-import-gpg v6
peter-evans/create-pull-request v6
.github/workflows/deploy.yaml
actions/checkout v4@b4ffde65f46336ab88eb53be808477a3936bae11
actions/setup-node v4
actions/cache v4
docker/setup-qemu-action v3
docker/setup-buildx-action v3
docker/login-action v3
docker/build-push-action v5
.github/workflows/merge-main-to-develop.yml
.github/workflows/open-pr-to-main.yml
actions/checkout v4@b4ffde65f46336ab88eb53be808477a3936bae11
actions/script v7
.github/workflows/publish-packages.yml
actions/checkout v4@b4ffde65f46336ab88eb53be808477a3936bae11
actions/setup-node v4
actions/cache v4
.github/workflows/release-draft.yml
release-drafter/release-drafter v6
.github/workflows/test.yaml
actions/checkout v4@b4ffde65f46336ab88eb53be808477a3936bae11
actions/setup-node v4
actions/cache v4
actions-rs/install v0.1
codecov/codecov-action v4
package.json
@typescript-eslint/eslint-plugin 6.20.0
@typescript-eslint/parser 6.20.0
eslint 8.56.0
husky 9.0.10
jest 29.7.0
lerna 8.0.2
lint-staged 15.2.1
prettier 3.2.5
ts-jest 29.1.2
typescript 5.3.3
node >=18
packages/cli/package.json
commander 12.0.0
ts-node 10.9.2
packages/common/package.json
env-paths 2.2.1
find-up 5.0.0
@ckb-lumos/base 0.21.1
@ckb-lumos/lumos 0.21.1
node >=18
packages/core/package.json
@ckb-lumos/base 0.21.1
@ckb-lumos/config-manager 0.21.1
@ckb-lumos/hd 0.21.1
@ckb-lumos/helpers 0.21.1
@iarna/toml 2.2.5
@jest/core 29.7.0
chalk 4.1.2
decompress 4.2.1
enquirer 2.4.1
find-up 5.0.0
jsonrepair 3.5.1
lodash 4.17.21
read 3.0.1
@types/decompress 4.2.7
@types/lodash 4.14.202
node >=18
packages/docker-node/package.json
@ckb-lumos/lumos 0.21.1
dockerode 4.0.2
@types/dockerode 3.3.23
node >=18
packages/io/package.json
@ckb-lumos/lumos 0.21.1
http-errors 2.0.0
koa-body 6.0.1
koa-compose 4.1.0
koa-router 12.0.1
path-to-regexp 6.2.1
rxjs 7.8.1
@ckb-lumos/base 0.21.1
@ckb-lumos/rpc 0.21.1
@types/koa-compose 3.2.8
@types/koa-router 7.4.8
koa 2.15.0
reflect-metadata 0.2.1
packages/models/package.json
@ckb-lumos/base 0.21.1
@ckb-lumos/bi 0.21.1
@ckb-lumos/codec 0.21.1
@ckb-lumos/experiment-tx-assembler 0.21.1
@ckb-lumos/lumos 0.21.1
inversify 6.0.2
ioredis 5.3.2
lodash 4.17.21
reflect-metadata 0.2.1
rxjs 7.8.1
tslib 2.6.2
@jest/globals 29.7.0
typescript 5.3.3
node >=18
packages/samples/mvp-dapp/package.json
@ckb-lumos/base 0.21.1
@ckb-lumos/bi 0.21.1
@ckb-lumos/codec 0.21.1
@ckb-lumos/lumos 0.21.1
@koa/cors 5.0.0
dotenv 16.3.2
http-errors 2.0.0
koa 2.15.0
koa-body 6.0.1
@types/koa__cors 5.0.0
typescript 5.3.3
packages/samples/sudt/package.json
@ckb-lumos/lumos 0.21.1
http-errors 2.0.0
koa 2.15.0
koa-body 6.0.1
ts-node 10.9.2
typedoc 0.25.7
typescript 5.3.3
packages/typeorm/package.json
inversify 6.0.2
mysql 2.18.1
pg 8.11.3
reflect-metadata 0.2.1
rxjs 7.8.1
tslib 2.6.2
typeorm 0.3.20
@jest/globals 29.7.0
ts-node 10.9.2
typescript 5.3.3
An actor in actor model is the most basic computing unit to run business logic insulated. It is self-contained that receives messages and processes them sequentially.
Actor runtime is the moderator which decides how, when, where each actor runs, and routes messages transported in the actor system.
Under the control of actor runtime, a large number of actors can perform simultaneously and independently to achieve parallel computation.
Kuai
provides the basic implementation of an actor for developers to extend business logic and dominates these actors with the actor runtime for scalability and reliability.
The main idea delivered by the actor model is Divide-and-Conquer
. If a heavy task is assigned to actor_a, the task would be split into sub-tasks and delegated to actor_a's child actors, recursively, until sub-tasks are small enough to be handled in one piece.
Within the runtime, an actor will be activated(created) or deactivated(destroyed) automatically so developers don't have to pay attention to the lifecycle of a single actor. Once a message is sent to an actor located by identity, the actor runtime will activate one to handle the message. And if an actor has been idle for a while(no messages, no internal state), the actor runtime will deactivate it. In some cases, which we will mention later, an actor should be kept alive, it could send messages to itself periodically as a reminder.
To achieve this, a supervision tree is going to be introduced because actor model has a tree-like hierarchical structure. More about supervision tree could be learned from supervision principles
Actors communicate with each other exclusively by sending messages to targets while targets are not referenced directly, instead, messages are routed by the runtime. With this, actors are decoupled and their lifecycles could be taken over by the runtime.
To deliver a message, the recipient should have an identity that is transparent to the sender. For simplicity, address
is used as the identity in Kuai
runtime.
There're several ways to get a recipient's address. The simplest way is to register the newly activated actor in a registry. A registry is a local, decentralized, and scalable key-value address storage. It allows an actor to look up one or more actors with a given key. The actors registered in the registry will be managed by the runtime under different supervision/monitoring strategies(strategies could be found in supervision principles too)
The other way is more modular, an actor will only send messages to addresses it received, e.g. a parent spawns a child actor and gets a response of the child actor's address, with the response, the parent could start communicating with its child actor.
/*
* field decorated by @State will be registered as an internal state
*/
class CustomActor extends Actor<CustomState> {
@State()
state_a: CustomState['state_a']
}
type Status = ok | error | continue | timeout | stop
class Actor<State, Message> {
/*
* functions to send messages
*/
// make a synchronous call
function call (
address: ActorAddress,
message: CustomMessage,
timeout?: number
): {
status: Status,
message: CustomMessage,
}
// make an asynchronous call
function cast (
address: ActorAddress,
message: CustomMessage,
): void
// send message synchronized to named actors running in specified nodes
function multi_call (
nodes: Array<Node>,
name: string, // registered actor name
message: CustomMessage,
): {
responses: Array<{
status: Status,
message: CustomMessage,
}>
}
// broadcast messages to named actors running in specified nodes
function abcast (
nodes: Array<Node>,
name: string, // registered actor name
message: CustomMessage,
): void
// reply to the sender in a synchronized call
function reply (
client: ActorAddress,
message: CustomMessage,
): void
/*
* functions for lifecycle
*/
// start an actor outside the supervision tree
function start (
actorConstructor: ActorConstructor,
init: CustomState,
options?: Record<string, string>
): {
status: Status,
address: ActorAddress,
}
// start an actor within the supervision tree
function startLink (
actorConstructor: ActorConstructor,
init: CustomState,
options?: Record<string, string>
): {
status: Status,
address: ActorAddress,
}
// stop a actor
function stop (
actor: ActorAddress,
reason: string,
): void
}
class CustomActor extends Actor<CustomState, CustomMessage>{
/*
* callback behaviors are injected by decorators, and matched by Symbols
*/
// invoked when the actor is activated
@Init()
constructor (
init: CustomState
): {
status: Status,
state: CustomState,
}
// invoked when the actor is deactivated
@Terminate()
function (
status: stop,
reason: string,
state: CustomState,
)
// invoked to handle synchronous call messages, the sender will wait for the response
@HandleCall(pattern: Symbol)
function (
message: CustomMessage,
from: ActorAddress,
state: CustomState
): {
status: Status,
message: CustomMessage,
state: CustomState,
}
// invoked to handle asynchronous call messages, the send won't wait for the response
@HandleCast(pattern: Symbol)
function (
message: CustomMessage,
state: CustomState,
): {
status: Status,
state: CustomState,
}
// invoked to handle `continue` instruction returned by the previous call
@HandleContinue()
function (
message: CustomMessage,
state: CustomState,
}: {
status: Status,
state: CustomState,
}
// invoked to handle all other unmatched messages
@HandleInfo()
function (
message: any,
state: CustomState,
): {
status: Status,
state: CustomState,
}
// invoked to inspect internal state of the actor
@FormatStatus()
function (
reason: string,
state: CustomState,
context: Context,
): void
}
The state
field in the parameters is the state of actor before the action and the state
field returned by callback is the state of actor after the action(framework will update the internal state of the actor, quite similar to useState
hook in React: useAction(preState => curState)
).
// TODO:
It's actually a Store
model we will design later.
interface ActorSpec {
name?: Symbol
id: Symbol
address: ActorAddress
strategy: SupervistionStrategy
}
class Supervisor extends Actor {
@State()
actors: Map<ActorSpec, Actor>
constructor (
children: Array<{
child: ActorConstructor,
spec: Pick<ActorSpec, 'strategy'>
}>,
options: Record<string, string>,
): {
status: Status
children: Array<ActorSpec>
}
// stop the supervisor
function stop (
reason: string,
)
// add actor children
function startLink (
children: Array<{
child: ActorConstructor,
spec: Pick<ActorSpec, 'strategy'>
}>,
options: Record<string, string>
): {
status: Status,
children: Array<ActorSpec>,
}
// add a single child
function startChild (
actorConstructor: ActorConstructor,
spec: Pick<ActorSpec, 'strategy'>,
): {
status: Status,
child: ActorSpec,
}
// restart a child spec which has been terminated
function restartChild(
spec: ActorSpec,
): {
status: Status,
}
// delete a child spec which has been terminated
function deleteChild (
spec: ActorSpec
): {
status: Status,
}
// terminate but keep the child spec
function terminateChild (
spec: ActorSpec
): {
status: Status,
}
}
// TODO: Design Store
model
// TODO: Design contract
model
// TODO: Design token
model
PRD: #54
The message queue in #29 is roughly implemented with a Map
for fast delivery so other modules won't be blocked.
It's expected to be done with redis stream as designed in #17
C4Context
title Architecture of Kuai runtime
Container_Boundary(b0, "Kuai runtime") {
Boundary(io_comp, "Input/Output", "Component") {
System(ckb, "CKB Node", "Chain data")
System(input, "input", "Requests from users/dapps")
System(output, "output", "Responses to users/dapps")
System(main_entry, "main_entry", "Entry of DApp that launches components")
Rel(input, main_entry, "")
Rel(output, main_entry, "")
Rel(main_entry, token, "manage")
Rel(main_entry, contract, "manage")
Rel(ckb, resource_binding, "bind")
}
Boundary(ckb_comp, "CKB related", "Component") {
Boundary(b5, "CKB models", "System") {
Component(store, "Store model")
Component(contract, "Contract model")
Component(token, "Token model")
Rel(token, contract, "extend")
Rel(contract, store, "extend")
Rel(store, actor_models, "extend")
}
System(resource_binding, "Resource binding", "Trigger state update by on-chain transactions")
Rel(resource_binding, store, "trigger")
}
Boundary(basic_comp, "CKB-independent", "Component") {
System(actor_models, "Actors", "Encapsulate code and data in a reusable actor as the most basic computation unit")
System(secrets, "Secrets", "Limit access to secrets, adopt secret scope policy with restriction permission")
System(configuration, "Configuration")
System(observability, "Observability", "Metrics, logs, and data tracing")
Rel(secrets, actor_models, "implement")
Rel(configuration, actor_models, "implement")
Rel(observability, actor_models, "extends")
}
Boundary(db_comp, "Database", "Component") {
System(storage, "Storage", "With pluggable DBMS for storing and querying data")
Rel(storage, store, "data query and update")
}
Boundary(external_services, "External Services", "Component") {
SystemDb_Ext(database, "DBMS")
System_Ext(secrets_manager, "Secret manager")
System_Ext(config_manager, "Configuration manager")
System_Ext(metrics, "Metrics service")
System_Ext(logs, "Logs service")
System_Ext(health_check, "Health check service")
Rel(database, storage, "connect")
Rel(secrets_manager, secrets, "")
Rel(config_manager, configuration, "")
Rel(metrics, observability, "")
Rel(logs, observability, "")
Rel(health_check, observability, "")
}
UpdateLayoutConfig($c4ShapeInRow="10", $c4BoundaryInRow="1")
}
There're 5 main components running in runtime
Input/Output Component: there're only two directions of data flow on CKB: chain -> user and user -> chain, so they are grouped together as an IO component. This component is to handle messages/data from the two endpoints.
CKB Related Component: this component is a group of dapp business rules. There're 3 basic abstractions of CKB's unit, store
, contract
, and token
to be extended for dapp specific rules. Each model extended from the basic abstraction could be regarded as a use case.
CKB-independent Component: this component provides general functions of a framework, especially those that are not coupled with CKB cell models. From this perspective, the dapp could be platform-agnostic.
Database Component: database component is outlined for flexibility, with this component, state storage could be decoupled from an explicit database.
External Services Component: some third-party services could be used to enhance functions of kuai.
As a dApp user, I want the local network to be simple enough to use out of the box.
We can take a cue from hardhat node
> npx hardhat node Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:8545/ Accounts ======== WARNING: These accounts, and their private keys, are publicly known. Any funds sent to them on Mainnet or any other live network WILL BE LOST. Account #0: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 (10000 ETH) Private Key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 Account #1: 0x70997970c51812dc3a010c7d01b50e0d17dc79c8 (10000 ETH) Private Key: 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d Account #2: 0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc (10000 ETH) Private Key: 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a
Since CKB does not have a tool like Ganache (Tippy is written in C# and it is outdated) now, we can use CKB with a default config for the testing purpose as local network at first
Kuai is likely to write contracts in C/Rust in the long term, so we can't live without the RISC-V toolchain, which means we will be depending on Docker, so the local network can also depend on Docker directly, which will require Docker on the developer's machine
The MVP DApp is expected to be work with MetaMask at this early stage.
Meanwhile, a browser extension wallet of Nervos CKB, named Nexus(https://github.com/ckb-js/nexus) is under development.
Once Nexus is ready for tests, we will try to support it in the MVP DApp.
As the proposal said
Note that a Store model could be a group of cells matching the same pattern and working as an entity, so it could be regarded as a virtual DApp. Say we have 2 DApps in School Roster Store models, each of them consists of many Student Store models. And we are going to build a Sports Meeting DApp, a new Sports Meeting Store model could be created and consist of partial Student Store from School A and B, respectively, it should work as same as a School Roster DApp.
A store
model should have a pattern
that is used to bind to specific cells. It's defined in the architecture as Resource binding
between CKB Node
and Store model
.
With the binding, a store
model maps itself to specific cells and parses data from those cells into a structured data model as the state of a dapp.
Any updates on the chain should be delivered to the store
model and keep its state up with on-chain data.
Please add the technical design of resource binding
between store
and ckb node
in this issue, and then we can make a plan for the implementation.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.