genroc

    docs / Guides / Process definition

    Typescript evaluation

    Learn how to run typescript through genroc

    If you want to have custom scripts as part of the process, initialize your project with genctl init --eval-node.

    The setup

    If we look at the project structure, you will see, that there are some extra fields compared to regular one.

    ├── .genroc                        # registers the `import` resolver for .ts files
    ├── compose.yaml                   # adds an `eval-node` worker service
    ├── package.json                   # @genroc/eval-node npm package
    ├── tsconfig.json
    └── definitions
        ├── hello.genroc.yaml          # the example process below
        ├── script-node.genroc.yaml    # the child process that runs a script
        └── greet.ts                   # the script hello runs

    The script-node.genroc.yaml is a child process which is an abstraction above the execution engine. Inside it uses external task to expose the script code and the input.

    compose.yaml then contains setup for genroc/eval-node docker container. This is our node worker that is able process the script tasks. You can take this as an example implementation of genroc worker.

    Example process

    The example process will look something like this:

    name: hello
    input_schema:
      type: object
      properties:
        who: { type: string, default: "world" }
    tasks:
      - id: greet
        action:
          type: child
          # `name`, `input_schema` and `raises` come from the child
          <<: "$process: ./script-node.genroc.yaml"
          input:
            # script code
            code: "$import: ./greet.ts"
            # script input
            input:
              who: "$: input.who"
          # defining the script output
          result_schema:
            type: object
            properties:
              greeting: { type: string }
              at: { type: string }
            required: [greeting]
        output: "$: self.result"
        switch:
          - goto: end
    
    output: "$: outputs.greet"

    We are using two kinds of resolvers. $process: is build in documented in child process section. Then we have $import: ./greet.ts, this is a special resolver intended for importing typescript files.

    The script format

    Genroc expects default export to be function and it will pass input as a first argument. It will take it's output as a result. The function may also be async.

    // greet.ts
    import type { Input, Output } from "./greet.genroc";
    
    export default function (input: Input): Output {
      return { greeting: `hello, ${input.who}`, at: new Date().toISOString() };
    }

    The bundler will also include any imported files and imported libraries.

    Node built-ins stay as imports and need "types": ["node"] in tsconfig.json.

    How does it work?

    The content of the .ts file is bundled with rollup into one string and included directly into the process definition, so it can't drift.

    The script-node child process exposes external task eval_node, which is then picked up by eval-node worker. eval-node is running in a separate container and it uses genroc worker API to poll for work.

    What if the script fails

    script-node child process raises different errors in different situations.

    CodeKindWhenerror.data
    script_threwraisethe script threw or its promise rejected{name, stack?}
    script_timeoutraisethe script ran past timeout_ms (default 5000){budget_ms}
    script_unknownraiseno worker answered within timeout_ms: none is running, all are busy, or one died holding the task—
    script_brokenpanicthe script does not compile, returns a value that is not JSON, or calls process.exit(){name, stack?}

    The raises are copied into your task by <<: "$process: ./script-node.genroc.yaml", so on_error can match them directly. script_broken is a panic and cannot be caught: it fails the caller and every process above it, because the only fix is editing the script.

    error.data.name is the thrown error's class name (class LimitExceeded extends Error {} gives LimitExceeded), unless the script sets e.name = "MyName" to override it. Throwing something that is not an Error gives Thrown.

    Generated types

    Run genctl generate and this setup will also write Input and Output types for the greet.ts script.

    This way the script input and output is tied to the current input and result_schema defined in process. genctl apply will fail if those types don't match.

    Timeouts

    You can set timeout for the script:

      - id: greet
        action:
          type: child
          # `name`, `input_schema` and `raises` come from the child
          <<: "$process: ./script-node.genroc.yaml"
          input:
            # script code
            code: "$import: ./greet.ts"
            # script input
            input:
              who: "$: input.who"
            timeout_ms: 10_000

    timeout_ms (default 5000) is the deadline of the whole script task, and the eval-node worker runs the script against that same deadline. It stops the script just before it and reports script_timeout; if no worker picks the task up in time, the result is script_unknown. Time spent waiting in the queue counts toward the budget.

    Custom resolvers

    If you open .genroc file, you can see it contains entry for import resolver. It executes genroc-import script which is a part of @genroc/eval-node package (installed in node_modules).

    definitions: ["./definitions/**/*.genroc.yaml"]
    
    resolvers:
      - name: import
        phase: typed
        ext: [.ts]
        command: [npx, genroc-import]
        # Slot addresses the generated `Input` and `Output` are typed from, relative to the task.
        types:
          Input: task.action.input.input
          Output: task.action.result

    The resolver can define what types it needs and genroc will provide json schema types for the specified fields. Here we need task.action.input.input which is the input slot next to the code. And then we also take result as a output.

    You can create your custom resolvers in the similar fashion (check project file reference).

    eval-node

    Typescript support is enabled by eval-node docker image which runs as a separate service. It is a node worker which expects the genroc server on port 8448 and resolves external task from script-node process.

    npm package

    Resolver in .genroc file calls eval-node npm package binary, which uses rollup to bundle the requested file into a single string (also strips types). It also inlines dependencies so you can use javascript/typescript packages as well.