Guides

Objects and identity

A method can return an object and take one. The platform gets a class, the same wrapper every time it asks for the same object.

Returns and parameters#

A method returns an object the way Rust already shares one, behind an Arc, and takes one by reference. Undra generates a class for it in each language, so you never see a handle.

In RustOn the platform
Arc<Shelf>, Option<Arc<Shelf>>, Vec<Arc<Shelf>> returned, also inside a Result or an async methoda Shelf, an optional one, a list of them
&Shelf, Arc<Shelf>, Option<&Shelf>, Vec<Arc<Shelf>> as a parametera Shelf you pass, an optional one, a list
a constructor returning Arc<Self>a constructor that gives the same object back while you hold it

A returned store is a store: observe it and its signals arrive like those of any other.

Show the code for Rust, Swift, Kotlin and TypeScript
rust
#[undra::api]
impl Workshop {
    /// The shelf called `name`, made on first use.
    pub fn shelf(&self, name: String) -> Arc<Shelf> { .. }
    pub fn find(&self, name: String) -> Option<Arc<Shelf>> { .. }
    pub fn shelves(&self) -> Vec<Arc<Shelf>> { .. }
    pub async fn open(&self, name: String) -> Result<Arc<Shelf>, WorkshopError> { .. }
    /// Takes stores as parameters.
    pub fn merge(&self, from: &Shelf, onto: &Shelf) { .. }
    pub fn total(&self, shelves: Vec<Arc<Shelf>>) -> u32 { .. }
}
swift
let inbox = try workshop.shelf(name: "inbox")        // a Shelf: observe it like any store
let again = try workshop.shelf(name: "inbox")
assert(inbox === again)                              // one object, one wrapper

let archive = try workshop.shelf(name: "archive")
workshop.merge(from: inbox, onto: archive)
let all: [Shelf] = try workshop.shelves()
let n = try workshop.total(shelves: all)
kotlin
val inbox = workshop.shelf("inbox")                  // a Shelf: observe it like any store
check(inbox === workshop.shelf("inbox"))             // one object, one wrapper

workshop.merge(from = inbox, onto = workshop.shelf("archive"))
val n = workshop.total(workshop.shelves())
ts
const inbox = await workshop.shelf("inbox");         // a Shelf: observe it like any store
console.assert(inbox === (await workshop.shelf("inbox")));   // one object, one wrapper

await workshop.merge(inbox, await workshop.shelf("archive"));
const n = await workshop.total(await workshop.shelves());

One object, one wrapper#

Ask for the same object twice and you get the same wrapper while the first is alive. A store returned twice is observed once, a list of shelves is a list you can compare, and in Swift the wrapper is Hashable by identity, so ForEach(shelves, id: \.self) works.

The reference you hold is given back when the wrapper is closed or collected, once. Hold the wrapper for as long as you use it; close() it to let go early.

What stays out#

An object travels as a parameter or a return, and nowhere else.

Not allowedWhy
An object inside a record, a variant or a store's signal, in a stream item, a query or a mutationa value is copied, compared, saved and merged; an object is none of those. Put the object behind a method (E0064 says so)
An object of another coretwo cores can issue the same handle numbers, so the generated code refuses it before anything is sent: UndraCallError.refused
An object in a port's parameter or returnpass a record or an id instead

After a restore#

An object a method returned is not part of a saved snapshot: after a restore (a web worker crash, undra dev reloading the core) its handle is stale, and calling the method again returns it. Constructed stores restore as before, and so do query handles: the snapshot keeps what a handle is made of and the core builds it again when the app first uses it (ADR-059). What a store keeps in a private field is whatever its restore hook rebuilds.

Good to know#

The playground's Workshop uses every shape on this page, and the contract scenario S27 checks them on Swift, Kotlin and TypeScript.

Edit this page on GitHub