fleetlessfleetlessdocs
Reference/SDK/Assets & URDF

SDK · Assets & URDF

The robot’s URDF and meshes, authenticated loading for three.js, and the completeness report of what the robot could and could not provide.

Generated from the SDK’s own source. The SDK Reference is the narrative; this page is every member, in the order the SDK declares them.

AssetsApi

A robot’s synced files, reachable as client.assets: its URDF, the meshes and textures that URDF references, and the glue a three.js renderer needs to fetch them with this client’s credentials.

list()

ts
list(robotId: string): Promise<{ assets: ({ id: string; robot_id: string; kind: "urdf" | "mesh" | "texture"; name: string; media_type: string; size_bytes: number; sha256: string; created_at: string })[]; active_sync: { sync_id: string; robot_id: string; state: "failed" | "succeeded" | "running"; done: number; total: number; failed: ({ reference: string; kind: "unresolvable" | "upload_failed" | "refused"; details?: { store_bytes: number; used_bytes: number; size_bytes: number } | null })[]; reason: string | null; stored: number | null; announced: number; started_at: string; updated_at: string } | null; urdf: { present: boolean; mesh_count: number; missing: ({ uri: string; element: "mesh" | "texture" })[] }; urdf_available: boolean | null; store: { bytes: number; used_bytes: number }; joint_state_slug: string | null }>

Every asset a robot has, plus whether its URDF is complete. urdf.missing names the package:// URIs the sync could not resolve — a count alone (“2 meshes missing”) sends a developer looking through a workspace by hand, the URIs are what they can act on.

Parameter Type Required Description
robotId string yes

Returns Promise<{ assets: ({ id: string; robot_id: string; kind: "urdf" | "mesh" | "texture"; name: string; media_type: string; size_bytes: number; sha256: string; created_at: string })[]; active_sync: { sync_id: string; robot_id: string; state: "failed" | "succeeded" | "running"; done: number; total: number; failed: ({ reference: string; kind: "unresolvable" | "upload_failed" | "refused"; details?: { store_bytes: number; used_bytes: number; size_bytes: number } | null })[]; reason: string | null; stored: number | null; announced: number; started_at: string; updated_at: string } | null; urdf: { present: boolean; mesh_count: number; missing: ({ uri: string; element: "mesh" | "texture" })[] }; urdf_available: boolean | null; store: { bytes: number; used_bytes: number }; joint_state_slug: string | null }>.

syncStatus()

ts
syncStatus(robotId: string, syncId: string): Promise<{ sync_id: string; robot_id: string; state: "failed" | "succeeded" | "running"; done: number; total: number; failed: ({ reference: string; kind: "unresolvable" | "upload_failed" | "refused"; details?: { store_bytes: number; used_bytes: number; size_bytes: number } | null })[]; reason: string | null; stored: number | null; announced: number; started_at: string; updated_at: string }>

The status of one sync by id — for reconnecting to a sync already in flight, not for starting one.

Starting a sync stays out of this SDK, on purpose. Assets are transferred only on a developer’s explicit request from the console — an Owner-tier action, done once. This method is a different thing: list()'s active_sync (or a busy refusal’s assetSyncBusyDetails) hands a caller a sync_id for a sync that is already running, and before this method existed there was no way for anything built on this SDK to do anything with that id except throw it away, even though the sync was readable server-side the whole time.

A page reload is the case this exists for: whatever held the sync_id in memory is gone, list() (or a fresh busy refusal) hands it back, and this is how a caller resumes watching the same sync instead of either losing the progress bar or being told to start a second one.

Parameter Type Required Description
robotId string yes
syncId string yes

Returns Promise<{ sync_id: string; robot_id: string; state: "failed" | "succeeded" | "running"; done: number; total: number; failed: ({ reference: string; kind: "unresolvable" | "upload_failed" | "refused"; details?: { store_bytes: number; used_bytes: number; size_bytes: number } | null })[]; reason: string | null; stored: number | null; announced: number; started_at: string; updated_at: string }>.

get()

ts
get(robotId: string, assetId: string): Promise<AssetBytes>

One asset’s bytes by id — a mesh, or any asset directly, addressed the same way createMeshLoader reaches one internally.

Parameter Type Required Description
robotId string yes
assetId string yes

Returns Promise<AssetBytes>.

References: AssetBytes

urdf()

ts
urdf(robotId: string): Promise<string>

The robot’s URDF, with every package:// mesh URI already rewritten to an absolute Fleetless asset URL — ready to hand straight to URDFLoader.parse(xml). Decoded as UTF-8 text rather than left as bytes because every consumer needs it as a string for exactly that call.

