Carried mounts: another world's content, read-only, for this session
Mounting a closure
A closure is a set of assets and everything they reference, published by its world. ZeroMind describes it with a closure manifest, and the sha-256 of the manifest's bytes (closureSha) is its address: the same closure always has the same address.
local m = carried.mount(closureSha, { key = "ada.avatar" }) -- holds it, starts filling it
local s = carried.wait(m.key) -- yields until it settles
assert(s.state == "ready", s.detail)
for _, entry in vfs.list(s.path .. "/chars") do print(entry.path) end
The mount stands at /zero/runtime/carried/<key>/, laid out the way the closure's world places its files (chars/ada.avatar/...). Every file reads with vfs.read (text and binary alike); nothing there can be written. The engine reads the manifest and each file through the device's content cache: a closure this device has read before mounts with no network request, and the log says how many files came from the cache.
key is one path segment (letters, digits, ., _, -); it defaults to closure-<first 16 hex digits>. A key holds one closure for as long as anyone uses it. Each carried.mount of the same closure at the same key holds it once more, and carried.release(key) lets one holder go: the last one takes the mount down and its bytes leave the engine's memory (carried.heldBytes() counts them), while the device cache keeps them. Every mount ends with the session it was made in.
Status
<key>/status (and carried.status(key)) states where a mount stands:
| field | meaning |
|---|---|
state | resolving (reading the manifest), fetching (reading the files), measuring, ready, refused, or none |
closureSha, commit | the closure, and the commit it was published at when the caller named one |
bytesFetched, bytesTotal | the bytes read so far, of the files the manifest names |
measured | once ready: files, folders, bytes, and kinds (files and bytes per kind) |
reason, detail | why nothing is mounted (unreachable: the content store answered nothing; notFound: no manifest is published at that address; incomplete: a file it names is not in the store) or why the closure was refused (notAClosure, pathOutside, corrupt), and the sentence saying so |
draft, users, builtin | see below |
A mount that ended none is tried again by the next carried.mount of it.
References inside the closure
A reference written inside the closure resolves inside it. A carried material draws with the texture the mount holds, whether it names it by guid, by path or by identity (props.lamp_albedo, the form MaterialAuthor.fromTexture writes), and whatever the open world holds under the same guid or identity: the open world's own content answers no reference a closure makes. carried.resolve(key, reference) answers any reference the way the closure's own content would: a guid it carries, a path of its world (props/lamp.model or /zero/source/props/lamp.model), an identity of its world (props.lamp), and otherwise the engine library asset it names. The asset API does the same for a reference read at a file of a mount: asset.tryResolve(reference, type, site) with site a path inside the mount (or its @carried.<key>:: identity), a ~.tail expanding against the package or root asset enclosing that file in the closure's world.
An asset of a mount is known in this engine by a session guid: asset.resolve("@carried.<key>::props.lamp", "model").guid, asset.guid and asset.meta(...).guid all answer it, and its handle (:handle()) is read, put on the GPU and bound under it. Every mount of one closure gives an asset the same session guid, and another closure (another version of the same asset) gives it another, so the carried copy and the open world's copy of one asset draw side by side, each from its own bytes. The .meta sidecar in the mount still states the guid the closure gives the asset, and asset.resolve(sessionGuid) answers the carried asset again.
The engine library a closure was built against is part of its manifest, one entry per builtin at the version (checksum) it was built against. Each resolves, as builtin in the status states:
host: this engine's library holds that version; references reach the library.mount: this engine holds another version, so that version is read into the mount, underdeps/@builtin/versions/<checksum>/. The open world's library and/zero/source/depsare never written.hostOtherVersion: no published library holds that version; references reach this engine's copy, andwhysays so.
A reference that arrives before its mount
A component can name carried content before this engine holds it: a peer delivers a visitor's Asset { source = ... } while the visitor's closure is still being fetched here, or a scene names @carried.<key>::... before the mount at that key is made. The component holds that reference and waits for it:
- A component whose asset field names a reference no asset answers is attached and idle; its
awake()runs the moment the reference resolves, which for carried content is the moment its mount reachesready. Nothing the owner sends again is needed. - A delivered write to a component already running keeps the value the component holds, and lands (reaching its
onPropertyChanged) once the reference resolves. - A reference naming its mount (
@carried.<key>::..., or a path under/zero/runtime/carried/<key>/) whose mount settles without the content (refused,none, orreadyholding nothing the reference names) standsrefused, with the mount's reason; the component keeps showing what it showed before. A later mount of that key that brings the content applies it. A reference by session guid names no key, so it standspendinguntil a mount holding its content is ready.
asset.pending() lists every reference waiting on this engine ({ count, entries }, each with its entity, component, field, reference, state and reason), and entity(id).component.pending() the ones on one entity, so a test or an agent waits on the list emptying rather than on a frame count. A reference names carried content the same way on every peer: by its session guid (asset.resolve(...).guid of the carried asset), which every mount of one closure gives it whatever the key, or by @carried.<key>::<identity> when the peers mount it at the same key.
What the open world never sees
asset.list, search,entity.findand the asset index list nothing of a mount.- Code in the open world cannot
requirea module from a mount, by path or by guid; the require fails naming the mount. - A push,
zm statusand the world's manifest never see a mount.
Drafts: previewing your own content through the same route
local d = carried.mountDraft("/zero/source/props/lamp.model")
carried.wait(d.key)
carried.mountDraft(assetPath) builds the closure of an asset in the open world's working tree (or your home folder): the asset, every asset its files reference, and the builtins they were built against. It writes that closure and its manifest into the device's content cache and mounts it from there exactly as a published closure mounts, labelled draft = true. It is this engine's alone. Mounting the same draft again rebuilds it from the tree as it stands.
A draft of content in your home folder (/zero/home/<you>) is your home folder's, wherever it is mounted. It is made for the code that reaches the folder (the editor, the agent's tools, execute, and code in that folder), and the mount's files, its listing, its status and its manifest answer that code alone, the way the folder itself does; so does a mount of the same closure by its address. Code a world ships is refused a draft of a home folder's asset, or of an asset that reaches one, and the refusal names the folder.