Modules and Lifecycle
JavaScript modules group host functions with callbacks for engine initialization, compilation, execution, and cleanup. Use them when an integration needs to acquire resources, observe work, or release state along with exposing functions to FQL.
Define a module
Use defineModule() to validate a definition while preserving its inferred TypeScript type:
The module name identifies its registration, lifecycle callbacks, and module-level diagnostics. The optional namespace independently places its functions under an FQL namespace. It is not inferred from name.
Without namespace, functions keep their existing top-level behavior:
FQL calls this function as app_name(). In the first example, FQL calls the namespaced function as OBSERVABILITY::TELEMETRY::track(); track() is not also registered globally.
Every lifecycle callback may return immediately or return a promise. Ferret waits for the promise before continuing the operation.
Define modules in TypeScript
The Node.js and browser entry points export ModuleDefinition, ModuleLifecycle, MaybePromise, and all lifecycle event types. A definition can also be declared directly:
defineModule() returns the same object that it receives. It provides validation and preserves the specific inferred type rather than wrapping the module in a runtime class.
Registration and immutability
create({ modules }) requires an array. Each module definition, its functions object, and its lifecycle object must be a plain JavaScript object. Module names must contain at least one non-whitespace character, function values must be callable, and lifecycle objects may contain only the documented callback names.
Module names are case-sensitive stable identifiers. Two modules named audit are rejected, while audit and Audit are distinct registrations as long as their functions do not conflict.
A namespace contains one or more FQL identifier segments separated by ::, such as TELEMETRY or OBSERVABILITY::TELEMETRY. Each segment starts with an ASCII letter and may contain ASCII letters, digits, or underscores. Omit namespace for top-level functions; an empty or malformed namespace is rejected. Declared spelling is preserved, while FQL resolves namespaces case-insensitively.
Engine creation revalidates every definition and snapshots its namespace, functions, and lifecycle callbacks. Mutating the original objects afterward does not change the engine. Create another engine when the application needs a different module configuration.
Function names continue to use Ferret’s case-insensitive canonical resolution. Collisions use the resulting qualified FQL identity, so two modules may both expose track under different namespaces. Engine creation rejects two registrations that resolve to the same qualified function, or a conflicting user-defined function and standard-library function, instead of choosing an implementation.
Qualified function keys are relative to the module namespace. For example, namespace OBSERVABILITY with function key TELEMETRY::flush exposes OBSERVABILITY::TELEMETRY::flush().
The existing functions option remains available for integrations that only need host functions. It can be combined with modules and is registered before the modules:
Lifecycle callbacks
The ModuleLifecycle interface exposes callbacks from Ferret Core’s engine, plan, and session lifecycle:
| Callback | When it runs | Event |
|---|---|---|
onInit |
During create(), after module registration |
None |
onClose |
During engine cleanup | None |
beforeCompile |
Before a source is compiled | CompileEvent |
afterCompile |
After a compilation attempt | CompileResultEvent |
onPlanClose |
When a plan closes | PlanEvent |
beforeRun |
Before a session executes | RunEvent |
afterRun |
After an execution attempt | RunResultEvent |
onSessionClose |
When a session closes | SessionEvent |
Each callback returns MaybePromise<void>, which means either void or a promise-like value that resolves to void.
Compile events
Compile callbacks receive an immutable snapshot of the normalized source:
CompileEvent.source always contains name and text. CompileResultEvent adds error?: unknown; a Ferret compiler failure is represented as a JavaScript Error.
Run and cleanup events
RunResultEvent adds error?: unknown, with Ferret VM failures represented as JavaScript Error objects. PlanEvent, RunEvent, and SessionEvent are immutable empty objects today because Ferret Core does not provide stable per-object metadata for those hooks. Their interfaces remain extensible for future stable fields.
Lifecycle events do not expose Go contexts, pointers, registrars, bridge identifiers, or handles. Cancellation remains available through the AbortSignal passed to compile, run, and session APIs.
Ordering and errors
Lifecycle ordering follows Ferret Core:
| Hook group | Order | Failure behavior |
|---|---|---|
| Initialization and before hooks | Module registration order (FIFO) | Stop at the first failure |
| After and close hooks | Reverse registration order (LIFO) | Continue and aggregate failures |
If a before hook fails, its operation does not begin and the corresponding after hooks do not run. When compilation or execution begins and then fails, result hooks still run and receive the underlying compiler or VM error.
A callback that throws synchronously or returns a rejected promise rejects the operation associated with that hook. Error messages include the module and lifecycle phase while retaining the original thrown or rejected message. An onInit failure rejects create() and triggers cleanup of the registered engine hooks.
Closing resources
Lifecycle-aware cleanup preserves the resource rules described in Executing Ferret:
- A close preflight rejects without cleanup while compilation or session creation is pending, or while a child session is running or closing.
- Once close begins, the resource becomes closed even if a lifecycle callback rejects. Remaining child cleanup and close callbacks continue, and the close promise rejects with the aggregated errors.
- Calling
close()again does not rerun lifecycle callbacks. - Operations that re-enter a resource while it is initializing or closing reject instead of racing its lifecycle. Do not call operations on that same resource from its close callback.
Always await close() and handle cleanup failures at the application boundary.