Services and gates in Rust

A Rust function can use the same platform services, database guards, and gates as a JavaScript function.

Docs menu

Each call in substrio_wasm::services corresponds to a JavaScript call and runs the same platform code. Its result is the same, so a function can move between the two languages without changing that behavior.

Each call crosses into the platform once. A call returning JSON writes it into a Vec<u8> you retain, so a warm call allocates nothing. A result larger than the vector's capacity is returned whole. The service is not run a second time to fill the buffer.

The caller

Gates apply to the caller of the running call. In JavaScript you pass request.auth. Rust has no auth argument because the platform already has the caller's identity, grants, and verified flag. A gate applies to the person who made the request.

Calls

Rust JavaScript Result
route(method, url, authorization, out) substrio.route(method, url, authorization) The app route gate result as JSON: {params, route, auth} on success, or {status, body} on rejection. Returns false when the call has no route gate.
call(name, args_json, out) substrio.functions.<name>(...args) The body of the other function's response. A Rust callee runs nested on this thread and receives args_json as the body of its call entry. A JavaScript callee runs through the platform function runtime and receives the arguments as its default export's argument. Either way the callee shares the caller's CPU budget.
queue(name) substrio.queue(name) A queue the live version declares.
Queue::send(body), send_item(&Item) queue.send(body, {delay, id}) A Sent: the message's seq, and duplicate() when its id was sent before.
Queue::send_batch(&[Item], out) queue.sendBatch(items) A Sent for each item, in order.
collections(out) substrio.collections() The app's collections as JSON.
collection_pipeline(name, stages_json, out) substrio.collection(name).pipeline(stages) The pipeline result as JSON.
collection_stats(name, out) substrio.collection(name).stats() The collection's statistics as JSON.
collection_drop(name, out) substrio.collection(name).drop() The drop result as JSON. The records are removed.
smtp(&Smtp) substrio.mail.smtp(options) An SmtpReply with ok, code and message. The message is sent before the call returns.
limit(key, max, window_ms, LimitOptions) db.limit(key, max, windowMs, options) A Limit with allowed, remaining and reset_at.
limit_peek(...), limit_refund(...) db.limit.peek(...), db.limit.refund(...) The same result. A peek consumes no allowance, and a refund returns the cost.
once(key, keep_ms, f) db.once(key, {keep}, fn) A Once with the reply's JSON text and replayed.
permission(name) db.permission(name) The declared permission's bit, from 0 to 63.
require_permission(bit, organization) db.requirePermission(permission, auth, organization) Ok when the caller holds the permission everywhere or in the organization.
holds_permission(bit, organization), holds_permissions(mask, organization) db.holdsPermission(permission, auth, organization) true when the caller holds the permission, or every bit of the mask.
held_permissions(organization) db.heldPermissions(auth, organization) Every permission the caller holds there, as one mask.
require_person_or_permission(person, bit, organization) db.requirePersonOrPermission(person, permission, auth, organization) Ok when person is the caller, or when the caller holds the permission.
require_verified() db.requireVerified(auth) Ok for a caller with a verified address, or for an app key.
user_id_of(&row) db.userIdOf(person) The user id of a row of _users.
user_ids_of(people, ids, out) db.userIdsOf(rows) The user ids of many _users rows, in order.
secret(collection, id, field, out) db.secret(collection, id, field) The plain value of a secret field. false when the document holds no value there.
unique_space(name, members) db.uniqueSpace(name, members) Ok once the members share one namespace of values.
drop_unique_space(name) db.dropUniqueSpace(name) true when the space was declared and is now dropped.

The caller's grants are evaluated first. When the caller has the requested permission everywhere, the gate passes without a lookup. Otherwise, the caller's membership in the organization is read once. held_permissions returns the caller's grants together with that membership, or every bit when the caller has the catalog's all.

collections and a collection's pipeline, stats and drop go through the platform request for that operation. They are rare calls that set up or inspect a collection, and no call on a hot path reaches them. The operation is known in advance, so the platform parses no method and matches no path.

Write a search or a batch of writes as a Rust chain. Chains run directly against the collection, and a search returns rows. The calls are described under Chains.

call runs the other function on the same thread, and that function uses what is left of this call's CPU and wall time. When it runs out of either, this call ends with it, as a chain of JavaScript calls does.

smtp runs the transfer to its end inside the call, because a Rust call has no promise to settle later. The transfer goes through the same outbound fence as JavaScript's and counts against the same cf_fetch_limit.

