classDiagram
class Panproto {
+init() Panproto$
+protocol(name) Protocol
+defineProtocol(spec) Protocol
+migration(src, tgt) MigrationBuilder
+checkExistence(src, tgt, builder) ExistenceReport
+compose(m1, m2) CompiledMigration
+diff(old, new) DiffReport
+Symbol.dispose()
}
class Protocol {
+name: string
+spec: ProtocolSpec
+schema() SchemaBuilder
+Symbol.dispose()
}
class SchemaBuilder {
+vertex(id, kind, opts?) SchemaBuilder
+edge(src, tgt, kind, opts?) SchemaBuilder
+hyperEdge(id, kind, sig, parent) SchemaBuilder
+constraint(vertex, sort, value) SchemaBuilder
+required(vertex, edges) SchemaBuilder
+build() BuiltSchema
}
class BuiltSchema {
+data: SchemaData
+protocol: string
+vertices: Record
+edges: Edge[]
+Symbol.dispose()
}
class MigrationBuilder {
+map(src, tgt) MigrationBuilder
+mapEdge(src, tgt) MigrationBuilder
+resolve(src, tgt, edge) MigrationBuilder
+compile() CompiledMigration
}
class CompiledMigration {
+spec: MigrationSpec
+lift(record) LiftResult
+get(record) GetResult
+put(view, complement) LiftResult
+Symbol.dispose()
}
Panproto --> Protocol : protocol()
Protocol --> SchemaBuilder : schema()
SchemaBuilder --> BuiltSchema : build()
Panproto --> MigrationBuilder : migration()
MigrationBuilder --> CompiledMigration : compile()
14 @panproto/core: The TypeScript SDK
The @panproto/core npm package wraps the WASM module in a fluent, type-safe API with automatic resource management. Most developers interact with panproto through this SDK.
For a quick-reference listing of all public APIs with parameter types, see tutorial appendix B.
14.1 Architecture overview
The SDK is organized into six core classes, each with a focused responsibility:
14.2 The panproto class
Panproto is the main entry point. It loads the WASM module and provides the top-level API:
export class Panproto implements Disposable {
readonly #wasm: WasmModule;
readonly #protocols: Map<string, Protocol>;
private constructor(wasm: WasmModule) {
this.#wasm = wasm;
this.#protocols = new Map();
}
/** The WASM module reference. Internal use only. */
get _wasm(): WasmModule {
return this.#wasm;
}
/**
* Initialize the panproto SDK by loading the WASM module.
*
* @param input - URL to the wasm-bindgen glue module, or a pre-imported
* glue module object (for bundler environments like Vite).
* Defaults to the bundled glue module.
* @returns An initialized Panproto instance
* @throws {@link import('./types.js').WasmError} if WASM loading failsKey methods:
init(wasmUrl?): Async factory. Loads the WASM binary (bundled by default) and returns a ready instance.protocol(name): Get or auto-register a built-in protocol (atproto,sql,protobuf,graphql,json-schema). Custom protocols must be registered first withdefineProtocol.defineProtocol(spec): Register a custom protocol specification.migration(src, tgt): Start building a migration between two schemas.checkExistence(src, tgt, builder): Validate a migration against protocol-derived existence conditions.compose(m1, m2): Compose two compiled migrations into one.diff(old, new): Compute a structural diff between two schemas.
Panproto implements Disposable. When disposed, it releases all cached protocol handles.
14.3 Protocol
A Protocol holds a WASM-side handle to the registered protocol specification and provides the schema() factory:
export class Protocol implements Disposable {
readonly #handle: WasmHandle;
readonly #spec: ProtocolSpec;
readonly #wasm: WasmModule;
constructor(handle: WasmHandle, spec: ProtocolSpec, wasm: WasmModule) {
this.#handle = handle;
this.#spec = spec;
this.#wasm = wasm;
}
/** The protocol name. */
get name(): string {
return this.#spec.name;
}
/** The full protocol specification. */
get spec(): ProtocolSpec {
return this.#spec;
}
/** The edge rules for this protocol. */
get edgeRules(): readonly EdgeRule[] {
return this.#spec.edgeRules;
}
/** The constraint sorts for this protocol. */
get constraintSorts(): readonly string[] {
return this.#spec.constraintSorts;
}
/** The object kinds for this protocol. */
get objectKinds(): readonly string[] {
return this.#spec.objKinds;
}
/** The WASM handle. Internal use only. */
get _handle(): WasmHandle {
return this.#handle;
}Five built-in protocol specs are provided: ATPROTO_SPEC, SQL_SPEC, PROTOBUF_SPEC, GRAPHQL_SPEC, and JSON_SCHEMA_SPEC. They’re auto-registered on first access via panproto.protocol('atproto').
14.4 Schemabuilder and builtschema
SchemaBuilder is an immutable fluent builder. Each method returns a new builder instance, leaving the original unchanged. This makes it safe to branch schema definitions:
* .build();
* ```
*/
export class SchemaBuilder {
readonly #protocolName: string;
readonly #protocolHandle: WasmHandle;
readonly #wasm: WasmModule;
readonly #ops: readonly SchemaOp[];
readonly #vertices: ReadonlyMap<string, Vertex>;
readonly #edges: readonly Edge[];
readonly #hyperEdges: ReadonlyMap<string, HyperEdge>;
readonly #constraints: ReadonlyMap<string, readonly Constraint[]>;
readonly #required: ReadonlyMap<string, readonly Edge[]>;
constructor(
protocolName: string,
protocolHandle: WasmHandle,
wasm: WasmModule,
ops: readonly SchemaOp[] = [],
vertices: ReadonlyMap<string, Vertex> = new Map(),
edges: readonly Edge[] = [],
hyperEdges: ReadonlyMap<string, HyperEdge> = new Map(),
constraints: ReadonlyMap<string, readonly Constraint[]> = new Map(),
required: ReadonlyMap<string, readonly Edge[]> = new Map(),
) {
this.#protocolName = protocolName;
this.#protocolHandle = protocolHandle;
this.#wasm = wasm;
this.#ops = ops;
this.#vertices = vertices;
this.#edges = edges;
this.#hyperEdges = hyperEdges;The builder accumulates SchemaOp objects (matching the Rust BuildOp tagged enum). On .build(), it packs them as MessagePack, sends them to WASM, and wraps the returned handle:
* @returns The validated, built schema
* @throws {@link SchemaValidationError} if the schema is invalid
*/
build(): BuiltSchema {
const opsBytes = packSchemaOps([...this.#ops]);
const rawHandle = this.#wasm.exports.build_schema(
this.#protocolHandle.id,
opsBytes,
);
const handle = createHandle(rawHandle, this.#wasm);
const data: SchemaData = {
protocol: this.#protocolName,
vertices: Object.fromEntries(this.#vertices),
edges: [...this.#edges],
hyperEdges: Object.fromEntries(this.#hyperEdges),
constraints: Object.fromEntries(
Array.from(this.#constraints.entries()).map(([k, v]) => [k, [...v]]),
),
required: Object.fromEntries(
Array.from(this.#required.entries()).map(([k, v]) => [k, [...v]]),
),
variants: {},BuiltSchema is the validated result. It holds both a WASM handle (for passing to migration functions) and a local SchemaData snapshot (for introspection without crossing the boundary):
return new BuiltSchema(handle, data, this.#wasm);
}
}
/**
* A validated, built schema with a WASM-side handle.
*
* Implements `Disposable` for automatic cleanup of the WASM resource.
*/
export class BuiltSchema implements Disposable {
readonly #handle: WasmHandle;
readonly #data: SchemaData;
readonly #wasm: WasmModule;
constructor(handle: WasmHandle, data: SchemaData, wasm: WasmModule) {
this.#handle = handle;
this.#data = data;
this.#wasm = wasm;
}
/** The WASM handle for this schema. Internal use only. */
get _handle(): WasmHandle {
return this.#handle;
}
/** The WASM module reference. Internal use only. */
get _wasm(): WasmModule {
return this.#wasm;
}
/** The schema data (vertices, edges, constraints, etc.). */
get data(): SchemaData {
return this.#data;
}
/** The protocol name this schema belongs to. */
get protocol(): string {
return this.#data.protocol;
}
/** All vertices in the schema. */
get vertices(): Readonly<Record<string, Vertex>> {
return this.#data.vertices;
}
/** All edges in the schema. */14.5 Migrationbuilder and compiledmigration
MigrationBuilder follows the same immutable fluent pattern. It accumulates vertex mappings, edge mappings, and resolvers:
export class MigrationBuilder {
readonly #src: BuiltSchema;
readonly #tgt: BuiltSchema;
readonly #wasm: WasmModule;
readonly #vertexMap: ReadonlyMap<string, string>;
readonly #edgeMap: readonly [Edge, Edge][];
readonly #resolvers: readonly [[string, string], Edge][];
constructor(
src: BuiltSchema,
tgt: BuiltSchema,
wasm: WasmModule,
vertexMap: ReadonlyMap<string, string> = new Map(),
edgeMap: readonly [Edge, Edge][] = [],
resolvers: readonly [[string, string], Edge][] = [],
) {
this.#src = src;
this.#tgt = tgt;
this.#wasm = wasm;
this.#vertexMap = vertexMap;
this.#edgeMap = edgeMap;
this.#resolvers = resolvers;
}
/**
* Map a source vertex to a target vertex.
*
* @param srcVertex - Vertex id in the source schema
* @param tgtVertex - Vertex id in the target schema
* @returns A new builder with the mapping added
*/
map(srcVertex: string, tgtVertex: string): MigrationBuilder {
const newMap = new Map(this.#vertexMap);
newMap.set(srcVertex, tgtVertex);
return new MigrationBuilder(
this.#src,
this.#tgt,
this.#wasm,
newMap,
this.#edgeMap,
this.#resolvers,
);
}The compile() method packs the mapping and sends it to the compile_migration WASM entry point. The result is a CompiledMigration with three data-path methods:
/**
* Compile the migration for fast per-record application.
*
* Sends the migration specification to WASM for compilation.
* The resulting `CompiledMigration` can be used to transform records.
*
* @returns A compiled migration ready for record transformation
* @throws {@link MigrationError} if compilation fails
*/
compile(): CompiledMigration {
const edgeMap = new Map(
this.#edgeMap.map(([src, tgt]) => [
{ src: src.src, tgt: src.tgt, kind: src.kind, name: src.name ?? null },
{ src: tgt.src, tgt: tgt.tgt, kind: tgt.kind, name: tgt.name ?? null },
] as const),
);
const resolver = new Map(
this.#resolvers.map(([[s, t], e]) => [
[s, t] as const,
{ src: e.src, tgt: e.tgt, kind: e.kind, name: e.name ?? null },
] as const),
);
const mapping = packMigrationMapping({
vertex_map: Object.fromEntries(this.#vertexMap),
edge_map: edgeMap,
hyper_edge_map: {},
label_map: new Map(),
resolver,
});
try {
const rawHandle = this.#wasm.exports.compile_migration(
this.#src._handle.id,
this.#tgt._handle.id,
mapping,
);
const handle = createHandle(rawHandle, this.#wasm);
return new CompiledMigration(handle, this.#wasm, this.toSpec());
} catch (error) {
throw new MigrationError(
`Failed to compile migration: ${error instanceof Error ? error.message : String(error)}`,
{ cause: error },
);
}
}
}lift(record): Forward-only transformation. The hot path: data goes through WASM as MessagePack bytes with no intermediate JS-heap allocation.get(record): Bidirectional get: extract a projected view and an opaque complement.put(view, complement): Bidirectional put: restore a full record from a (possibly modified) view and the complement fromget.
The complement from get() is an opaque Uint8Array. It captures the data discarded by the forward projection, enabling lossless round-tripping. Treat it as a black box; its internal format is a MessagePack-encoded Rust Complement struct.
14.6 Wasmhandle and Resource safety
Every WASM-side resource is wrapped in a WasmHandle, which implements Disposable for use with the using declaration:
parse_expr: glue.parse_expr,
eval_func_expr: glue.eval_func_expr,
execute_query: glue.execute_query,
// Phase 12: Fiber, hom, and graph
fiber_at: glue.fiber_at,
fiber_decomposition_wasm: glue.fiber_decomposition_wasm,
poly_hom: glue.poly_hom,
preferred_conversion_path: glue.preferred_conversion_path,
conversion_distance: glue.conversion_distance,
};
const memory: WebAssembly.Memory = initOutput.memory;
if (!memory) {
throw new WasmError('WASM module missing memory export');
}
return { exports, memory };
} catch (error) {
if (error instanceof WasmError) throw error;
throw new WasmError(
`Failed to load WASM module: ${error instanceof Error ? error.message : String(error)}`,
{ cause: error },
);
}
}
// ---------------------------------------------------------------------------
// Handle registry — prevents resource leaks
// ---------------------------------------------------------------------------
/** Weak reference registry for leaked handle detection. */
const leakedHandleRegistry = new FinalizationRegistry<CleanupInfo>((info) => {
// Safety net: if a disposable wrapper is GC'd without being disposed,
// free the underlying WASM handle.
try {
info.freeHandle(info.handle);
} catch {
// WASM module may already be torn down; swallow.Two layers of safety prevent handle leaks:
Symbol.dispose: Explicit cleanup. Called automatically byusingor manually by the consumer.FinalizationRegistry: Safety net. If aWasmHandleis garbage-collected without being disposed, the registry callsfree_handleto release the WASM resource.
The FinalizationRegistry is a last resort, not a primary mechanism. GC timing is non-deterministic, so relying on it can cause resource exhaustion under load. Always prefer explicit disposal via using or Symbol.dispose().
The FinalizationRegistry will eventually clean it up, but “eventually” is unpredictable. Under load, leaked handles accumulate until GC runs, which can exhaust WASM memory. Always use using or explicit [Symbol.dispose]() calls.
14.7 Messagepack encoding helpers
The msgpack.ts module provides four encoding helpers that wrap @msgpack/msgpack:
export function packToWasm(value: unknown): Uint8Array {
return encode(value);
}
/**
* Decode MessagePack bytes received from WASM.
*
* @typeParam T - The expected decoded type
* @param bytes - MessagePack-encoded bytes from WASM
* @returns The decoded value
*/
export function unpackFromWasm<T = unknown>(bytes: Uint8Array): T {
return decode(bytes) as T;
}
/**
* Encode a schema operations list for the `build_schema` entry point.
*
* @param ops - Array of builder operations
* @returns MessagePack-encoded bytes
*/
export function packSchemaOps(ops: readonly SchemaOp[]): Uint8Array {
return encode(ops);
}
/**
* Encode a migration mapping for WASM entry points.
*
* @param mapping - The migration mapping object
* @returns MessagePack-encoded bytes
*/
export function packMigrationMapping(mapping: MigrationMapping): Uint8Array {
// The Rust Migration struct uses `map_as_vec` for fields with non-string keys,
// which deserializes from a sequence of [key, value] pairs, not a map.packToWasm(value): Encode any JS value to MessagePack bytes for a WASM entry point.unpackFromWasm<T>(bytes): Decode MessagePack bytes from WASM into a typed JS value.packSchemaOps(ops): Encode aSchemaOp[]for thebuild_schemaentry point.packMigrationMapping(mapping): Encode aMigrationMappingforcompile_migrationorcheck_existence.
The SchemaOp interface uses serde’s internally-tagged format: the op field acts as the discriminant, and all variant fields sit at the same level.
14.8 Lens API
The lens.ts module provides three handle classes for bidirectional schema transformations: ProtolensChainHandle for schema-independent lens families, LensHandle for concrete lenses, and SymmetricLensHandle for symmetric bidirectional sync.
ProtolensChainHandle wraps a WASM-side protolens chain and can be instantiated against a concrete schema to produce a LensHandle:
export class ProtolensChainHandle implements Disposable {
readonly #handle: WasmHandle;
readonly #wasm: WasmModule;
constructor(handle: WasmHandle, wasm: WasmModule) {
this.#handle = handle;
this.#wasm = wasm;
}
/** The underlying WASM handle. Internal use only. */
get _handle(): WasmHandle {
return this.#handle;
}
/**
* Auto-generate a protolens chain between two schemas.
*
* @param schema1 - The source schema
* @param schema2 - The target schema
* @param wasm - The WASM module
* @returns A ProtolensChainHandle wrapping the generated chain
* @throws {@link WasmError} if the WASM call fails
*/
static autoGenerate(schema1: BuiltSchema, schema2: BuiltSchema, wasm: WasmModule): ProtolensChainHandle {
try {
const rawHandle = wasm.exports.auto_generate_protolens(schema1._handle.id, schema2._handle.id);
return new ProtolensChainHandle(createHandle(rawHandle, wasm), wasm);
} catch (error) {
throw new WasmError(
`auto_generate_protolens failed: ${error instanceof Error ? error.message : String(error)}`,
{ cause: error },
);
}
}
/**
* Instantiate this protolens chain against a concrete schema.
*
* @param schema - The schema to instantiate against
* @returns A LensHandle for the instantiated lens
* @throws {@link WasmError} if the WASM call fails
*/
instantiate(schema: BuiltSchema): LensHandle {
try {
const rawHandle = this.#wasm.exports.instantiate_protolens(this.#handle.id, schema._handle.id);
return new LensHandle(createHandle(rawHandle, this.#wasm), this.#wasm);
} catch (error) {
throw new WasmError(
`instantiate_protolens failed: ${error instanceof Error ? error.message : String(error)}`,
{ cause: error },
);
}
}LensHandle wraps a concrete lens with get, put, and law-checking operations:
try {
const hintsBytes = packToWasm(hints);
const rawHandle = wasm.exports.auto_generate_protolens_with_hints(
schema1._handle.id,
schema2._handle.id,
hintsBytes,
);
return new ProtolensChainHandle(createHandle(rawHandle, wasm), wasm);
} catch (error) {
throw new WasmError(
`auto_generate_protolens_with_hints failed: ${error instanceof Error ? error.message : String(error)}`,
{ cause: error },
);
}
}
/** Release the underlying WASM resource. */
[Symbol.dispose](): void {
this.#handle[Symbol.dispose]();
}
}
// ---------------------------------------------------------------------------
// PipelineBuilder — fluent API for constructing protolens chains
// ---------------------------------------------------------------------------
/**
* Fluent builder for constructing protolens chains from combinator steps.
*
* Each method appends a step to the pipeline. Call `build()` to compile
* the steps into a `ProtolensChainHandle` via the WASM boundary.
*
* ```ts
* const chain = new PipelineBuilder(wasm)
* .renameField('post', 'text', 'body')
* .addField('post', 'createdAt', 'string')
* .build();
* ```
*/
export class PipelineBuilder {
readonly #steps: PipelineStep[] = [];
readonly #wasm: WasmModule;
constructor(wasm: WasmModule) {
this.#wasm = wasm;
}
/** Rename a field (vertex name + JSON property key). */
renameField(parent: string, oldName: string, newName: string): this {
this.#steps.push({ step_type: 'rename_field', parent, name: oldName, target: newName });
return this;
}
/** Remove a field (drop sort with edge cascade). */
removeField(field: string): this {
this.#steps.push({ step_type: 'remove_field', name: field });
return this;
}
/** Add a field with a default value. */
addField(parent: string, fieldName: string, fieldKind: string): this {
this.#steps.push({ step_type: 'add_field', parent, name: fieldName, kind: fieldKind });ProtolensChainHandle.autoGenerate(schema1, schema2, wasm): Auto-generate a protolens chain between two schemas.ProtolensChainHandle.instantiate(schema): Instantiate the chain against a concrete schema.ProtolensChainHandle.compose(other): Compose with another chain.ProtolensChainHandle.fuse(): Fuse all steps into a single protolens.ProtolensChainHandle.lift(morphismBytes): Lift along a theory morphism.LensHandle.autoGenerate(schema1, schema2, wasm): Auto-generate and instantiate a lens.LensHandle.get(record): Forward projection: extract view and complement.LensHandle.put(view, complement): Backward put: restore from view and complement.LensHandle.checkLaws(instance): Verify GetPut and PutGet laws.
14.9 Schema enrichment
The SchemaBuilder supports enrichment through constraints, which encode defaults, coercions, and merge policies:
// Add constraints to encode enriched schema properties
using schema = protocol.schema()
.vertex('post:body', 'object')
.vertex('post:body.text', 'string')
.edge('post:body', 'post:body.text', 'prop', { name: 'text' })
.constraint('post:body.text', 'maxLength', '3000') // refinement type
.constraint('post:body.text', 'minLength', '1') // lower bound
.build();Constraints serve triple duty: they encode refinement types (value bounds), default behaviors (when a field has a known initial value), and coercion hints (when a field can be safely converted between representations). The protocol’s constraint_sorts array determines which constraint sorts are recognized during compatibility checking.
14.10 Migration analysis
The SDK provides analysis capabilities through the diff/classify pipeline:
// Compute structural diff
const diffReport = panproto.diff(oldSchema, newSchema);
// Classify into breaking vs. non-breaking
const compat = panproto.classify(diffReport);
// Coverage: fraction of source vertices surviving in target
const coverage = compat.non_breaking.length /
(compat.breaking.length + compat.non_breaking.length);For optic classification, the protolens chain structure reveals whether a migration is an isomorphism (lossless, rename-only), a lens (lossy, drops data), or more complex:
// Auto-generate protolens
using chain = ProtolensChainHandle.autoGenerate(schema1, schema2, wasm);
// Fuse to analyze the composed transform
using fused = chain.fuse();
// Serialize to inspect the transform structure
const json = fused.toJSON();
// json.complement_constructor === "Empty" means isomorphism
// json.complement_constructor contains "DroppedSortData" means lens14.11 GAT Engine access
The SDK provides direct access to the GAT engine for theory construction and composition:
// Create theories
using thGraph = panproto.createTheory({
name: 'ThGraph',
sorts: [{ name: 'Vertex', params: [] }, { name: 'Edge', params: [] }],
ops: [
{ name: 'src', inputs: [['e', 'Edge']], output: 'Vertex' },
{ name: 'tgt', inputs: [['e', 'Edge']], output: 'Vertex' },
],
eqs: [],
});
// Compose theories via colimit
using shared = panproto.createTheory({ name: 'Shared', sorts: [{ name: 'Vertex', params: [] }], ops: [], eqs: [] });
using composed = panproto.colimitTheories(thGraph, thConstraint, shared);14.12 Expression parser & evaluator
The SDK provides three functions for working with the expression language:
parseExpr(source: string, wasm: WasmModule): Exprparses Haskell-style source text into an AST.evalExpr(expr: Expr, env: Record<string, Literal> | undefined, wasm: WasmModule): Literalevaluates an expression with optional environment bindings.formatExpr(source: string, wasm: WasmModule): stringround-trip formats an expression into canonical form.
import { parseExpr, evalExpr, formatExpr } from '@panproto/core';
const expr = parseExpr('\\x -> x + 1', wasm);
const result = evalExpr(expr, { x: 2 }, wasm); // 3
const pretty = formatExpr('\\x->x+ 1', wasm); // "\\x -> x + 1"14.13 Declarative query API
The executeQuery function runs a declarative query against an instance:
executeQuery(query: InstanceQuery, instance: Instance, wasm: WasmModule): QueryMatch[]InstanceQuerytype:{ anchor, predicate?, groupBy?, projection?, limit?, path? }QueryMatchtype:{ nodeId, anchor, value, fields }
import { executeQuery } from '@panproto/core';
const matches = executeQuery(
{ anchor: 'post:body', predicate: { field: 'text', op: 'contains', value: 'hello' } },
instance,
wasm,
);14.14 VCS integration
The SDK exposes version control operations for schemas and data:
// Initialize a repository
using repo = panproto.vcsInit('atproto');
// Stage and commit schema changes
panproto.vcsAdd(repo, schema);
const hash = panproto.vcsCommit(repo, 'add post schema', 'author@example.com');
// Query history
const log = panproto.vcsLog(repo, 10);
const status = panproto.vcsStatus(repo);14.15 Type definitions
The types.ts module defines all TypeScript interfaces that mirror Rust structures. Key types include:
WasmExports: The raw WASM module interface listing all entry pointsProtocolSpec,Vertex,Edge,HyperEdge,Constraint: Schema domain typesMigrationSpec,LiftResult,GetResult: Migration domain typesExistenceReport,ExistenceError: Existence checking resultsDiffReport,SchemaChange,Compatibility: Schema diff resultsTheorySpec,SortSpec,OperationSpec,EquationSpec: GAT theory typesProtolensChainSpec,ComplementConstructor: Protolens types
export type Compatibility = 'fully-compatible' | 'backward-compatible' | 'breaking';
/** Schema diff report. */
export interface DiffReport {
readonly compatibility: Compatibility;
readonly changes: readonly SchemaChange[];
}
// ---------------------------------------------------------------------------
// Existence checking
// ---------------------------------------------------------------------------
/** A structured existence error. */14.16 Error hierarchy
The SDK defines a four-class error hierarchy, all extending PanprotoError:
export interface DirectedEquation {
readonly name: string;
readonly lhs: Term;
readonly rhs: Term;
readonly implTerm: Expr;
readonly inverse?: Expr | undefined;
}
/** Conflict resolution strategy. */
export type ConflictStrategy =
| { readonly type: 'keep_left' }
| { readonly type: 'keep_right' }
| { readonly type: 'fail' }
| { readonly type: 'custom'; readonly expr: Expr };
/** Conflict resolution policy. */
export interface ConflictPolicy {
readonly name: string;
readonly valueKind: ValueKind;
readonly strategy: ConflictStrategy;
}
/** Pattern for expression matching. */
export type Pattern =
| { readonly type: 'wildcard' }
| { readonly type: 'var'; readonly name: string }
| { readonly type: 'lit'; readonly value: Literal }
| { readonly type: 'record'; readonly fields: readonly [string, Pattern][] }
| { readonly type: 'list'; readonly items: readonly Pattern[] };
/** Expression in the pure functional language. */
export type Expr =
| { readonly type: 'var'; readonly name: string }
| { readonly type: 'lam'; readonly param: string; readonly body: Expr }
| { readonly type: 'app'; readonly func: Expr; readonly arg: Expr }
| { readonly type: 'lit'; readonly value: Literal }
| { readonly type: 'record'; readonly fields: readonly [string, Expr][] }
| { readonly type: 'list'; readonly items: readonly Expr[] }
| { readonly type: 'field'; readonly expr: Expr; readonly name: string }
| { readonly type: 'index'; readonly expr: Expr; readonly index: Expr }
| { readonly type: 'match'; readonly scrutinee: Expr; readonly arms: readonly [Pattern, Expr][] }
| { readonly type: 'let'; readonly name: string; readonly value: Expr; readonly body: Expr }PanprotoError: Base class for all panproto errors.WasmError: Errors from the WASM boundary (load failures, call failures, disposed handles).SchemaValidationError: Schema building errors, with a list of individual error strings.MigrationError: Migration compilation or composition errors.ExistenceCheckError: Existence check failures, bundling the fullExistenceReport.
14.17 Usage pattern
A typical SDK session:
import { Panproto } from '@panproto/core';
// Initialize (loads WASM)
using panproto = await Panproto.init();
// Get a protocol
const atproto = panproto.protocol('atproto');
// Build schemas
using oldSchema = atproto.schema()
.vertex('post', 'record', { nsid: 'app.bsky.feed.post' })
.vertex('post:body', 'object')
.edge('post', 'post:body', 'record-schema')
.build();
using newSchema = atproto.schema()
.vertex('post', 'record', { nsid: 'app.bsky.feed.post' })
.vertex('post:body', 'object')
.vertex('post:body.tags', 'array')
.edge('post', 'post:body', 'record-schema')
.edge('post:body', 'post:body.tags', 'prop', { name: 'tags' })
.build();
// Compile migration
using migration = panproto.migration(oldSchema, newSchema)
.map('post', 'post')
.map('post:body', 'post:body')
.compile();
// Transform records
const result = migration.lift(inputRecord);The using keyword (TC39 Explicit Resource Management) ensures all WASM handles are freed when the variables go out of scope. If your runtime doesn’t support using, call [Symbol.dispose]() manually or use a try/finally block.