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.