Runtime System
The runtime (@gi-tcg/gts-runtime) provides the execution model for transpiled GTS code. It defines the Model-View-ViewModel (MVVM) pattern that processes define statements at runtime.
Overview
When a .gts file is transpiled and executed, the generated JavaScript calls runtime functions to:
- Create attribute node trees ("View") from GTS definitions
- Parse those trees through ViewModels and run codes on associated Models
- Extract binding values (exported variables from
asclauses)
Entry Points
createDefine(rootVM, node) — executes a define statement (fire-and-forget):
function createDefine(
rootVM: ViewModel<any, any>,
node: SingleAttributeNode,
): void {
const view = new View<any>({ attributes: [node] });
rootVM.parse(view);
}createBinding(rootVM, node) — executes a define and returns binding values:
function createBinding(
rootVM: ViewModel<any, any>,
node: SingleAttributeNode,
): unknown[] {
const bindingCtx = new BindingContext();
const view = new View<any>({ attributes: [node] }, bindingCtx);
rootVM.parse(view);
return bindingCtx.getBindings();
}ViewModel (src/view_model.ts)
ViewModel Class
The ViewModel<ModelT, BlockDef> is the central execution unit:
class ViewModel<ModelT, BlockDef extends AttributeBlockDefinition> {
constructor(private Ctor: new () => ModelT) {}
parse(view: View<...>): ModelT {
const model = new this.Ctor();
for (const attrNode of view._node.attributes) {
// Look up registered action (or binder if inside binding context)
// Call the action with (model, positionals, namedView)
// If binding flag set, collect the return value
}
return model;
}
}Execution flow:
- Instantiate the Model class (
new Ctor()) - Iterate over attribute nodes
- For each attribute, look up the registered action (or binder) by name
- Call the action with
(model, positionals, new View(named, bindingCtx)) - If the attribute has a
bindingflag and we're in a binding context, collect the return value - Return the built Model
Action vs. Binder:
- Actions are used during
createDefine— they execute the game logic (e.g., set properties on the builder Model) - Binders are used during
createBinding— they compute the exported value (e.g., return a handle/ID)
defineViewModel
function defineViewModel<T, BlockDef, InitMeta>(
Ctor: new () => T,
modelDefFn: (helper: AttributeDefHelper<T>) => BlockDef,
initMeta?: InitMeta,
): ViewModel<T, BlockDef & { [Meta]: InitMeta }>;Usage (from examples/provider/vm.ts):
const CharacterVM = defineViewModel(
CharacterBuilder,
(helper) => ({
id: helper.attribute<{
(id: number): AR.Done;
required(): true;
as<TMeta>(this: AR.This<TMeta>): CharacterHandle<TMeta["varNames"]>;
}>(
(model, pos) => {
/* action: set ID on model */
},
(_, [id]) => id as CharacterHandle<any>, // binder: return handle
),
since: helper.simpleAttribute()(function (version: "v3.3.0" | "v3.4.0") {
this.setVersion(version);
}),
tags: helper.simpleAttribute()(function (...tags: Tag[]) {}),
health: helper.simpleAttribute()(function (value: number) {}),
energy: helper.simpleAttribute()(function (value: number) {}),
skills: helper.attribute<{
(...handles: CharacterSkillHandle[]): AR.Done;
}>(() => {}),
}),
{} as { varNames: never },
);AttributeDefHelper
The helper provides two methods for defining attributes:
attribute<T>(action, binder?) — full control over action and binder:
action(model, positionals, namedView)— called during definebindercan be:- A function
(model, positionals, namedView) => value— custom binder - A
ViewModel— automatically callsvm.parse(namedView)as the binder - Omitted — no-op binder
- A function
simpleAttribute(options?) — returns a callable that takes (action, binder?):
actionreceivesthis: ModelTand spread positional argsbinderreceivesthis: ModelTand spread positional args, returns the binding valueoptions.required?: booleanandoptions.uniqueKey?: stringadd corresponding typed methods to the returned attribute definition
AttributeReturn Types
The AttributeReturn (aliased as AR) namespace provides return type utilities:
| Type | Description |
|---|---|
AR.Done | Attribute has no nested block and doesn't rewrite meta |
AR.This<TMeta> | Access the current meta type (for this parameter) |
AR.EnableIf<Cond, T> | Conditional type helper |
AR.With<VM, TMeta> | Attribute opens a nested ViewModel block |
AR.DoneRewriteMeta<NewMeta> | Attribute rewrites the meta type |
AR.WithRewriteMeta<NewMeta, VM, TMeta> | Opens nested VM and rewrites meta |
Meta rewriting is how GTS tracks typing-only accumulated state through attribute chains. For example, the variable attribute adds a variable name to the meta:
variable: helper.attribute<{
<TMeta extends BuilderMeta, const TVarName extends string>(
this: AR.This<TMeta>,
variable: TVarName,
initialValue: number,
): AR.WithRewriteMeta<
{
varNames: TMeta["varNames"] | TVarName;
},
typeof VariableVM
>;
}>(() => {});After variable "foo", 42;, the meta type changes from { varNames: never } to { varNames: "foo" }, enabling type-safe access to the variable later.
Transpilation Output Example
Given this GTS source:
define character {
id 1201 as Barbara;
health 10;
}The transpiler generates:
import { createDefine, createBinding } from "@gi-tcg/gts-runtime";
import __gts_rootVm from "@example/provider/vm";
const __gts_node_0 = {
name: "character",
positionals: () => [],
named: {
attributes: [
{ name: "id", positionals: () => [1201], named: null, binding: "public" },
{ name: "health", positionals: () => [10], named: null },
],
},
};
const __gts_bindings_0 = createBinding(__gts_rootVm, __gts_node_0);
export const Barbara = __gts_bindings_0[0];
createDefine(__gts_rootVm, __gts_node_0);At runtime:
createBindinginstantiates aRootBuilder, finds thecharacteraction, which creates aCharacterVMand parses the nested attributes- The
idattribute's binder returns1201 as CharacterHandle, which becomesBarbara createDefinere-runs the same process (for side effects like registration)
Provider Pattern
The runtime is designed to be used with a provider — a separate package that defines the ViewModels for a specific game domain. The provider exports:
./vm— the root ViewModel (default export)./runtime— re-exports from@gi-tcg/gts-runtime
This separation allows GTS files to be written against a stable language interface while the game implementation evolves independently.