Thread safety
An Engine serves one operation at a time and is not thread-safe. Concurrent public entries fail fast with InvalidOperationException; Jint does not silently serialize them.
var value = await engine.EvaluateAsync(script);
pool.Return(engine); // only after the operation has completedKeep one engine exclusively assigned to one request or operation. Await EvaluateAsync, ExecuteAsync, InvokeAsync, Modules.ImportAsync, and UnwrapIfPromiseAsync before returning the engine to a pool or disposing it. Dispose also fails while an operation owns the engine.
Same-thread synchronous re-entry from a host callback is supported. Starting an async engine API from inside an active engine call is not; the callback must return before ownership can transfer.
A JavaScript callback converted to a CLR delegate may be invoked on another host thread while the operation that exposed it is still open, while an async engine operation or blocking event-loop drain is outstanding, or while the host is waiting for scheduled work. Outside those admission windows, the call fails rather than silently racing or deadlocking the engine.
Queuing from another thread
Tasks.Post is the deliberate cross-thread exception. It only enqueues and never runs script on the caller:
engine.Tasks.Post(() => engine.Invoke("onMessage", payload));The action runs on whichever thread next calls engine.Tasks.ProcessTasks(). Background task and module completions may likewise enqueue work, but they do not grant permission for unrelated host calls while an async operation owns the engine.
engine.Advanced.Retire() is another cross-thread entry. It permanently stops new script and queued work, wakes a pending promise or scheduled-work wait, and releases transient resources once the current engine entry returns. It does not preempt a script already running. Dispose the engine after that entry and any outstanding *Async task complete.
Values and construction
JavaScript objects are engine-affine. Build JsObject, JsArray, and other engine-owned values on the thread that owns the engine, or while it is idle. Passing an object-valued JsValue to another engine is unsupported.
A primitive JsValue (a string, number, boolean or BigInt) never changes once an engine has produced it. Script that appends to the variable or property a string came from produces a new string and leaves the one the host holds as it was. So reading a primitive from several threads at once is safe, even while the engine that produced it keeps running. Some strings produce their text on first read, such as a long concatenation, a slice or a LazyJsString. Those first reads may race, and every reader still gets the complete text.
A primitive may also be passed to another engine. Two engines can append to the same string at once, and each builds its own result.
Prepared<Script> and Prepared<Module> are different: they are reusable, thread-safe, and may execute concurrently on separate engines. A configured Options may also be shared by engines being constructed concurrently, but finish configuring it before construction begins.
For a dedicated host loop, use TimeUntilNextScheduledWork, WaitForScheduledWork, and ProcessTasks as described in Web APIs. There must still be only one drainer per engine.