Crons
With SCP (Storage-based Consensus Paradigm) and its nature of deterministic calculation, defining and executing scheduled tasks can be built-in to smart contracts.
You can use JSON-based functional programming language called FPJSON (opens in a new tab) to build complex tasks.
Add a Cron
const cron = {
start: 1659505371,
span: 60 * 60 * 12,
do: true,
times: 10,
jobs: [ [ "upsert", { times: db.inc(1) }, "conf", "count-crons" ] ]
}
await db.addCron(cron, "count-crons")
span
and jobs
are mandatory fields.
start
- timestamp the cron should start at, if omitted, it will be set toblock.timestamp
end
- timestamp the cron should end at, if omitted, it will run indefinitelyspan
- seconds between each execution, be careful not to set it too short like1
second outside the test purposesdo
- whether the task should be executed atstart
, it defaults tofalse
if omitted, and in that case, the first execution time will bestart + span
times
- how many times the task should be executed, it will run forever if omittedjobs
- array of tasks to be executed
Jobs
jobs
field is an array jobs to be executed sequentially.
The first argument of each job is the method. The DB operations get
set
update
upsert
add
delete
batch
are supported.
Except for get
the second argument is the query.
["set", [{name: "Bob", age: 20}, "people", "Bob"]]
In case of get
, the second argument is a global variable to set the return value to, and the third argument is the query.
["get", "Bob", ["people", "Bob"]]
Now you can get Bob
with {var: "Bob"}
or ["var", "Bob"]
.
let
will assign the result of the job to a global variable.
["let", "age", ["compose", ["add", 1], ["prop", "age"], {var: "Bob"}]]
do
will simply execute a job without assigning the result to a variable. It's useful when you want to define variables for later use.
["do", ["compose",["map", ["apply", ["let"]], ["toPairs"]], {var: "Bob"}]]
For example, the job above will first fetch a global variable Bob
with {var: "Bob"}
, {name: "Bob", age: 21}
.
Then convert it to [["name", "Bob"], ["age", 21]]
with toPairs
.
Then map
the pairs and apply
each pair to let
, which results in defining two global variables name
and age
with respective values.
Conditional Statements
Feature added in contract version 0.28.14
if
creates a conditional statement. If the second item evaluates true
, it executes the third item.
["if", ["identity", true], ["update", [{age: 30}, "people", "Bob"]]]
Likewise, ifelse
will execute the fourth item in case the second item evaluates false
.
[
"ifelse",
["identity", true], // condition
["update", [{age: 30}, "people", "Bob"]], // executed if true
["update", [{age: 40}, "people", "Bob"]] // executed if false
]
break
will break out of the current cron execution. You can combine it with if
in practice.
["if", ["identity", true], ["break"]]
Get Crons
await db.getCrons()
Remove Crons
await db.removeCrons("count-crons")
Tick
Feature added in contract version 0.28.14
Cron executions are recorded to contract when someone executes a write query, which means if no one executes anything for a long time, cron jobs can pile up and slow down every read query. In such cases, you could execute tick()
query to record cron executions.
await db.tick()
JSON-based Functional Programming
Since JS functions cannot be stored as SmartWeave states, we have invented a little new programming lanuguage called FPJSON (opens in a new tab) to allow writing functional instructions in the JSON format.
You should familiarize yourself with Ramda (opens in a new tab) which enables Haskell-like functional programming with JS. You can use all the powerful ramda functions with point-free style (opens in a new tab) in JSON.
Simple Examples
Basic
add(1, 2) // ramdajs
["add", 1, 2] // JSON
Currying
add(1)(2) // ramdajs
[["add", 1], 2] // JSON
Composition
pipe(add(10), subtract(__, 3))(5) // ramdajs
[["pipe", ["add", 10], ["subtract", ["__"], 3]], 5] // JSON
Point-free style means you cannot write something like this with the JSON format.
sortBy((v)=> v.age)(people) // ramdajs
It's because you cannot write arbitrary JS lines such as (v)=> v.age
.
Instead, you can do this using another ramda funciton prop
.
sortBy(prop("age"), people) // ramdajs
["sortBy",["prop", "age"], people] // JSON
Global Variables
Pure functional programming without any side-effects is easy to get extremely complex and entangled even for simple logics.
WeaveDB inserts global variables to each cron scope to ease up the unnecessary complexities.
Every time a cron executes, the global variables will start with block
metadata.
{
block: {
height: SmartWeave.block.height,
timestamp: SmartWeave.block.timestamp,
}
}
Access Variables
To access a variable, there are two ways.
Object with var
field
{var: "block"} // will return the block object
{var: "block.timestamp"} // will only return the timestamp
In action
["add", { var: "block.timestamp" }, 60 * 60] // add 1 hour to the timestamp
Custom ramda function
["var", "block.height"]
The var
function needs to be invoked with another argument. The second argument can be whatever and ignored.
So for example, within a composition, you can rewrite the ongoing value with a new value with var
function and keep on.
["pipe", ...[DOING_SOMETHING], ["var", "block.height"], ...[NOW_KEEP_ON_WITH_BLOCK_HEIGHT]]
The difference between {var: "block"}
and ["var", "block"]
is {var: "block"}
will be evaluated and replaced before each job starts, whereas ["var", "block"]
will be evaluated dynamically during the job executes.
Define Variables
To define new global variables or assign a value to existing variables, use let
custom ramda function.
["let", "variable_path", "value"]
In action
["pipe", ["add", 60 * 60], ["let", "hour_later"], {var: "block.timestamp"}]
After this the global variables will be...
{
block: {
height: SmartWeave.block.height,
timestamp: SmartWeave.block.timestamp,
},
hour_later: SmartWeave.block.timestamp + 60 * 60
}
Note the difference between var object
and var function
when accessing the variable.
You can do this,
["pipe", ["add", 60 * 60], ["let", "hour_later"], ["var", "hour_later"], {var: "block.timestamp"}]
but you cannot do this since {var: "hour_later"}
will be evaluated before the job starts.
["pipe", ["add", 60 * 60], ["let", "hour_later"], {var: "hour_later"}, {var: "block.timestamp"}]
Dynamic Path
When accessing and defining variables, you can use dynamic paths with $
.
For example, assign a path to a variable.
["let", "dynamic_path", "hour_later"]
Then use the variable with $
, to achieve the same result as the previous examples.
["pipe", ["add", 60 * 60], ["let", "$dynamic_path"], ["var", "$dynamic_path"], {var: "block.timestamp"}]
Complex Examples
The Bookmarking demo demonstrates complex logics of using crons.