Parameter Type Required Description
robotId string yes

Returns Promise<string>.

createMeshLoader()

ts
createMeshLoader(robotId: string, delegate: MeshLoaderDelegate, options?: CreateMeshLoaderOptions): MeshLoaderDelegate

The mesh callback for urdf-loader: an <img> tag and the default three.js loaders cannot set an Authorization header, and the platform deliberately has no signed URLs and no token in the query string, so every app would otherwise write this glue itself, and each one differently.

Returns a function with loadMeshCb’s own signature — assign it directly:

ts
loader.loadMeshCb = client.assets.createMeshLoader(robotId, loader.defaultMeshLoader.bind(loader))

delegate does the actual format-specific parsing (STL/OBJ/DAE/GLTF — loader.defaultMeshLoader already knows how); this method’s own job is only what a plain loader cannot do: fetch path with the Authorization header, and hand the delegate something it can load without one. It does that by fetching the bytes itself, wrapping them in a Blob, and calling delegate with an object URL substituted for path — so the delegate never touches the network, and the object URL is revoked the moment delegate reports success or failure (or the timeout elapses), never left for the caller to remember.

Refuses, via onComplete(null, err), if the manager it is handed already has prepareUrdfScene’s URL modifier installed — the two read different URDF sources and combining them on one manager breaks one half or the other, never obviously (see prepareUrdfScene’s own doc comment). Checked at the point the mistake would actually manifest rather than left to a paragraph a developer might not read.

Parameter Type Required Description
robotId string yes
delegate MeshLoaderDelegate yes
options CreateMeshLoaderOptions no

Returns MeshLoaderDelegate.

References: MeshLoaderDelegate, CreateMeshLoaderOptions

prepareUrdfScene()

ts
prepareUrdfScene(robotId: string, manager: UrdfSceneManager, options?: PrepareUrdfSceneOptions): Promise<UrdfSceneResources>

Authenticated loading for everything three.js fetches to render a textured robot — not only meshes. Installs manager.setURLModifier so every load manager oversees resolves to a pre-fetched blob: URL: a top-level <mesh>, a <material>'s <texture>, and an image a .dae references internally via <init_from> — three cases, one mechanism, because the browser never fetches an asset directly. Every network read goes through this SDK with the bearer token first; a blob: URL is document-local and dies with the page, so nothing about who may read what changes.

ts
const manager = new THREE.LoadingManager()
const { urdfText, missing, dispose } = await client.assets.prepareUrdfScene(robotId, manager)
const loader = new URDFLoader(manager)
const robot = loader.parse(urdfText)
scene.add(robot)
// later, once the scene has finished loading (or on unmount):
dispose()

Do not also install createMeshLoader on the same manager. The two consume different URDF sources — this method fetches the URDF’s raw bytes, createMeshLoader is meant to pair with urdf()'s cloud-rewritten text. Not a double-fetch. What actually happens is asymmetric breakage, whichever URDF text the combination ends up parsing: paired with this method’s raw text, createMeshLoader receives urdf-loader’s resolvePath() output (/pkg/rel) rather than an absolute Fleetless URL and 404s every mesh, while textures still resolve; paired with urdf()'s rewritten text instead, meshes load and every texture 401s — createMeshLoader bypasses this method’s URL modifier entirely for meshes (that’s what loadMeshCb means), so there is no hook left for a texture. Either way a developer who combined them by accident would debug the wrong symptom, which is worse than the original (already wrong) warning being merely unhelpful.

Enforced, not only documented. createMeshLoader’s returned callback checks whether the manager it is handed already has this method’s URL modifier installed and fails loudly via onComplete(null, err) before ever touching the network, rather than relying on a developer having read this paragraph.

Why raw bytes, not urdf()'s rewritten text. Both urdf-loader’s default mesh loading and ColladaLoader compute the base path they use to resolve a .dae’s internal references (LoaderUtils.extractUrlBase) from the URL they were originally asked to load — before manager.resolveURL()/the URL modifier ever runs; the modifier only changes what bytes get fetched, never what further relative references resolve against. Feeding it urdf()'s already-rewritten .../assets/<uuid> text would compute a base of .../assets/, and textures/skin.png joined onto that would never match anything this method’s map knows about. Raw package:// text is what keeps the two in sync.

