Since the latest release, the Cyclone 3DR script engine supports four major new capabilities: asynchronous programming with async/await, HTTP requests with the Fetch API, ES module imports, and WebAssembly execution.
Async / Await
The script engine now fully supports async functions and the await keyword. The engine automatically tracks all pending promises and keeps the script alive until every asynchronous operation has completed.
Basic Usage
async function loadData()
{
var response = await fetch("https://api.example.com/data");
var data = await response.json();
print("Received: " + data.value);
}
loadData();
Error Handling
Use standard try/catch blocks inside async functions to handle errors.
async function safeFetch(url)
{
try
{
var response = await fetch(url);
return await response.json();
}
catch (err)
{
print("Request failed: " + err);
return null;
}
}
Parallel Operations
Use Promise.all() to run multiple asynchronous operations in parallel.
async function fetchMultiple()
{
var urls = [
"https://api.example.com/data1",
"https://api.example.com/data2",
"https://api.example.com/data3"
];
var responses = await Promise.all(urls.map(function(u) { return fetch(u); }));
var results = await Promise.all(responses.map(function(r) { return r.json(); }));
results.forEach(function(data) {
print("Result: " + data.value);
});
}
Other Promise Methods
The following Promise static methods are all supported:
- Promise.all(): Resolves when all promises resolve. Rejects if any promise rejects.
- Promise.race(): Resolves or rejects with the first promise to settle.
- Promise.allSettled(): Resolves when all promises settle (regardless of success or failure).
- Promise.resolve() / Promise.reject(): Create already-settled promises.
var first = await Promise.race([
fetch("https://server-a.example.com/data"),
fetch("https://server-b.example.com/data")
]);
var results = await Promise.allSettled([
fetch("https://reliable-server.example.com/data"),
fetch("https://unreliable-server.example.com/data")
]);
results.forEach(function(r) {
print(r.status);
});
- Note
- You can also use traditional promise chains with .then(), .catch(), and .finally() if you prefer.
Fetch API
Scripts can send HTTP and HTTPS requests with the global fetch() function.
- Warning
- What the script engine provides is a subset of the Web Fetch API, not a complete implementation. The call syntax and the promise-based behavior are the same as in a browser, but only what is described in this section is available. Everything else is missing or behaves differently: read What Is Not Supported before porting browser code.
Calling fetch()
fetch(url [, options]) starts the request and immediately returns a Promise which resolves with a response object once the whole answer has been received.
| Argument | Type | Description |
| url | string | Mandatory. Absolute URL using the http:// or https:// scheme. |
| options.method | string | GET (default), POST, PUT, DELETE or HEAD. Lowercase is accepted. |
| options.headers | object | Plain object whose properties are the request headers to send. |
| options.body | string | Request body. Sent for POST and PUT only. |
The url must be a string: passing a Request or a URL object is not supported.
Any other option (mode, credentials, cache, redirect, referrer, integrity, signal, keepalive, ...) is silently ignored.
- Warning
- Unlike the browser, calling fetch() with a missing or non-string url throws immediately instead of returning a rejected promise. Inside an async function that exception is converted into a rejection and can be caught with try/catch, but in the main body of a script it aborts the execution.
GET Request
var response = await fetch("https://api.example.com/items");
var data = await response.json();
print("Items count: " + data.length);
POST Request
The body must be a string. Serialize your data yourself, typically with JSON.stringify(), and declare the matching Content-Type: no header is added for you.
var response = await fetch("https://api.example.com/items", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer my-token"
},
body: JSON.stringify({ name: "New Item", quantity: 10 })
});
if (response.ok)
{
var result = await response.json();
print(
"Created item: " + result.id);
}
else
{
print(
"Error: " + response.status +
" " + response.statusText);
}
print(any arg)
Print to output the argument.
- Warning
- A body which is not a string is converted with the standard JavaScript string conversion, which almost never gives what you expect (an object becomes the text "[object Object]"). Binary request bodies (ArrayBuffer, typed arrays, Blob, FormData, URLSearchParams) are not supported.
Response Object
The whole answer is downloaded before the promise resolves, so the response object below always carries a complete body.
| Property / Method | Type | Description |
| response.ok | boolean | true when the HTTP status is in the 200-299 range. |
| response.status | number | HTTP status code (200, 404, 500, ...). |
| response.statusText | string | HTTP reason phrase ("OK", "Not Found", ...). |
| response.headers | object | Response headers as a plain object. See Request and Response Headers. |
| response.text() | Promise | Resolves with the body decoded as UTF-8 text. |
| response.json() | Promise | Resolves with the body parsed as JSON. |
| response.arrayBuffer() | Promise | Resolves with the raw body bytes as an ArrayBuffer, for binary content such as a WebAssembly module. |
That list is exhaustive. There is in particular no response.url, response.type, response.redirected, response.bodyUsed, response.body stream, response.clone(), response.blob() nor response.formData().
- Note
- The response is a plain JavaScript object, not an instance of the Web API Response class. The engine declares no global Response, Request nor Headers constructor, so tests such as response instanceof Response cannot be used. Properties whose name starts with an underscore are internal to the engine: never rely on them.
Since the body is held in memory, text(), json() and arrayBuffer() may be called several times and in any order on the same response. A browser would fail on the second call, so do not rely on this if the same script must also run in a browser.
- Warning
- Those three methods read the response they are called on: always invoke them as response.text(), response.json() or response.arrayBuffer(). A detached reference such as var read = response.text; read(); does not work.
response.text() always decodes the body as UTF-8, whatever the charset advertised by the server. Other encodings are not converted.
response.json() parses the same UTF-8 text and its promise is rejected when the body is not valid JSON.
Request and Response Headers
Request headers are given as a plain object of string values. The Headers class is not available, so it cannot be used to build them.
var response = await fetch("https://api.example.com/items", {
headers: { "Accept": "application/json", "X-Api-Key": "my-key" }
});
Response headers are exposed as a plain object too, which means the browser accessors response.headers.get(), .has() and .forEach() do not exist. Two consequences are worth remembering:
-
Header names are kept exactly as the server sent them, and property lookup is case sensitive. Compare names in lowercase rather than assuming a spelling.
-
A header sent several times (Set-Cookie is the usual case) only keeps its last value.
function getHeader(response, name)
{
var wanted = name.toLowerCase();
var keys = Object.keys(response.headers);
for (var i = 0; i < keys.length; i++)
{
if (keys[i].toLowerCase() === wanted)
return response.headers[keys[i]];
}
return null;
}
print("Type: " + getHeader(response, "content-type"));
Supported HTTP Methods
-
GET (default): Retrieve data.
-
POST: Submit data, with a body.
-
PUT: Update data, with a body.
-
DELETE: Remove data. The body is ignored.
-
HEAD: Retrieve headers only. The response body is empty.
- Warning
- No other method is implemented: PATCH, OPTIONS, TRACE or a custom verb rejects the promise with the message "fetch(): unsupported HTTP method".
A body is only transmitted with POST and PUT. It is silently dropped for GET, DELETE and HEAD.
Error Handling
Network errors (unknown host, connection refused, aborted transfer, TLS failure) reject the fetch() promise.
HTTP errors (4xx, 5xx) do not: the promise resolves and response.ok is set to false. Always check response.ok or response.status.
try
{
await fetch("https://this.server.does.not.exist/data");
}
catch (err)
{
print("Network error: " + err);
}
var response = await fetch("https://api.example.com/missing-endpoint");
if (!response.ok)
{
print("HTTP error: " + response.status);
}
- Warning
- Rejections carry a string, not an Error nor a TypeError object. Printing it or concatenating it works as shown above, but err.message, err.name and err.stack are all undefined.
HTTPS, Redirections and Cancellation
-
HTTPS: TLS 1.2 or later is enforced and the server certificate is verified. A self-signed, expired or mismatched certificate makes the request fail, and there is no option to bypass the verification.
-
Redirections: they are followed automatically, except a redirection which would downgrade HTTPS to HTTP. As response.url does not exist, the final URL cannot be read back from the response.
-
Timeout: no timeout option is exposed. A request which never answers keeps the script waiting.
-
Cancellation: AbortController and options.signal do not exist. Stopping the script from the user interface aborts the requests still in flight, and their promises are rejected.
-
Authentication: there is no credentials option and no interactive credential prompt. Send an Authorization header, or any token your server expects, explicitly.
What Is Not Supported
Summary of the browser features which are not available:
-
The Request, Response, Headers, FormData, Blob and URLSearchParams classes.
-
Every option other than method, headers and body, in particular signal (AbortController), mode, credentials, cache, redirect, referrer, integrity and keepalive.
-
HTTP methods other than GET, POST, PUT, DELETE and HEAD.
-
Non-string request bodies: binary uploads and multipart form data.
-
Streaming: response.body, ReadableStream, and progress notifications while uploading or downloading.
-
response.clone(), response.blob(), response.formData(), response.url, response.type, response.redirected and response.bodyUsed.
-
Body decoding other than UTF-8.
-
Error objects: rejection values are strings.
-
Schemes other than http:// and https://.
- Note
- No browser origin exists here, so no CORS restriction applies to a script: any reachable server can be called.
Dynamic Import
The import() operator allows you to dynamically load ES modules at runtime.
Importing a Local Module
Create an ES module file (e.g. my_utils.js) with exports:
export function greet(name) { return "Hello, " + name + "!"; }
export const VERSION = "1.0";
Then import it in your script:
var utils = await import("C:/Scripts/my_utils.js");
print(utils.greet("World"));
print("Version: " + utils.VERSION);
Relative Paths
When your script is saved to a file, you can use relative paths. The import is resolved relative to the current script's location.
var utils = await import("./my_utils.js");
var helpers = await import("./lib/helpers.js");
- Note
- The .js extension is optional. If omitted and the file is not found, the engine automatically appends .js.
Importing from a URL
You can also import ES modules from an HTTP/HTTPS URL:
var mod = await import("https://example.com/scripts/utils.js");
print(mod.someFunction());
Static Imports Between Modules
Inside an ES module file, you can use the standard import ... from syntax to import other modules:
import { PI, multiply } from "./math_utils.js";
export function circumference(radius)
{
return 2 * PI * radius;
}
- Note
- The import ... from syntax only works inside ES module files. It cannot be used in your main script.
Your main script must use the dynamic import() function to load the first module. That module can then use import ... from to load its own dependencies.
var geo = await import("./geometry.js");
print("Circumference: " + geo.circumference(10));
This means you can organize your code into multiple module files with dependencies between them, and only the entry point needs to use import().
Module Caching
Modules are cached after the first load. Importing the same module multiple times returns the same instance without re-fetching or re-compiling.
var m1 = await import("./math_utils.js");
var m2 = await import("./math_utils.js");
TypeScript Definitions (.d.ts)
To enable autocompletion for your modules in the built-in script editor, create a TypeScript definition file next to your module:
export function greet(name: string): string;
export const VERSION: string;
When the module is loaded, the script editor automatically discovers the .d.ts file and provides:
- Autocomplete suggestions for the module's exported functions and variables.
- Type hints when hovering over imported symbols.
Error Handling
If a module cannot be found or has syntax errors, the import() promise is rejected.
try
{
await import("./nonexistent_module.js");
}
catch (err)
{
print("Import failed: " + err);
}
WebAssembly
The script engine supports WebAssembly through the standard WebAssembly JavaScript API. This allows scripts to load and execute compiled WebAssembly modules for computationally intensive tasks.
Typical Workflow
The standard workflow is to fetch a .wasm file, convert the response to an ArrayBuffer, and then instantiate it:
var response = await fetch("https://example.com/math.wasm");
var bytes = await response.arrayBuffer();
var result = await WebAssembly.instantiate(bytes);
var add = result.instance.exports.add;
print("10 + 20 = " + add(10, 20));
Validating a Module
You can validate a WASM binary before instantiating it:
var response = await fetch("https://example.com/module.wasm");
var bytes = await response.arrayBuffer();
if (WebAssembly.validate(bytes))
{
var result = await WebAssembly.instantiate(bytes);
}
else
{
print("Invalid WASM module!");
}
Available API
-
WebAssembly.instantiate(bytes [, imports]): Compile and instantiate a WASM module. Returns a Promise resolving to an object with module and instance properties.
-
WebAssembly.compile(bytes): Compile a WASM module without instantiating it. Returns a Promise resolving to a WebAssembly.Module.
-
WebAssembly.validate(bytes): Synchronously checks if bytes form a valid WASM module. Returns a boolean.
Complete Example: Import + Fetch + Async
Here is a complete example combining all four features:
var math = await import("./modules/math_utils.js");
var response = await fetch("https://api.example.com/measurements");
var measurements = await response.json();
var total = 0;
measurements.forEach(function(m) {
total = math.add(total, m.value);
});
print("Total: " + total);
print("Average: " + (total / measurements.length));
print("Circle area for avg radius: " + math.circleArea(total / measurements.length));
- Note
- All asynchronous operations (fetch, import, WebAssembly) use the same promise-based mechanism. The script engine automatically waits for all pending operations before the script finishes.