Skip to content

@repo/datadata/commands

Domain commands: the app's named, typed mutators (defineCommands) that the client runs for its optimistic view and the server runs authoritatively, each granted as an authorization kind of its own.

Functions

defineCommands

export declare function defineCommands<S extends SchemaRegistry, const B extends CommandBuilders<S>>(schemas: S, builders: B): DefinedCommands<S, B>;

Declare the app's domain commands against its schema registry, docType by docType. Returns the registry (docType → command name → definition) for the client (createClient's commands) and the server (createServer's, the Durable Object's, the in-memory factories') to share, after checking what the types cannot: every docType named is one the schema registry declares, and every args shape compiles to a validator. Both are definition-time errors, thrown here.

Each docType's callback builds that docType's commands with the command definer it is handed, which ties a command's run to the docType's data type and to the inferred type of its args shape. A command belongs to the docType it is defined under: two docTypes may each define a rename with args of their own, and a shared mutator is a function both callbacks call.

The callback also receives the docType's schema, so an args shape is picked from the document's own field shapes rather than written again: a schema is a plain literal, and a field taken from it by path carries its type and its constraints into the args, for the mutator's typing and the validation alike. Destructuring drops the fields the server stamps. An args shape may also cite the schema's named sub-shapes (defs) as { type: "ref", to }.

const COMMANDS = defineCommands(APP_SCHEMAS, {
  chat: (command, schema) => {
    const messages = schema.root.fields.messages;
    // The message minus what the server stamps, keyed as the record is.
    const { authorId, postedAt, ...authored } = messages.values.fields;
    return {
      postMessage: command({
        args: {
          type: "object",
          fields: { id: { type: "string", brand: messages.keyBrand }, ...authored },
        },
        contract: "relative",
        run(doc, args, context) {
          if (doc.data.messages[args.id]) return { refuse: { code: "exists", message: "…" } };
          const message = { ...args, authorId: context.principal.subject, postedAt: context.now };
          return { data: { ...doc.data, messages: { ...doc.data.messages, [args.id]: message } } };
        },
      }),
    };
  },
});

A command is its own authorization kind: a principal invokes postMessage on a document only where the docType's access.commands.postMessage (or a per-document entry's) admits it, whether or not it holds update. What the command protects is the writes made through it: a principal that holds update can patch past the mutator's checks, and the only rules every write meets are the schema's, so an invariant the app needs against every writer goes in the schema.

Interfaces

CommandContext

export interface CommandContext

What a mutator reads besides the document and its args. Every value here is the server's to stamp: now and principal are the facts an audit field or an ownership check is derived from (a timestamp or an author in args is replayable but client-controlled, so it is a claim, never the fact). On the client the same context is built from the connection's served identity and its clock, so a prediction agrees with the commit on everything but the values derived from here — now above all.

docType: string;

The docType of the document the command runs against.

now: number;

The time of execution in epoch milliseconds, stamped by the executor.

principal: Principal;

The principal the command executes as: the connection that sends it.

CommandDefiner

export interface CommandDefiner<S extends SchemaRegistry, Type extends keyof S & string>

The function a docType's defineCommands callback receives: it types one CommandSpec against that docType, inferring the args shape from the spec it is given, and returns it unchanged.

<const Args extends JsonSchemaValue>(spec: CommandSpec<S, Type, Args>): CommandSpec<S, Type, Args>;

Types spec against the docType and returns it.

CommandDefinition

export interface CommandDefinition

One command as the executor sees it, with no schema typing: the shape of its args, its contract, and its mutator. Which docType it belongs to is where it sits in the CommandRegistry. The typed form an app writes is CommandSpec, checked by defineCommands.

args: JsonSchemaValue;

The shape of args in the schema's field vocabulary (an object value in the common case), validated before the mutator runs; a failure is refused invalidArgs. It may cite the docType's named sub-shapes as { type: "ref", to }, resolved through defs.

contract: CommandContract;

What the command promises under concurrency; see CommandContract.

defs?: Readonly<Record<string, JsonSchemaValue>>;

The docType's named sub-shapes (its schema's defs, {} when it declares none), which args may cite; defineCommands attaches them from the schema registry. A registry assembled by hand may leave them out.

run(document: DatadataDocument, args: never, context: CommandContext): CommandOutcome<unknown>;

The mutator: pure, synchronous and single-document. It runs speculatively on the client at every render, authoritatively once on the server, so it must not reach outside its inputs (no network, no clock but context.now, no randomness). document is the mutator's own copy: edit its data in place and return it, or return new data, as with a client-side update.

CommandRefusal

export interface CommandRefusal

A mutator's refusal: an app-defined code (the client exposes it on the rejection as CommandRefusedError.code) and a message for people.

code: string;

An app-defined, stable code the caller can branch on ("notAuthor").

message: string;

Why the command was refused, for people.

CommandSpec

export interface CommandSpec<S extends SchemaRegistry, Type extends keyof S & string, Args extends JsonSchemaValue>

The typed form of one command in a defineCommands call, under the docType Type it is defined for: run receives a document of that type's data and args of the args shape's inferred type, a ref in it resolved through the docType's defs.

args: Args;

The args shape; see CommandDefinition.args.

contract: CommandContract;

What the command promises under concurrency; see CommandContract.

