metano-runtime
v2.3.0
Published
Runtime support library for Metano-generated TypeScript code
Downloads
960
Readme
metano-runtime
Runtime support library for code generated by the Metano
C# → TypeScript transpiler. You generally don't install or use this directly —
Metano adds it to your generated package.json#dependencies automatically, and
the generated code imports from it as needed.
What's in here
HashCode — C#-compatible hash combining
Used by generated hashCode() methods on records/classes. Implements
xxHash32 for fast, well-distributed hashes.
import { HashCode } from "metano-runtime";
const hc = new HashCode();
hc.add(someString);
hc.add(someNumber);
hc.add(someObject);
const hash = hc.toHashCode(); // numberHashSet<T> — value-equality set
A Set-like collection that uses equals()/hashCode() methods on objects when
available, instead of reference equality. Mirrors C# HashSet<T> semantics.
import { HashSet } from "metano-runtime";
class Point {
constructor(readonly x: number, readonly y: number) {}
equals(other: Point) { return this.x === other.x && this.y === other.y; }
hashCode() { const hc = new HashCode(); hc.add(this.x); hc.add(this.y); return hc.toHashCode(); }
}
const set = new HashSet<Point>();
set.add(new Point(1, 2));
set.has(new Point(1, 2)); // true — structural equalityEnumerable — LINQ for TypeScript
Lazy, composable sequences with the full LINQ API. Used by the transpiler to lower
C# IEnumerable<T> chains.
import { Enumerable } from "metano-runtime";
const result = Enumerable.from([1, 2, 3, 4, 5])
.where((x) => x > 1)
.select((x) => x * 2)
.toArray(); // [4, 6, 8, 10]Operators supported:
- Composition:
where,select,selectMany,orderBy,orderByDescending,thenBy,thenByDescending,take,takeWhile,skip,skipWhile,distinct,distinctBy,groupBy,concat,reverse,zip,append,prepend,union,intersect,except - Terminals:
toArray,toMap,toSet,first,firstOrDefault,last,lastOrDefault,single,singleOrDefault,any,all,count,sum,average,min,max,minBy,maxBy,contains,aggregate
linq() — descriptor-based pipeline + provider introspection
For IQueryable-style scenarios, the runtime also exposes a tagged-descriptor
pipeline: each operator (where, select, orderBy, …) is a standalone
factory that returns a { kind, …closures, apply, queryable? } descriptor.
linq(source, ...stages) runs the chain by calling each stage's apply,
returning a lazy Iterable<T> (or a scalar, for terminal-bearing chains).
import { getStages, linq, select, where } from "metano-runtime";
const query = linq(
users,
where((u) => u.age >= 18),
select((u) => u.name),
);
for (const name of query) {
console.log(name);
}When the result is an object (the lazy Iterable<T> from a composition chain,
or any object-typed terminal result like toArray), the runtime attaches the
descriptor list at a non-enumerable symbol-keyed slot — LINQ_STAGES,
registered via Symbol.for("metano-runtime/system/linq:stages"). An
introspecting consumer (IQueryable provider, query planner, debugger) reads
it via getStages:
import { getStages, linq, select, where } from "metano-runtime";
const query = linq(items, where((x) => x.active), select((x) => x.name));
const stages = getStages(query); // [WhereOp, SelectOp] | undefined
if (stages) {
// walk `stages[i].queryable` (ExprTree) to translate into SQL / GraphQL / …
}Contract:
- The slot is non-enumerable, so
JSON.stringify,{ ...spread }, andObject.keysdo not leak the chain into the wire shape. getStagesreturnsundefinedfor primitive results (scalar terminals likecount/sum) and for values produced outside this runtime.- The slot is scoped via
Symbol.for, so multiple module copies (duplicate npm resolutions) share the same key —getStagesfrom any copy sees the stages attached by any other. - The closure side-channel (
stage.apply) keeps working in parallel; a provider that cannot introspect a given stage may fall back to it.
ExprTree — queryable expression trees
When a LINQ chain opts into queryable mode (IQueryable<T> receiver or a
[Queryable] parameter on the C# side), the transpiler emits a paired
expression tree (QueryableMeta) alongside the closure. Providers walk the
tree by kind to translate predicates into SQL, OData, GraphQL, etc.
Supported node kinds today: param, capture, literal, member, call,
binary, unary, conditional, new (object-initializer projections
such as select(u => new UserDto { Name = u.Name })), and lambda
(nested arrows like where(u => u.orders.some(o => o.active))).
Param, capture, literal, and new nodes carry an optional qualified
type field shaped { name, from? } — name is the simple identifier
the provider dispatches on, and from is the cross-package origin (the
[EmitPackage] id) when the type lives in another package. Local and
primitive types omit from. This disambiguates same-named types defined
in different packages without forcing providers to parse module paths.
// where((p: Product) => p.unitPrice >= minPrice)
//
// `Product` lives in another package consumed via [EmitPackage("inventory")];
// `number` is a primitive (no origin), so `from` is omitted.
{
kind: "binary",
op: ">=",
left: {
kind: "member",
target: { kind: "param", name: "p", type: { name: "Product", from: "inventory" } },
member: "unitPrice",
},
right: { kind: "capture", name: "minPrice", type: { name: "number" } },
}ExprTreeVisitor — generic walker for IQueryable provider authors
metano-runtime/system/linq ships a stable visitor surface so providers
that translate QueryableMeta into another dialect (SQL, GraphQL, OData,
HTTP query strings) do not need to reimplement parameter scoping, the
recursive walker, captures lookup, or the primitive comparator. Three
entry points cover the common cases:
evaluateExprTree(tree, scope)— pure JS evaluation. Default behavior for mock / in-memory providers; resolves the param, looks up captures, short-circuits boolean ops, mirrors JS semantics on arithmetic.compileLambdaBody(meta, fallbackParamName?)— wraps the evaluator into a(item) => unknownpredicate by closing overmeta.captures. PassfallbackParamName(defaults to"x") for literal-only bodies that never mention the param.ExprTreeVisitor<R>— abstract base with onevisitXmethod per node kind. Default implementations recurse, so subclasses override only the nodes they translate. HelpersreadParamNameandcompareValuesare exported for providers that bypass the visitor.
import {
ExprTreeVisitor,
type ExprBinary,
type ExprLiteral,
type ExprMember,
type ExprParam,
} from "metano-runtime";
class SqlPredicate extends ExprTreeVisitor<string> {
protected override visitParam(node: ExprParam): string {
return node.name;
}
protected override visitLiteral(node: ExprLiteral): string {
return JSON.stringify(node.value);
}
protected override visitMember(node: ExprMember): string {
return `${this.visit(node.target)}.${node.member}`;
}
protected override visitBinary(node: ExprBinary): string {
return `(${this.visit(node.left)} ${node.op} ${this.visit(node.right)})`;
}
}
// new SqlPredicate().visit(meta.tree) → "(u.age >= 18)"The array-backed sample (targets/js/sample-queryable-arrays) uses
compileLambdaBody directly — its provider is now ~50 LOC of stage
dispatch with all generic walker concerns delegated to the runtime.
UUID — branded UUID type
A branded primitive that System.Guid maps to. At runtime it's literally a
string, so serialization and interop with ordinary string APIs work without
ceremony, but the type system distinguishes "a validated UUID" from "any old
string".
import { UUID } from "metano-runtime";
const id = UUID.newUuid(); // "550e8400-e29b-41d4-a716-446655440000"
const compact = UUID.newCompact(); // "550e8400e29b41d4a716446655440000"
const empty = UUID.empty; // "00000000-0000-0000-0000-000000000000"
const wrapped = UUID.create("abc-123"); // unchecked wrap, caller is trusted
if (UUID.isUuid(someValue)) {
// type narrowed to UUID here
}The Metano transpiler uses UUID.create() in JSON deserialization and
UUID.newUuid() as the lowering for Guid.NewGuid().
Primitive type guards
Runtime type predicates used by generated overload dispatchers:
import {
isInt32,
isString,
isBoolean,
isFloat64,
isBigInt,
// etc.
} from "metano-runtime";
if (isInt32(value)) {
// value is number && Number.isInteger(value) && within int32 range
}JSON serialization (metano-runtime/system/json)
A declarative serialization framework that the transpiler uses to convert
JsonSerializerContext subclasses. You don't construct these by hand — Metano
generates them from your C# code.
import { JsonSerializer } from "metano-runtime/system/json";
import { JsonContext } from "#/json-context"; // generated
const todo = new TodoItem("Write docs", false, "high");
const json = JsonSerializer.serialize(todo, JsonContext.default.todoItem);
// → { title: "Write docs", completed: false, priority: "high" }
const parsed = JsonSerializer.deserialize(json, JsonContext.default.todoItem);
// → TodoItem { title: "Write docs", completed: false, priority: "high" }Built-in type descriptors:
primitive— string, number, boolean passthroughtemporal—Temporal.*types viatoString()/parse()decimal—decimal.jsviatoNumber()/new Decimal()map—Map<K,V>↔ plain objectarray—T[]with recursive element handlinghashSet—HashSet<T>↔ arraybranded—[Branded]/[InlineWrapper]types (passthrough +.create())enum— string enum validationnumericEnum— numeric enum validationnullable— wraps inner descriptor with null handlingref— lazy reference to anotherTypeSpec
ImmutableCollection helpers
Pure functions that emulate System.Collections.Immutable.ImmutableList<T> /
ImmutableArray<T> operations on plain arrays, so immutable collections serialize
naturally without a custom wrapper:
import { ImmutableCollection } from "metano-runtime";
const a = [1, 2, 3];
const b = ImmutableCollection.add(a, 4); // [1, 2, 3, 4] — new array
const c = ImmutableCollection.removeAt(b, 0); // [2, 3, 4]Installation
You normally do not install this manually. Metano adds it to your generated
package.json#dependencies automatically. If you need to install it for some
reason:
# Bun (recommended for Metano projects)
bun add metano-runtime
# npm
npm install metano-runtime
# pnpm
pnpm add metano-runtimePeer dependencies
@js-temporal/polyfill— required if your transpiled code usesDateTime,DateOnly,TimeOnly, etc. Metano adds this to yourdependenciesautomatically whenever it detects a Temporal mapping.
Development
This package lives in the Metano monorepo.
cd js/metano-runtime
bun install
bun run build # tsgo -b .
bun test # 245+ testsTests live under test/ mirroring the src/ directory structure. Imports use
#/ subpath imports pointing at src/ (configured in package.json#imports).
License
MIT
