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 to block.timestamp
  • end - timestamp the cron should end at, if omitted, it will run indefinitely
  • span - seconds between each execution, be careful not to set it too short like 1 second outside the test purposes
  • do - whether the task should be executed at start, it defaults to false if omitted, and in that case, the first execution time will be start + span
  • times - how many times the task should be executed, it will run forever if omitted
  • jobs - 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.

Bookmarking Tutorial