run(document: DatadataDocument<InferJsonSchema<S[Type]>>, args: InferValue<Args, SchemaDefs<S[Type]>>, context: CommandContext): CommandOutcome<InferJsonSchema<S[Type]>>;

The mutator; see CommandDefinition.run.

UntypedCommandInvocation

export interface UntypedCommandInvocation

A command a session sends with nothing typed: any docId and name, an optional type and unknown args, checked at runtime only. Every CommandInvocation is one.

args: unknown;

The command's args, validated against its args shape.

docId: string;

The document the command runs on.

name: string;

The command's name, as the command map defines it for the docType.

type?: string;

The document's docType, which the command is defined for; checked against the document.

Types

CommandArgs

export type CommandArgs<C, Type extends keyof C, Name extends keyof C[Type]> = C[Type][Name] extends {
    args: infer A;
} ? InferValue<A, SchemaDefs<C[Type][Name]>> : never;

The args type of command Name of docType Type in the command map C.

CommandArgsInput

export type CommandArgsInput<C, Type extends keyof C, Name extends keyof C[Type]> = string extends keyof C[Type] ? unknown : C[Type][Name] extends {
    args: infer A;
} ? InferValueInput<A, SchemaDefs<C[Type][Name]>> : never;

The args a caller sends for command Name of docType Type in the command map C: the input shape of its args, where a field with a default may be left out (the executor fills it before the mutator runs, which receives CommandArgs). unknown when C does not name its commands (a plain CommandRegistry).

CommandBuilders

export type CommandBuilders<S extends SchemaRegistry> = {
    readonly [Type in keyof S & string]?: (command: CommandDefiner<S, Type>, schema: S[Type]) => Readonly<Record<string, CommandDefinition>>;
};

One callback per docType, each building that docType's commands with the definer it is handed and the docType's own schema, whose field shapes an args shape picks from (see defineCommands).

CommandContract

export type CommandContract = "relative" | "overwrite" | "exact";

What a command promises under concurrency — which of the three shapes its registration declares, so the guard follows from the promise rather than being chosen per call (see docs/domain-commands-design.md, "The contract"):

- "relative": the intent is relative to the state the command finds, so two of them both count — two increments make +2. No guard: the server serializes them and each runs against the state it meets. - "overwrite": the command sets what it was given, so the later of two wins, as two patches on one path would. No guard either; a mutator that must not overwrite compares a revision it is handed in args with the one it finds and refuses, which reads as a domain refusal. - "exact": the intent is absolute over the whole document the caller saw (an approve), so the command is sent with a "sequence" guard and refused preconditionFailed when anything moved; the caller re-reads and decides again. Its prediction renders only while the document is the one the caller saw.

CommandInvocation

export type CommandInvocation<C extends CommandRegistry> = string extends keyof C ? UntypedCommandInvocation : {
    [Type in keyof C & string]: {
        [Name in keyof C[Type] & string]: {
            docId: string;
            type: Type;
            name: Name;
            args: CommandArgsInput<C, Type, Name>;
        };
    }[keyof C[Type] & string];
}[keyof C & string];

One command a session sends (LiveSession.command) under the command map C: a { docId, type, name, args } shape per command C defines, so type and name pick the command and args is typed by its shape (CommandArgsInput). type names the docType the command is defined for and must be the document's own. A map that does not name its commands (a plain CommandRegistry) takes any name, an optional type and unknown args, checked at runtime only.

CommandOutcome

export type CommandOutcome<Data> = {
    data: Data;
} | {
    refuse: CommandRefusal;
};

What a mutator returns: the document's new data, or a refusal. A mutator never throws for a domain reason; a throw is a bug and is reported as the reserved COMMAND_REFUSAL_CODES mutatorError refusal.

CommandRefusalCode

export type CommandRefusalCode = (typeof COMMAND_REFUSAL_CODES)[keyof typeof COMMAND_REFUSAL_CODES];

One of COMMAND_REFUSAL_CODES.

CommandRegistry

export type CommandRegistry = Readonly<Record<string, Readonly<Record<string, CommandDefinition>>>>;

The app's commands, keyed by docType and then by command name — what defineCommands returns, and what the client and the server are given. A command belongs to one docType: two docTypes may each define a command of the same name, with args shapes and mutators of their own, and a name sent at a docType that does not define it is refused unknownCommand.

DefinedCommands

export type DefinedCommands<S extends SchemaRegistry, B> = {
    readonly [Type in keyof B]-?: B[Type] extends (...args: never[]) => infer Commands ? {
        readonly [Name in keyof Commands]: Commands[Name] & {
            readonly defs: SchemaDefs<Type extends keyof S ? S[Type] : never>;
        };
    } : never;
};

The registry defineCommands returns for the builders B over the schema registry S: each docType's map as built, every command carrying the docType's defs.

SchemaDefs

export type SchemaDefs<S> = S extends {
    defs: infer D;
} ? D : {};

A schema's defs map, {} when it declares none: what a ref in a command's args resolves through.

Variables

COMMAND_REFUSAL_CODES

COMMAND_REFUSAL_CODES: {
    readonly unknownCommand: "unknownCommand";
    readonly invalidArgs: "invalidArgs";
    readonly mutatorError: "mutatorError";
}

The refusal codes the library reserves: a refusal with one of these came from the executor, not the mutator.