Sobamail Runtime Reference¶
Imports¶
In Sobamail platform, all modules use hash values in import statements to guarantee that code is reproducible from the root module down to the last leaf in the import tree. The hash values are simply obtained by encoding Sha224 digest value of module source code using "urlsafe base64" encoding.
Here is a python script that generates the import hash for a given script file.
import sys
import hashlib
for fn in sys.argv[1:]:
digest = hashlib.sha224(io.open(fn, "rb").read()).digest()
digest_b64 = base64.urlsafe_b64encode(digest) \
.replace(b"=", b"") \
.decode("ascii")
print(f"${fn}: ?sha224={digest_b64}")
Special JS Primitives¶
ArrayBuffer (Tagged)¶
Sobamail runtime adds a special type of ArrayBuffer, which is returned by
soba.data.* family of functions. They are used as mutation handles instead of
literal binary blobs.
Execution Context¶
Runtime functions execute in either a replicated or a non-replicated
context, as described under Mutations. Functions that
are non-deterministic, and therefore cannot be called while processing a
replayable mutation, are flagged with the [NR] marker throughout this reference;
functions without the marker may be invoked in either context. The current
context may be determined at runtime with
soba.app.isReplicated().
API Reference¶
soba.app.account() [NR]¶
Returns the account associated with the current mailbox.
Returns:
String: The account name in email address format.
soba.app.instanceId()¶
Returns the instance id of the current application instance.
Returns:
String: The instance id.
soba.app.isReplicated()¶
Returns whether the object key at hand is replicated or not. Replicated objects
are the ones that appear in the insertEvent attributes or the DeleteRow
object.
Returns:
Boolean:trueif the object at hand is replicated,falseotherwise.
soba.app.mailboxUuid() [NR]¶
Returns the uuid of the current mailbox replica in braced hexadecimal uuid format.
Returns:
String: The replica uuid, in braced hexadecimal uuid format.
soba.app.objectKey()¶
Returns the triggering object key.
Returns:
String: The object key in curly braced namespace format.
soba.app.peers() [NR]¶
Returns the list of users that share this app instance.
Returns:
String[]: An array of email address strings.
soba.data.delete(table, hash)¶
Deletes data from the specified table and generates a DeleteRow mutation with
a happens-before relationship with the mutation in the insertEvent attribute.
Parameters:
table(String): The name of the table.hash(ArrayBuffer): The hash of the row entry to be deleted, as returned by thefind()orinsert()function. AnyArrayBufferis accepted; it need not be a tagged mutation handle.
soba.data.find(table, object)¶
Retrieves the mutation hash from the specified table based on the query. It's
used to have deterministic yet dynamic happens-before relationships with other
mutations. If no matching insert mutation has ever existed in the mutation log,
a fatal exception is thrown. A matching mutation that existed but was later
deleted is returned with its deleted flag set (see below).
Parameters:
table(String): The name of the table.object(Object): The query object used to search for the data. The object keys must be a subset of column names with LWW constaints.
Returns:
Returns an object with the following attributes:
object(Record): The mutation contents.hash(ArrayBuffer (Tagged)): The hash of the data that matches the query.deleted(bool):trueif the matching insert mutation exists but was subsequently deleted by another mutation;falseif the row is still live.
soba.data.findLast(table, object) [NR]¶
Retrieves the last mutation hash from the specified table based on the query. It's used to have non-deterministic happens-before relationships with other mutations. Since the result is not deterministic, this function throws in replicated context.
Parameters:
table(String): The name of the table.object(Object): The query object used to search for the data. The object keys must be a subset of column names with LWW constaints.
Returns:
Returns an object with the following attributes:
object(Record): The mutation contents.hash(ArrayBuffer (Tagged)): The hash of the data that matches the query.deleted(bool):trueif the matching insert mutation exists but was subsequently deleted by another mutation;falseif the row is still live.
soba.data.insert(table, data)¶
Inserts data into the specified table.
Parameters:
table(String): The name of the table.data(Object): The data to be inserted. Must be a mapping from column names to values of type (String,Number,ArrayBuffer) or valuenull.
Return Value:
Returns an object with the following key:
hash(ArrayBuffer (Tagged)): The hash value of the inserted mutation.
Note
The returned object also carries inserted and applied boolean fields.
They are reserved and currently carry no meaning -- do not rely on them.
soba.data.update(table, operation, extra)¶
Applies a relative update to a single column of an existing row. Rather than
replacing the whole row, the update describes an operation (op) to be applied
to one column of the row identified by the parent mutation handle. The
update establishes a happens-before relationship with that parent mutation, so
concurrent updates compose deterministically across replicas.
Only commutative operations are permitted (e.g. addition, multiplication), since concurrent updates from different replicas may be applied in any order and must still converge on the same value. Non-commutative operators (such as subtraction or division) are rejected.
Parameters:
table(String): The name of the table.operation(Object): The update to apply.parent(ArrayBuffer (Tagged)): The mutation handle of the row to update, as returned byfind(),findLast(), orinsert().column(String): The name of the column to update.op(String): The operator to apply, e.g."+"to addvalueto the current column value.value(Any): The operand forop.extra(Object): Additional fields merged into the generated mutation but not written to the table. Typically used to carry a freshuuid(seesoba.type.uuid.v4()) so that otherwise-identical updates are not deduplicated by their hash.
Example:
soba.data.update("counter", {
parent : parent.hash,
column : "value",
op : "+",
value : content.value,
}, {
uuid : soba.type.uuid.v4(),
});
soba.schema.table(defn)¶
Creates a table in the local SQLite database with the specified options:
Must be called from the Mutator constructor. Throws otherwise.
Parameters:
defn(Object): The definition object for the table.name(String): The name of the table.insertEvent(Class): Mutation object generated on row insertiondeleteEvent(Class): Mutation object generated on row deletion. Must beDeleteRowobject from the Base.mjs module.-
columns(Array): An array of column definitions.name(String): The name of the column.checks(Array): An array of single column check objects.-
op(String): The check operation to be performed. One of:["==", "!=", "typeof", "in", "fk", "lww", "regexp"]These values mean the following:
-
==: The equals constraint: The incoming value must be a constant equal to the value stored in thevaluekey. This is enforced by a SQLiteCHECKconstraint, so consult the SQLite docs for exact semantics. -
!=: The not equals constraint: The incoming value must be a constant not equal to the value stored in thevaluekey. This is enforced by a SQLiteCHECKconstraint, so consult the SQLite docs for exact semantics. -
typeof: The type-of constraint: The type of the incoming value must equal to the value stored in thevaluekey. Accepted values for this constraint are:["blob", "integer", "null", "real", "text"]This is enforced by a SQLiteCHECKconstraint, using SQLite'stypeofoperator, so consult the SQLite docs for exact semantics. -
in: The in constraint: The incoming value must be equal to one of the values stored in the Array inside thevaluekey. This is enforced by a SQLiteCHECKconstraint, using SQLite'sINoperator, so consult the SQLite docs for exact semantics. -
fk: The foreign key constraint: The incoming value must be equal to one of the values stored in the given column of the given table. This is enforced by a SQLiteFOREIGN KEY ... ON DELETE CASCADEconstraint. This constraint establishes a happens-before relation between the insert mutation of the target table and the insert mutation at hand. This prevents execution of the mutation at hand by other replicas if its parent mutation is missing. -
lww: The last-write-wins constraint: The incoming value must be different from one of the values stored in the given column of the current table. This is enforced by a SQLiteUNIQUEconstraint. This constraint establishes a happens-before relation between theDeleteRowmutation of the insert mutation at hand, if it exists, according to the following rules:-
If the value is missing from the table:
-
If a
DeleteRowmutation can't be found in the mutation log for the same value, no causal relationship stems from the LWW constraint. -
If a
DeleteRowmutation is found in the mutation log for the same value, a happens-before relationship is established between the new insert mutation and the old delete mutation.
-
-
If the value is present on the table:
-
The digest value of the new insert mutation matches with the previous insert mutation, the current insert operation is ignored.
-
If the value already exists and the digest value of the new insert mutation does not match with the previous insert mutation, a
DeleteRowmutation is generated for the column value in previous insert mutation and the new insert mutation is generated to have a happens-before relationship with this newDeleteRowmutation.
-
-
-
regexp: The regular expression constraint: The incoming value must be a string and must match the given regular expression in thevaluekey. This is enforced by a CHECK constraint using SQLite'sREGEXPoperator, which in turn calls into the native regular expression engine of V8, so consult the SQLite, V8 and ECMAScript docs for exact semantics.
-
-
value(Any): The value to be used in the check. table(String): The name of the table to be used in foreign key constraintcolumn(String): The name of the column of the target table to be used in the foreign key constraint.
-
checks(Array): An array of multi-column check objects.op(string): The check operation to be performed. One of:["fk", "lww"]- Check-specific keys specified below:
op === "lww": For the Last-Write-Wins check:columns(string[]): The name of the columns to be used in the LWW checks.value(true): Must betrue
op === "fk": For the Foreign Key check:table(string): The name of the target table.columns({col:string}[]): A mapping from column names of the source table to the column names of the target table
The multi-column FK and LWW checks operate the same as their single-column variants. The column names and values are serialized to JSON and treated exactly like the single-column constraints.
soba.db.exec(query [, params ...]) [NR]¶
Always exposes a read-only SQLite connection.
Throws in replicated context.
Parameters:
query: (String): The SQL query to run. Can denote parameters only using the '?' notation.params: (null|true|false|Number|String|ArrayBuffer): Zero or more parameters passed to the SQL query.
Return Value:
Returns an object with the following keys:
-
code: Number: One of SQLite extended return codes. -
data: any[][]: Array of rows where rows are Array of JS values.
soba.task.emit(object)¶
Used to return a persistent response to the current event. Useful for triggering other apps.
A response is only meaningful for the originating, non-replicated event, so this
function is a no-op when called while replaying a replicated mutation. It is not
flagged [NR] -- calling it in replicated context is harmless rather than
an error -- but it has no effect there.
Parameters:
object: null|true|false|String|Number|ArrayBuffer|Array|Object: A Javascript object that can be serialized as msgpack.
Returns:
Nothing.
soba.task.respond(object)¶
Used to return an ephemeral response to the current event. Useful for responding to application GUI requests.
As with soba.task.emit(), a response only makes sense
for the originating, non-replicated event; this function is a no-op when called
while replaying a replicated mutation. It is not flagged [NR] -- calling
it in replicated context is harmless rather than an error -- but it has no effect
there.
Parameters:
object: null|true|false|String|Number|ArrayBuffer|Array|Object: A Javascript object that can be serialized as msgpack -- ie only consisting of the listed primitives.
Returns:
Nothing.
soba.mail.send(message) [NR]¶
Sends an email.
Throws in replicated context.
Parameters:
message: (Message): AMessageinstance.
Return Value:
Nothing.
soba.log.{debug,info,warning,error}(message)¶
Logs a message with the specified level.
Parameters:
message(String): The message to be logged.
soba.pack.msgpack(object)¶
Serializes a JavaScript value into its msgpack representation. The encoding is deterministic, which makes it suitable for building stable digests and mutation payloads: the same value built the same way always produces the same bytes, on every replica.
That determinism follows from JavaScript's specified property iteration order
(integer-index keys first, in ascending order, then string keys in insertion
order), combined with a single fixed encoder. Note the consequence: object
identity here is insertion-order based, not sort based. {a: 1, b: 2} and
{b: 2, a: 1} encode to different bytes -- and therefore hash differently and do
not dedup -- even though they are logically equal.
Numeric-string keys ("0", "2", …) also sort ahead of non-numeric keys
regardless of insertion order. Build the object the same way each time and this
is a non-issue.
Parameters:
object: null|true|false|String|Number|ArrayBuffer|Array|Object: A Javascript value consisting only of the listed primitives.
Return Value:
ArrayBuffer: The msgpack-encoded bytes, as a raw (untagged)ArrayBuffer-- not anArrayBuffer (Tagged)mutation handle.
soba.text.encode(string)¶
Encodes a JavaScript string into its UTF-8 byte representation.
Parameters:
string(String): The string to encode.
Return Value:
ArrayBuffer: The UTF-8 encoded bytes.
soba.text.decode(buffer)¶
Decodes UTF-8 bytes into a JavaScript string.
Parameters:
buffer(ArrayBuffer): The UTF-8 encoded bytes to decode.
Return Value:
String: The decoded string.
soba.binary.equals(a, b)¶
Compares two binary buffers for byte-wise equality.
Parameters:
a(ArrayBuffer): The first buffer.b(ArrayBuffer): The second buffer.
Return Value:
Boolean:trueif both buffers have the same length and identical contents,falseotherwise.
soba.binary.b64Encode(buffer)¶
Encodes a binary buffer into a standard base64 string (RFC 4648 §4, the
+// alphabet with = padding -- not the url-safe variant).
Parameters:
buffer(ArrayBuffer): The bytes to encode.
Return Value:
String: The standard base64-encoded representation of the input.
soba.binary.b64Decode(string)¶
Decodes a standard base64 string (RFC 4648 §4, the +// alphabet with =
padding -- not the url-safe variant) back into a binary buffer.
Parameters:
string(String): The standard base64-encoded string to decode.
Return Value:
ArrayBuffer: The decoded bytes.
soba.blob.create(data)¶
Creates a blob from the given binary data and returns a Blob object. The
blob's id is derived deterministically from the contents, so creating a blob with
identical data yields the same blob id.
Parameters:
data(ArrayBuffer): The binary contents of the blob.
Return Value:
Blob: A{https://sobamail.com/module/base/v1}Blobobject, whose id matchessoba.type.blobId.pattern.
TODO
There is currently no runtime function to read a blob's contents back by id. A blob-fetch API is on the roadmap.
soba.clock.logical.id() [NR]¶
Returns the identifier of the current logical clock.
Returns:
String: The logical clock id.
soba.clock.logical.digest() [NR]¶
Returns a digest of the current state of the logical clock -- that is, a summary of every mutation this replica has observed so far (its causal frontier).
The digest is returned as a tagged mutation handle so that it can be used like any other handle to establish a happens-before relationship. Using it as a dependency makes the new mutation causally depend on the entire set of mutations known to this replica at the time of the call.
Warning
This is a coarse, powerful dependency. Because it pins the new mutation
behind everything this replica had seen, other replicas cannot apply it until
they too have observed that whole frontier. Used carelessly it can stall
convergence on replicas that are behind. Prefer a specific handle from
find() when a narrower dependency suffices.
Returns:
ArrayBuffer (Tagged): The logical clock digest, as a tagged mutation handle.
soba.clock.physical.secs() [NR]¶
Returns the current physical wall-clock time in seconds since the Unix epoch. Wall-clock time differs between replicas, so this function is forbidden in replicated context.
Returns:
Number: Seconds since the Unix epoch.
soba.clock.physical.msecs() [NR]¶
Returns the current physical wall-clock time in milliseconds since the Unix epoch. Wall-clock time differs between replicas, so this function is forbidden in replicated context.
Returns:
Number: Milliseconds since the Unix epoch.
soba.clock.physical.usecs() [NR]¶
Returns the current physical wall-clock time in microseconds since the Unix epoch. Wall-clock time differs between replicas, so this function is forbidden in replicated context.
Returns:
Number: Microseconds since the Unix epoch.
soba.platform.isTest()¶
Returns whether the current Sobamail platform implementation is running in test mode.
Return Value:
Boolean:trueif running under a test harness,falseotherwise.
soba.platform.releaseChannel()¶
Returns the release channel of the current Sobamail platform implementation.
Return Value:
Returns "Stable" in the real world.
soba.platform.version() [NR]¶
Returns the version of the current Sobamail platform implementation.
Throws in replicated context.
Return Value:
Returns an array of integers that represent the version of the current Sobamail platform implementation.
soba.type.*.isValid(s)¶
The isValid helpers check whether a given string is a valid value of the
corresponding type. Each is a pure, deterministic predicate.
soba.type.blobId.isValid(s)— valid blob id.soba.type.userName.isValid(s)— valid user name.soba.type.domainName.isValid(s)— valid DNS domain name.soba.type.folderName.isValid(s)— valid folder name.
Parameters:
s(String): The string to be checked.
Returns:
Boolean:trueif the string is a valid value for that type,falseotherwise.
TODO
isValid helpers for the uuid, address, and namespace types are not
yet implemented, even though their patterns are exposed.
Validate against the corresponding .pattern in the meantime.
soba.type.uuid.v4() [NR]¶
Generates a UUIDv4.
Throws in replicated context.
Returns:
String: A new uuid, in braced hexadecimal uuid format.
soba.type.*.pattern¶
The pattern properties expose the regular expression source string used to
validate each type. They are constants, useful for client-side validation or for
declaring regexp column checks in soba.schema.table().
| Property | Validates |
|---|---|
soba.type.uuid.pattern |
A braced hexadecimal uuid. |
soba.type.address.pattern |
An email address. |
soba.type.blobId.pattern |
A blob id. |
soba.type.domainName.pattern |
A DNS domain name. |
soba.type.userName.pattern |
A user name. |
soba.type.folderName.pattern |
A folder name. |
soba.type.namespace.pattern |
A curly braced namespace. |
Value:
String: The regular expression source for the corresponding type.