once stores the reply's JSON text as supplied. When f returns an error, the claim is released and the error is returned, so a retry runs f again. When f panics, the call ends and the platform releases the claim.

Every secret read is written to the audit trail before the value is returned. The value is cleared from the platform buffer after it reaches yours.

Refusals

A failed call returns an Error whose code matches the platform code in the JavaScript error. db::last_error_message() returns its message.

Code Name When
0x0102 INVALID_ARGUMENT An empty key, a window or keep outside 1 to 2^53 - 1, a cost of 0, a permission bit past 63, an undeclared permission name, a queue name that is not 1 to 100 letters, digits, ., _ or -, or a unique space with no member or an unreadable name.
0x1003 PERMISSION_DENIED The caller does not hold the permission. The message names it.
0xA008 CREDENTIAL_REQUIRED A gate was asked and nobody is signed in.
0xA012 EMAIL_VERIFICATION_REQUIRED require_verified for a person whose address is not verified.
0xF009 UNSUPPORTED_OPERATION A service that writes or runs another function was called inside db::transaction. This covers a collection's admin calls, call, a queue send, a limit take or refund, and once.
0x8001 TYPE_MISMATCH user_id_of or user_ids_of was given a row that is not a person. It also covers a key that holds no limiter or once record, and a stored reply with no JSON form.
0x400B INVALID_ID secret was given text that is not a document id.
0x4000 KEY_NOT_FOUND secret named no document.
0x8006 SECRET_FIELD secret named a field that is not a secret field. A unique space member that is a secret field is refused with it too.
0x4009 UNIQUE A unique space was declared over a value that is already held in two places. The message names it.
0x1000 STORAGE_IO A queue message could not be stored.
0x3005 TIMEOUT A unique space was declared while another call held a transaction open past this call's deadline.
0xC006 FUNCTION_NOT_FOUND call named no function of the app.
0xC595 CALL_DEPTH_EXCEEDED call would nest deeper than jsc.limit_call_depth allows. The message names the limit.
0xC596 FUNCTION_NO_PLAIN_ENTRY A JavaScript function called substrio.functions.<name> on a function that has no default export.
0x900B CF_EXECUTION_FAILED The function call ran trapped or threw. The message is the callee's own.
0xC597 QUEUE_NOT_DECLARED The live version declares no such queue, or it dropped the queue since queue answered.
0xC598 ONCE_IN_PROGRESS Another call of the same once key is running now.
0xC599 ONCE_INDETERMINATE A run of the key was cut short by a restart. What it wrote is unknown, so it does not run again until its keep time passes.
0xC59A ONCE_CLAIM_LOST f ran longer than its keep time and the key was taken again.
0xC036 NO_CALL_CONTEXT The call has no database.

A server's refusal of an SMTP message is a reply with ok false, never an error.

Example

A route that lets a person edit an event when they host it or hold EDIT_EVENTS in its organization, at most 10 times a minute, and queues a notice of the edit:

#![no_std]
extern crate alloc;

use alloc::format;
use alloc::string::ToString;
use substrio_wasm::{db, services, Ctx, Env, Request, Response};

fn fetch(req: &Request, _env: &Env, _ctx: &Ctx) -> Response {
    let run = || -> Result<Response, db::Error> {
        let edit = services::permission("EDIT_EVENTS")?;
        let events = db::collection("events")?;
        let id = req.path().trim_start_matches("/events/");
        let (host, org) = match events.find_by_id(id, |r| {
            let host = r.get("host_user").and_then(|v| v.as_i64()).unwrap_or(0) as u64;
            let org = r.get("org").and_then(|v| v.as_str()).unwrap_or("").to_string();
            (host, org)
        })? {
            Some(found) => found,
            None => return Ok(Response::text(404, "no such event")),
        };
        services::require_person_or_permission(host, edit, &org)?;
        let hit = services::limit(id, 10, 60_000, services::LimitOptions::default())?;
        if !hit.allowed {
            return Ok(Response::text(429, "too many edits"));
        }
        events.set(id, req.text().unwrap_or("{}"))?;
        services::queue("notices")?.send(&format!("{{\"event\":\"{}\"}}", id))?;
        Ok(Response::text(200, "saved"))
    };
    run().unwrap_or_else(|e| Response::json(500, format!("{{\"code\":{}}}", e.code)))
}

substrio_wasm::export!(fetch);

See also Rust functions, the entry page: when to pick Rust, every call exposed to it, deploy and testing.