urdf-loader resolves package:// itself, before any of this runs — a second resolution stage this method has to account for. URDFLoader.parse()'s own resolvePath() rewrites package://pkg/rel using this.packages (default '') to /pkg/rel — a root-relative URL — and that is what reaches loadMeshCb/ColladaLoader/manager.resolveURL(), not the original string. So the modifier is registered under two keys per asset: the literal package:// name (asset.name, for a caller who sets loader.packages = (pkg) => \package://${pkg}`to reconstruct it, or a renderer that never went throughresolvePath()at all) and the root-relative formurdf-loader's own *default* packages: ‘’ produces (/pkg/rel) — covering the common case with zero required caller configuration. A .dae's internal <init_from>ref resolves the same way one level deeper:ColladaLoadercomputes its own working path from *its*urlargument (already/pkg/relby the time it gets there under the default), so the internal reference lands on/pkg/textures/skin.png— exactly the second key, derived the same way. A caller whoseloader.packages` does something else entirely (a custom map, not the default and not the reconstruction above) is outside what this method can predict — see the ownership rule below for what happens to that reference.

This method only claims what it owns — not every unmapped reference. manager is frequently the caller’s own scene-wide LoadingManager, shared for an HDRI, an environment map, a font atlas, a ground texture — none of which have anything to do with this robot. Refusing everything unmapped would silently empty every one of those the moment a caller shares their manager. So the rule is narrower: a package:// reference, or a root-relative path whose leading segment names a ROS package this robot’s assets (or missing) actually mention, is this method’s to resolve or refuse; an unmapped one falls back to the normalized form (below) and then to a shared, inert, page-local blob: URL — never the original string, so a hostile URDF naming an unsynced or off-namespace reference still cannot make three.js touch the network for it. Anything else — not in that namespace — is left completely alone, except an absolute http(s) URL, which is refused regardless of namespace: the one case this method cannot leave ambiguous, because a hostile URDF naming an attacker’s host directly (bypassing package:// entirely) is exactly what this rule exists to close, and three.js would otherwise fetch it for real, off-origin, the moment the direct and namespace checks both miss.

A .dae’s own internal reference gets a second-chance, normalized lookup, reproduced in a real browser. three.js builds the request for one by plain string concatenation — no ../. collapsing — while asset.name carries the normalized tail (@fleetless/contracts’ naming rule). So ../textures/skin.png or ./textures/skin.png, both ordinary exporter output, would otherwise miss the direct key even though the reference is entirely resolvable. Verified independently that the top-level <mesh>/<texture> case never needs this: asset.name there is the URDF’s own package:// URI verbatim and resolvePath() rewrites it by the same unnormalized concatenation on both sides, so the direct key already matches.

dispose() does not undo any of this. See its own doc comment on UrdfSceneResources.

Pre-fetch is unavoidable, not merely a choice: a URL modifier cannot be asynchronous, so every asset it might be asked for has to already be a blob: URL before URDFLoader.parse runs. Bounded by options.concurrency (default 6) and scoped to only kind: 'mesh' and kind: 'texture' assets — which is already “what the URDF references”, since a re-sync reconciles and assets the current URDF no longer references stop belonging to the robot. Not an unbounded fetch of everything the robot has ever had.

Parameter Type Required Description
robotId string yes
manager UrdfSceneManager yes
options PrepareUrdfSceneOptions no

Returns Promise<UrdfSceneResources>.

References: UrdfSceneManager, PrepareUrdfSceneOptions, UrdfSceneResources

AssetBytes

An asset’s bytes plus its declared media type — the shape assets.get answers with.

Property Type Required Description
body Uint8Array yes The asset’s raw bytes, exactly as stored.
mime string | null yes The declared media type, or null if the store did not record one.

MeshLoaderDelegate

The signature urdf-loader’s own loadMeshCb uses (verified against that library’s docs, not guessed): manager/material pass straight through from whatever called this, and onComplete is how a mesh loader reports success or failure — it never throws.

This SDK does not depend on urdf-loader or three.js — manager, material and the resolved object are all opaque here, exactly what makes createMeshLoader usable from any renderer that speaks this same shape, not only that one library.

ts
type MeshLoaderDelegate = (path: string, manager: unknown, material: unknown, onComplete: (obj: unknown | null, err?: Error) => void) => void

CreateMeshLoaderOptions

What assets.createMeshLoader accepts beyond the robot and the delegate.

Property Type Required Description
timeoutMs number no How long to wait for delegate’s onComplete before giving up. Defaults to 30s. A URDF pulls in thirty-odd meshes and a dashboard using this callback can run for days — a delegate that never calls back (an exception three.js swallowed internally, a parser stuck on a malformed mesh) must not leak the object URL or hang the load forever.

UrdfSceneManager

