blob: c849fb61469ceb41ef65c0b07de252d2a64c910f [file]
= QuickJS Language
:doctitle: QuickJS
:shortname: quickjs
:artifactid: camel-quickjs
:description: Evaluates a JavaScript expression using QuickJS4J
:since: 4.23
:supportlevel: Preview
:tabs-sync-option:
*Since Camel {since}*
The QuickJS language evaluates JavaScript as an xref:manual::expression.adoc[Expression] or
xref:manual::predicate.adoc[Predicate] in Camel routes.
`camel-javascript` provides GraalVM JavaScript with Java interoperability, while `camel-quickjs`
provides a lightweight pure-Java JavaScript runtime using QuickJS4J and JSON-based data exchange.
QuickJS4J compiles QuickJS to WebAssembly and runs it as Java bytecode through Endive (the successor
to Chicory). There is no JNI and no native library. Native-image compatibility and
architecture-specific support have not been validated as part of this module.
Do not treat this language as a drop-in replacement for xref:js-language.adoc[JavaScript]. The
scripting APIs and Exchange bindings are different.
For example, you can use QuickJS in a xref:manual::predicate.adoc[Predicate] with the
xref:eips:choice-eip.adoc[Content-Based Router] EIP.
== QuickJS Options
// language options: START
include::partial$language-options.adoc[]
// language options: END
== Variables
The following variables are bound for each evaluation. Values are JSON snapshots taken before the
script runs, not live Java objects. Live `Exchange`, `Message` and `CamelContext` instances are
never exposed; the `camel` API below is the way to read the current state or to change it.
[width="100%",cols="10%,10%,80%",options="header",]
|=======================================================================
|Variable |Type |Description
|body |JSON value |the message body after JSON conversion
|headers |Object |the message headers after JSON conversion
|properties |Object |the exchange properties after JSON conversion
|exchangeId |String |the exchange id
|variables |Object |the exchange variables after JSON conversion (an empty object when there are none)
|exception |Object or null |`null`, or `{ type, message }` of the exception on the exchange (or the caught exception in an error handler)
|camel |Object |the controlled Camel API, see below
|=======================================================================
`message`, `exchange`, and `context` are not bound. Scripts that refer to them raise a JavaScript
`ReferenceError`.
Assigning to `headers`, `properties`, `variables` or `body` inside a script changes only the
JavaScript snapshot. It does not mutate the Camel `Exchange`. Use the `camel` API, or the
expression result (for example `.transform().quickjs(...)`) when you need to change the message.
=== The camel API
`camel` is a frozen object whose functions read and write the *current* exchange through
QuickJS4J host functions. Arguments and results cross the boundary as JSON, so a value written
with `camel.setHeader` is stored as a `String`, `Number`, `Boolean`, `List` or `Map`.
[width="100%",cols="30%,70%",options="header",]
|=======================================================================
|Function |Description
|`camel.getBody()` |the current message body (JSON snapshot; a streaming body raises the same error as the `body` binding)
|`camel.setBody(value)` |replaces the message body
|`camel.getHeader(name)` |a header of the current message (JSON snapshot)
|`camel.setHeader(name, value)` |sets a header on the current message
|`camel.removeHeader(name)` |removes a header and returns its previous value
|`camel.getProperty(name)` / `camel.setProperty(name, value)` / `camel.removeProperty(name)` |the same for exchange properties
|`camel.getVariable(name)` / `camel.setVariable(name, value)` / `camel.removeVariable(name)` |the same for exchange variables
|`camel.log(level, message)` |logs through the `org.apache.camel.language.quickjs.script` logger at `trace`, `debug`, `info`, `warn` or `error`
|=======================================================================
[tabs]
====
Java::
+
[source,java]
----
from("direct:start")
.setBody().quickjs("camel.setHeader('processed', true); body.toUpperCase()")
.to("mock:result");
----
XML::
+
[source,xml]
----
<route>
<from uri="direct:start"/>
<setBody>
<quickjs>camel.setHeader('processed', true); body.toUpperCase()</quickjs>
</setBody>
<to uri="mock:result"/>
</route>
----
YAML::
+
[source,yaml]
----
- route:
from:
uri: direct:start
steps:
- setBody:
expression:
quickjs:
expression: 'camel.setHeader(''processed'', true); body.toUpperCase()'
- to:
uri: mock:result
----
====
The `camel` API is only available while a route expression is evaluated; the generic
`ScriptingLanguage.evaluate(script, bindings, resultType)` entry point has no current exchange.
=== Expressions and statements
A script that is a single expression (`body.amount > 100`, `{ a: body }`) is compiled as an
expression and returns its value. Any other script (several statements, a trailing semicolon, a
`var` declaration) is evaluated as statements and returns its completion value, the value of the
last statement, exactly as `eval` would. Note that `{ a: 1 }` is therefore an object when it is
the whole script but a block statement when it is followed by other statements.
The generic `ScriptingLanguage.evaluate(script, bindings, resultType)` API uses caller-supplied
map keys as JavaScript function parameters. Those keys must be valid JavaScript identifiers
(for example `body` or `foo_bar`). Names such as `foo-bar`, `123foo`, or reserved words such as
`for` are rejected with a Camel evaluation exception rather than a raw JavaScript `SyntaxError`.
Route expressions do not use this map.
== Data types
Values cross the Java/JavaScript boundary as JSON:
* `null`, string, boolean, and number pass through.
* `Map` becomes a JavaScript object. Header and property names are strings, so
`headers.MyHeader` and `headers['MyHeader']` both work.
* `List` and arrays become JavaScript arrays.
* `byte[]` becomes a Base64 JSON string. `char[]` becomes a JSON string.
* Other Java types are serialized with Jackson into a JSON object or array snapshot.
Java methods such as `getAge()` are not callable from JavaScript; use JSON fields
such as `body.age`.
* `Exchange`, `Message`, `CamelContext`, `Class`, and `ClassLoader` values are rejected
when they appear as the message body, with an evaluation error. They are never passed
into the script.
* Streaming bodies (`InputStream`, `Reader`, and Camel `StreamCache`) are rejected with
an evaluation error. The stream is not read, closed, or otherwise consumed.
* Header and property values that cannot be JSON-serialized (including Camel internals
and streaming values) are omitted from the JavaScript snapshot so evaluation can still
use the remaining data.
Serialization failures of the message body raise a Camel evaluation exception that names
the unsupported type.
== Expression and predicate
As an expression, the JavaScript value of the script becomes the Camel result (then converted
with Camel type converters when a result type is requested).
As a predicate (`.when().quickjs(...)` or `.filter().quickjs(...)`), the result is converted to
boolean with Camel's standard `ObjectHelper.evaluateValuePredicate` rules: a `Boolean` is used
directly; the strings `true`/`false` are parsed; any other non-empty, non-null value is true.
== Engine lifecycle
Every worker thread owns one QuickJS engine, created on first use and closed when the language stops. Each
engine keeps the last 1,000 route expressions it evaluated in compiled form, so a script is compiled once per
thread and then only executed. A JavaScript exception thrown by a script leaves the engine usable; a trap inside
the runtime (a `camel` function that failed, a stack overflow) does not, and the engine of that thread is then
discarded and recreated on the next evaluation.
QuickJS keeps every module it has evaluated until its context is freed, and QuickJS4J evaluates a
module per call, so an engine grows with every evaluation. The language therefore recycles a
thread's engine once its WebAssembly memory exceeds `engineMaxMemory` (64 MB) or it has run
`engineMaxEvaluations` (50,000) evaluations. Both are properties of `QuickjsLanguage` and can be set like any
language option, for example in `application.properties`:
[source,properties]
----
camel.language.quickjs.engineMaxMemory = 134217728
camel.language.quickjs.engineMaxEvaluations = 100000
----
or programmatically through `((QuickjsLanguage) context.resolveLanguage("quickjs")).setEngineMaxMemory(...)`.
== Security
JavaScript runs in the QuickJS4J sandbox. The runtime does not expose Java classes, reflection,
class loaders, or live Camel objects. WASI has no filesystem or network preopens. Stdout from
scripts is discarded so a reused engine does not accumulate output. Per-evaluation stderr is
captured into Camel exceptions (for example `ReferenceError`) and then cleared so later
evaluations do not include stale error output.
QuickJS4J host plumbing is not available to user scripts: `java_invoke` throws a
`TypeError` and `quickjs4j_engine` is undefined, including when accessed through
`globalThis`. The `camel` API is the only host bridge scripts can reach, and it only
dispatches to the functions listed above.
== Usage
[tabs]
====
Java::
+
[source,java]
----
import static org.apache.camel.language.quickjs.QuickjsLanguage.quickjs;
public class MyRouteBuilder extends RouteBuilder {
@Override
public void configure() {
from("direct:start")
.choice()
.when().quickjs("headers.MyHeader == 'foo'").to("mock:foo")
.otherwise().to("mock:other");
}
}
----
XML::
+
[source,xml]
----
<route>
<from uri="direct:start"/>
<choice>
<when>
<quickjs>headers.MyHeader == 'foo'</quickjs>
<to uri="mock:foo"/>
</when>
<otherwise>
<to uri="mock:other"/>
</otherwise>
</choice>
</route>
----
YAML::
+
[source,yaml]
----
- route:
from:
uri: direct:start
steps:
- choice:
when:
- expression:
quickjs:
expression: 'headers.MyHeader == ''foo'''
steps:
- to:
uri: mock:foo
otherwise:
steps:
- to:
uri: mock:other
----
====
Transform the body with the expression result:
[tabs]
====
Java::
+
[source,java]
----
from("direct:start")
.transform().quickjs("body.toUpperCase()")
.to("mock:result");
----
XML::
+
[source,xml]
----
<route>
<from uri="direct:start"/>
<transform>
<quickjs>body.toUpperCase()</quickjs>
</transform>
<to uri="mock:result"/>
</route>
----
YAML::
+
[source,yaml]
----
- route:
from:
uri: direct:start
steps:
- transform:
expression:
quickjs:
expression: body.toUpperCase()
- to:
uri: mock:result
----
====
You can load the script from an external resource with the
`resource:scheme:location` syntax, for example `resource:classpath:myscript.js`
or `resource:file:/path/to/script.js`.
[WARNING]
====
Do not derive `resource:classpath:` or `resource:file:` paths from untrusted
input such as message headers or query parameters. A path taken from untrusted
data can cause Camel to load and evaluate an unexpected JavaScript file.
====
== Dependencies
To use QuickJS in your Camel routes, you need to add the dependency on
*camel-quickjs*.
QuickJS4J is licensed under Apache License 2.0 (ASF Category A).
If you use Maven, you could add the following to your `pom.xml`,
substituting the version number for the latest release.
[source,xml]
----
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-quickjs</artifactId>
<version>x.x.x</version>
</dependency>
----