Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

gobj-ui: assets

Assets

Source code: src/yui_asset.js

A treedb node often owns something that is not JSON: a photo, a plan, a clip. Those bytes cannot live in the treedb’s records, so the treedb keeps them on disk under its own directory and the node names one with an fkey into the system topic __assets__ — a column flagged ['fkey', 'file'], see File columns. The SDK’s C_ASSETS is the way out: it publishes the bytes to a browser.

get-asset answers in one of two shapes, and the backend decides which:

{"mode": "url",    "url": "/media/ab/cd/<id>.jpg?e=<expires>&s=<token>"}
{"mode": "inline", "content_type": "image/jpeg", "content64": "..."}

It signs a URL when a web server sits in front of the store and hands over the bytes when there is none, so a consumer has one code path and a node with no web server still shows its images instead of showing nothing.

These helpers do not talk to the backend. Asking is an action and belongs in the view’s own state machine; they are the two ends of it — read the id out of the link before asking, and turn the answer into an element afterwards.


A column that holds a link comes back in one of three shapes, and which one is the reader’s choice, not the schema’s: the stored "__assets__^<id>^as_devices_foto", the bare "<id>" that fkey_only_id collapses it to, or an expanded {id}. Each can come alone or in a list — and an unset single-valued fkey is still an empty list. All of them are read.

yui_asset_ids(ref)

Every asset id the column names, in order. Always an array, possibly empty.

yui_asset_id(ref)

The first id the column names, or null. An empty column answers nothing rather than throwing.


Showing it

yui_asset_src(answer)

Turns either shape of a get-asset answer into something an element can load: the signed URL as it comes, or a data: URL built from the inline bytes.

Answers nullnever an empty string — when the answer carries neither. An <img src=""> reloads the page in some browsers, which is a worse failure than the one being reported.

yui_asset_element(answer, opts)

The element for one asset, built from the content type the backend stored and not from the name a person typed: <img>, <video> or <audio>. Video and audio are assets too, and an <img> whose src is a film shows the broken box this exists to remove.

It wires onerror, so a dead element is replaced by the marker below whatever the reason — an expired signature, a blob gone from the store, an unsupported codec.

opts.detail is what the marker shows: pass the original name or the source path, the thing a person can act on. opts.alt sets the alt text of an image, opts.key overrides the i18n key of the marker, and opts.class is added to the element.

yui_asset_missing(detail, opts)

The marker for an asset that is not there, on its own.

A missing image used to leave a broken box and no word about it, which is indistinguishable from a slow one and from a bug — 47 such holes in one day on one deployment before anybody noticed. The marker takes the space the image would have taken, so a card does not jump, and it says which asset is missing.

The label carries its i18n key (asset not available), so it follows a language change; detail is DATA and is never translated. Import src/yui_asset.css for it to have a shape.


Example

import {yui_asset_id, yui_asset_element} from "@yuneta/gobj-ui/src/yui_asset.js";
import "@yuneta/gobj-ui/src/yui_asset.css";

//  in the view's state machine, not in a DOM callback:
const id = yui_asset_id(device.foto);
if(id) {
    gobj_send_event(gobj, "EV_ASK_ASSET", {asset_id: id, slot: "foto"}, gobj);
}

//  ...and when the answer arrives, in the action:
$box.appendChild(yui_asset_element(answer, {detail: device.foto_name}));

Filling one: the file column control

Reading an asset is the half above. Writing one is a column flagged ['fkey','file'] and the control the form draws for it (src/yui_file_field.js). The form and the treedb topic table use it themselves — a consumer needs these only to build a file UI of its own.

Three things about the shape are not obvious, and each one is a bug it avoids:

YUI_FILE_ACCEPT

What the treedb’s default ceiling holds, as an accept attribute: a hint to the file dialog and never a check. The check is treedb’s, on the bytes, at the door — a browser filter is a convenience, and a client that means to lie walks past it.

yui_file_control(gobj, {name, value, readonly, accept, on_pick})

The control: a button, what it is holding, and the way to clear it. It hangs its state on the returned element — yui_file (the picked File, or null), yui_file_value (the id the column keeps) and yui_file_render(), because writing a property fires nothing and the host has to be able to say “now draw what you are holding”.

The <input type="file"> is hidden and driven by the button: a bare file input cannot be styled and says “No file chosen” in the browser’s language, not the app’s.

yui_file_read(file)

Reads one picked file into what the manifest carries — {content64, content_type, original_name, size, id}. Async: this is the promise the host awaits.

yui_files_manifest(picks, record)

Turns what was read into the write: the record takes the id in each picked column, and the bytes go beside it in __files__. That key is not a column — it is an instruction to the treedb write path, consumed and dropped at the door. A field that carries the bytes and a field that keeps them are one word apart and 460 MB of RAM apart.

yui_file_sha256(buffer)

The sha256 as lowercase hex, or null — and null is a legal answer, not a failure: crypto.subtle exists only in a secure context, so a dev server on plain http has none. Then the column goes empty and treedb fills the id from what arrives. What the hash buys is an integrity check the backend can make (a wrong id with good bytes is refused), never the identity itself.

yui_array_buffer_to_base64(buffer)

Base64 in chunks, because String.fromCharCode(...bytes) spreads the whole array onto the call stack and a few hundred KB is already a RangeError — which, for a file picker, is every file that matters.

yui_file_size_label(bytes)

A size a person reads: bytes up to 1 KB, then one decimal.

yui_file_id_label(id)

An id a person can look at: a sha256 in full is 64 characters of noise in a form, so it is shortened, and the whole of it belongs in the title.