three.js’s own LoadingManager.setURLModifier(callback) shape — the only thing assets.prepareUrdfScene needs from a three.js LoadingManager, so a caller passes theirs straight in. Every load the manager oversees is routed through callback first — not only the loader you handed the manager to, but every loader it constructs internally on the same manager (ColladaLoader’s own TextureLoader for a .dae’s <init_from> images, in particular). That is the one hook that exists one level up, for everything, where createMeshLoader’s per-loader loadMeshCb override does not reach: TextureLoader has no override hook of its own.

Structural, not import('three') — this SDK does not depend on three.js or urdf-loader, same discipline as MeshLoaderDelegate above.

setURLModifier()

ts
setURLModifier(callback: (url: string) => string): unknown

Installs a callback every load through this manager is routed through first.

Parameter Type Required Description
callback (url: string) => string yes

Returns unknown.

PrepareUrdfSceneOptions

What assets.prepareUrdfScene accepts beyond the robot and the manager.

Property Type Required Description
concurrency number no How many assets to fetch in parallel. Default 6 — a default that keeps memory and connection counts sane for the common case (dozens of meshes), not a number with a sweep behind it. Pass your own if you have a reason to. Must be a positive integer — 0 or negative rejects with invalid_option rather than silently fetching nothing and returning a scene that renders completely blank with no error to explain why.
signal AbortSignal no Cancels this call — a caller who navigates away or switches to a different robot mid-load can abort every in-flight fetch this method has started, not merely stop it from starting new ones. Checked before the first request; if it fires while a request is already in progress, the SDK forwards it straight to fetch(), so the connection itself is torn down, not just abandoned by this SDK while it keeps running in the background. Every blob: URL already created before the abort is revoked before this call rejects with FleetlessError('aborted', ...) — the same guarantee a load that fails outright already had: an aborted load must not leak what it had already fetched. manager’s URL modifier is only ever installed once every asset has resolved (success or the throw below) — an aborted call never installs a partial one, so manager is left exactly as it was if this rejects before that point.

UrdfSceneResources

What assets.prepareUrdfScene resolves with: the URDF text to parse, what the sync could not resolve, and the cleanup for everything it fetched.

Property Type Required Description
urdfText string yes The robot’s URDF as raw text — package:// URIs intact, not rewritten to asset URLs. Hand it straight to URDFLoader.parse(urdfText) once setURLModifier is installed (this method already installed it on manager before returning).
missing ({ uri: string; element: "mesh" | "texture" })[] yes The same entries assets.list()'s urdf.missing reports — verbatim, not reduced to bare strings. Each carries element ('mesh' | 'texture') alongside uri: before this, both kinds arrived as an undifferentiated string[] and a caller could only ever say “N meshes missing”, wrongly, for a URDF whose gap was actually a texture. Reducing this field back to string[] here would throw the distinction away again at exactly the point a caller would render it. If you only need the URIs, missing.map(m => m.uri). Top-level only — not a .dae’s internal references. The cloud builds this list from the URDF text alone (<mesh>/<texture> filename attributes), which is the only place it can see without parsing every .dae a sync touches; it never has and never can include an internal <init_from> reference. Do not build a completeness check on it as though it covered both: a .dae-internal reference the sync could not resolve surfaces through the sync’s own failure reporting instead, not here. Not thrown either way: an incomplete URDF still renders what it has (urdfCompleteness’s own contract), so the caller decides whether to warn, block, or ignore before calling URDFLoader.parse(urdfText). A reference NOT in this list that still fails to load at render time is a different failure — the store answered but the fetch itself did not.

dispose()

ts
dispose(): void

Revokes every object URL this call created that carries bytes. Call once the scene has finished loading (success or failure) or on unmount — safe to call more than once.

One object URL is deliberately kept: the shared zero-byte placeholder every refused reference resolves to. It costs nothing to leave alive, and leaving it is what lets this method stay simple — the installed URL modifier goes on refusing an owned-but-gone reference correctly, rather than falling back to the original string once the map is empty.

Does not touch manager’s URL modifier. Resetting it to the identity function here would reopen the very hole the modifier exists to close, the moment the same manager was used again — for a second robot, or for anything else — until a later prepareUrdfScene call happened to overwrite it. The installed modifier is left running, and with this call’s map now empty it already refuses anything it would have owned and passes through anything it would not have, correctly, on its own.

Returns void.

AssetListResponse

The asset-list-response wire schema, re-exported so app code and the SDK agree on the shape — see API Schemas for its fields.

Asset

The asset wire schema, re-exported so app code and the SDK agree on the shape — see API Schemas for its fields.

UrdfCompleteness

The urdf-completeness wire schema, re-exported so app code and the SDK agree on the shape — see API Schemas for its fields.