Skip to content

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: true if the object at hand is replicated, false otherwise.

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:

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 the find() or insert() function. Any ArrayBuffer is 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): true if the matching insert mutation exists but was subsequently deleted by another mutation; false if 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): true if the matching insert mutation exists but was subsequently deleted by another mutation; false if 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 value null.

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 by find(), findLast(), or insert().
  • column (String): The name of the column to update.
  • op (String): The operator to apply, e.g. "+" to add value to the current column value.
  • value (Any): The operand for op.
  • extra (Object): Additional fields merged into the generated mutation but not written to the table. Typically used to carry a fresh uuid (see soba.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 insertion
  • deleteEvent (Class): Mutation object generated on row deletion. Must be DeleteRow object 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 the value key. This is enforced by a SQLite CHECK constraint, 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 the value key. This is enforced by a SQLite CHECK constraint, 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 the value key. Accepted values for this constraint are: ["blob", "integer", "null", "real", "text"] This is enforced by a SQLite CHECK constraint, using SQLite's typeof operator, 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 the value key. This is enforced by a SQLite CHECK constraint, using SQLite's IN operator, 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 SQLite FOREIGN KEY ... ON DELETE CASCADE constraint. 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 SQLite UNIQUE constraint. This constraint establishes a happens-before relation between the DeleteRow mutation of the insert mutation at hand, if it exists, according to the following rules:

        • If the value is missing from the table:

          • If a DeleteRow mutation can't be found in the mutation log for the same value, no causal relationship stems from the LWW constraint.

          • If a DeleteRow mutation 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 DeleteRow mutation 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 new DeleteRow mutation.

      • regexp: The regular expression constraint: The incoming value must be a string and must match the given regular expression in the value key. This is enforced by a CHECK constraint using SQLite's REGEXP operator, 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 constraint
    • column (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 be true
    • 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:

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): A Message instance.

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 an ArrayBuffer (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: true if both buffers have the same length and identical contents, false otherwise.

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:

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: true if running under a test harness, false otherwise.

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: true if the string is a valid value for that type, false otherwise.

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.